diff --git a/.air.toml b/.air.toml index 78321b7a6a3..87083f809aa 100644 --- a/.air.toml +++ b/.air.toml @@ -7,7 +7,6 @@ exclude_unchanged = true follow_symlink = true include_dir = ["apps", "conf", "devenv/dev-dashboards", "pkg", "public/views"] include_ext = ["go", "ini", "toml", "html", "json"] -pre_cmd = ["make gen-go"] stop_on_error = true send_interrupt = true kill_delay = 500 diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js index 8d742ae5187..3b44630965e 100644 --- a/.betterer.eslint.config.js +++ b/.betterer.eslint.config.js @@ -35,7 +35,7 @@ module.exports = [ 'data/', 'deployment_tools_config.json', 'devenv', - 'e2e/test-plugins', + 'e2e-playwright/test-plugins', 'e2e/tmp', 'packages/grafana-ui/src/components/Icon/iconBundle.ts', 'pkg', @@ -104,6 +104,7 @@ module.exports = [ '**/__mocks__/**', '**/public/test/**', '**/mocks.{ts,tsx}', + '**/mocks/**/*.{ts,tsx}', '**/spec/**/*.{ts,tsx}', ], rules: { diff --git a/.betterer.results b/.betterer.results index bfa5bfe7558..8c04156fcf0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -476,8 +476,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "2"] ], "packages/grafana-prometheus/src/querybuilder/shared/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "packages/grafana-prometheus/src/resource_clients.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -489,9 +488,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], - "packages/grafana-prometheus/src/test/mocks/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "packages/grafana-prometheus/src/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -961,9 +957,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`profiler\`)", "6"], [0, 0, 0, "Do not re-export imported variable (\`updateLegendValues\`)", "7"] ], - "public/app/core/navigation/mocks/routeProps.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/core/navigation/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -1499,22 +1492,9 @@ exports[`better eslint`] = { "public/app/features/browse-dashboards/components/NewFolderForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], - "public/app/features/browse-dashboards/state/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"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"] - ], "public/app/features/connections/components/ConnectionsRedirectNotice/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"] ], - "public/app/features/connections/pages/index.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./AddNewConnectionPage\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./DataSourceDashboardsPage\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./DataSourceDetailsPage\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./DataSourcesListPage\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./EditDataSourcePage\`)", "4"], - [0, 0, 0, "Do not re-export imported variable (\`./NewDataSourcePage\`)", "5"] - ], "public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], @@ -1568,13 +1548,6 @@ exports[`better eslint`] = { "public/app/features/correlations/components/EmptyCorrelationsCTA.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "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"] - ], - "public/app/features/correlations/mocks/useCorrelations.mocks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/correlations/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -1713,15 +1686,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/serialization/transformToV1TypesUtils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "4"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"], - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "6"] - ], "public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -1730,10 +1694,6 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "4"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"] ], - "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/DashboardLinkForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -1781,13 +1741,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/dashboard-scene/settings/version-history/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./HistorySrv\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryButtons\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryComparison\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryHeader\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./VersionHistoryTable\`)", "4"] - ], "public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], @@ -1870,10 +1823,6 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "6"] ], - "public/app/features/dashboard/components/AnnotationSettings/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/components/DashExportModal/DashboardExporter.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1956,10 +1905,6 @@ exports[`better eslint`] = { "public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard/components/LinksSettings/index.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./LinkSettingsEdit\`)", "0"], - [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, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"] @@ -2228,10 +2173,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], - "public/app/features/dataframe-import/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"] - ], "public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] @@ -2251,14 +2192,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/datasources/state/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"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"] - ], "public/app/features/datasources/state/navModel.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -2299,24 +2232,6 @@ exports[`better eslint`] = { "public/app/features/dimensions/editors/ValueMappingsEditor/ValueMappingsEditor.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/dimensions/editors/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"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"] - ], - "public/app/features/dimensions/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"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "6"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"] - ], "public/app/features/dimensions/scale.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -2362,17 +2277,6 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/demo/trace-generators.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/explore/TraceView/components/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./TracePageHeader\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./TraceTimelineViewer/SpanDetail/DetailState\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./TraceTimelineViewer\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./model/transform-trace-data\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./settings/SpanBarSettings\`)", "4"], - [0, 0, 0, "Do not re-export imported variable (\`./utils/filter-spans\`)", "5"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "6"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "8"] - ], "public/app/features/explore/TraceView/components/model/ddg/types.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./PathElem\`)", "0"] ], @@ -2393,21 +2297,9 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/model/transform-trace-data.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/explore/TraceView/components/types/index.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`../settings/SpanBarSettings\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./TNil\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./TTraceTimeline\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./links\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "4"] - ], "public/app/features/explore/TraceView/components/utils/DraggableManager/demo/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./DraggableManagerDemo\`)", "0"] ], - "public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./DraggableManager\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./EUpdateTypes\`)", "1"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"] - ], "public/app/features/explore/TraceView/createSpanLink.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -2444,8 +2336,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/geo/utils/frameVectorSource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/inspector/InspectDataOptions.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], @@ -2602,36 +2493,15 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"] ], - "public/app/features/plugins/admin/components/Badges/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./PluginDeprecatedBadge\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./PluginDisabledBadge\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./PluginEnterpriseBadge\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./PluginInstallBadge\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./PluginUpdateAvailableBadge\`)", "4"] - ], "public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/plugins/admin/components/GetStartedWithPlugin/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./GetStartedWithPlugin\`)", "0"] ], - "public/app/features/plugins/admin/components/InstallControls/index.tsx:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./InstallControlsButton\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./InstallControlsWarning\`)", "1"] - ], "public/app/features/plugins/admin/components/PluginDetailsPage.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/plugins/admin/mocks/catalogPlugin.mock.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], - "public/app/features/plugins/admin/mocks/localPlugin.mock.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/features/plugins/admin/mocks/remotePlugin.mock.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/features/plugins/admin/pages/Browse.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -2666,10 +2536,10 @@ exports[`better eslint`] = { "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"] ], - "public/app/features/plugins/sandbox/distortion_map.ts:5381": [ + "public/app/features/plugins/sandbox/distortions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/plugins/sandbox/sandbox_plugin_loader.ts:5381": [ + "public/app/features/plugins/sandbox/sandboxPluginLoader.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"] @@ -2913,10 +2783,6 @@ exports[`better eslint`] = { "public/app/features/transformers/editors/CalculateFieldTransformerEditor/WindowOptionsEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`CalculateFieldTransformerEditor\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`calculateFieldTransformRegistryItem\`)", "1"] - ], "public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -2982,12 +2848,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Do not use any type assertions.", "3"] ], - "public/app/features/users/mocks/userMocks.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/features/variables/adapters.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -3022,10 +2882,7 @@ exports[`better eslint`] = { ], "public/app/features/variables/inspect/NetworkGraph.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/variables/inspect/VariablesUnknownTable.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], @@ -3142,10 +2999,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/azuremonitor/azureMetadata/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"] - ], "public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./ArgQueryEditor\`)", "0"] ], @@ -3212,26 +3065,6 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "5"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "6"] ], - "public/app/plugins/datasource/azuremonitor/mocks/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/azuremonitor/mocks/panelData.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"] - ], - "public/app/plugins/datasource/azuremonitor/mocks/utils.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], - "public/app/plugins/datasource/azuremonitor/mocks/variables.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/azuremonitor/types/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"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"] - ], "public/app/plugins/datasource/azuremonitor/types/query.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`../dataquery.gen\`)", "0"], [0, 0, 0, "Do not re-export imported variable (\`../dataquery.gen\`)", "1"], @@ -3263,23 +3096,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/datasource/cloud-monitoring/components/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./Aggregation\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./AliasBy\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./AlignmentFunction\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./Alignment\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./AnnotationsHelp\`)", "4"], - [0, 0, 0, "Do not re-export imported variable (\`./Fields\`)", "5"], - [0, 0, 0, "Do not re-export imported variable (\`./GroupBy\`)", "6"], - [0, 0, 0, "Do not re-export imported variable (\`./LabelFilter\`)", "7"], - [0, 0, 0, "Do not re-export imported variable (\`./MQLQueryEditor\`)", "8"], - [0, 0, 0, "Do not re-export imported variable (\`./MetricQueryEditor\`)", "9"], - [0, 0, 0, "Do not re-export imported variable (\`./PeriodSelect\`)", "10"], - [0, 0, 0, "Do not re-export imported variable (\`./Preprocessor\`)", "11"], - [0, 0, 0, "Do not re-export imported variable (\`./Project\`)", "12"], - [0, 0, 0, "Do not re-export imported variable (\`./SLOQueryEditor\`)", "13"], - [0, 0, 0, "Do not re-export imported variable (\`./VisualMetricQueryEditor\`)", "14"] - ], "public/app/plugins/datasource/cloud-monitoring/datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -3291,14 +3107,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/plugins/datasource/cloud-monitoring/mocks/cloudMonitoringDatasource.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"] - ], - "public/app/plugins/datasource/cloud-monitoring/mocks/cloudMonitoringInstanceSettings.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/cloud-monitoring/types/query.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`../dataquery.gen\`)", "0"], [0, 0, 0, "Do not re-export imported variable (\`../dataquery.gen\`)", "1"], @@ -3307,9 +3115,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloud-monitoring/types/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/cloud-monitoring/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/datasource/cloudwatch/components/ConfigEditor/ConfigEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -3347,153 +3152,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], - "public/app/plugins/datasource/cloudwatch/mocks/CloudWatchVariables.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/commentOnlyQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/multiLineFullQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/multiLineFullQueryWithCaseClause.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/partialQueryWithFunction.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/partialQueryWithSubquery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/singleLineFullQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-sql-test-data/whitespaceQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/commentOnlyQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/filterQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./commentOnlyQuery\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./empty\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./filterQuery\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./multiLineFullQuery\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./newCommandQuery\`)", "4"], - [0, 0, 0, "Do not re-export imported variable (\`./singleLineFullQuery\`)", "5"], - [0, 0, 0, "Do not re-export imported variable (\`./sortQuery\`)", "6"], - [0, 0, 0, "Do not re-export imported variable (\`./whitespaceQuery\`)", "7"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/multiLineFullQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/newCommandQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/singleLineFullQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/sortQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/whitespaceQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-ppl-test-data/multilineQueries.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-ppl-test-data/newCommandQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-ppl-test-data/singleLineQueries.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"], - [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Do not use any type assertions.", "13"], - [0, 0, 0, "Do not use any type assertions.", "14"], - [0, 0, 0, "Do not use any type assertions.", "15"], - [0, 0, 0, "Do not use any type assertions.", "16"], - [0, 0, 0, "Do not use any type assertions.", "17"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./multiLineFullQuery\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./multiLineIncompleteQueryWithoutNamespace\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./singleLineEmptyQuery\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./singleLineFullQuery\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./singleLineTwoQueries\`)", "4"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineFullQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineEmptyQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineFullQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineTwoQueries.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/dashboardOnLoadedEvent.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/afterLabelValue.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./afterLabelValue\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./insideLabelValue\`)", "1"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/insideLabelValue.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/afterFunctionQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./afterFunctionQuery\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./secondArgAfterSearchQuery\`)", "1"], - [0, 0, 0, "Do not re-export imported variable (\`./secondArgQuery\`)", "2"], - [0, 0, 0, "Do not re-export imported variable (\`./singleLineEmptyQuery\`)", "3"], - [0, 0, 0, "Do not re-export imported variable (\`./thirdArgAfterSearchQuery\`)", "4"], - [0, 0, 0, "Do not re-export imported variable (\`./withinStringQuery\`)", "5"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/secondArgAfterSearchQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/secondArgQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/singleLineEmptyQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/thirdArgAfterSearchQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/withinStringQuery.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/datasource/cloudwatch/mocks/monarch/Monaco.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "public/app/plugins/datasource/cloudwatch/types.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -3566,9 +3224,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/datasource/elasticsearch/configuration/mocks/configOptions.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/elasticsearch/datasource.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -3582,11 +3237,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/elasticsearch/test-helpers/render.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/datasource/elasticsearch/types.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./dataquery.gen\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`ElasticsearchQuery\`)", "1"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"] - ], "public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -3627,17 +3277,10 @@ exports[`better eslint`] = { "public/app/plugins/datasource/grafana-testdata-datasource/components/SimulationSchemaForm.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/grafana-testdata-datasource/components/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`./RandomWalkEditor\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`./StreamingClientEditor\`)", "1"] - ], "public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/datasource/grafana/components/AnnotationQueryEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], @@ -3787,10 +3430,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], - "public/app/plugins/datasource/influxdb/mocks/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "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"], @@ -3817,8 +3456,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`./trace\`)", "0"] ], "public/app/plugins/datasource/jaeger/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/plugins/datasource/loki/LanguageProvider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -3858,13 +3496,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/loki/mocks/datasource.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], - "public/app/plugins/datasource/loki/mocks/metadataRequest.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -4000,9 +3631,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], - "public/app/plugins/datasource/tempo/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/datasource/zipkin/QueryField.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -4013,9 +3641,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/zipkin/utils/transforms.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/zipkin/webpack.config.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`config\`)", "0"] - ], "public/app/plugins/panel/annolist/AnnoListPanel.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -4134,9 +3759,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/geomap/types.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./panelcfg.gen\`)", "0"] ], - "public/app/plugins/panel/geomap/utils/layers.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/panel/geomap/utils/tooltip.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] @@ -4359,9 +3981,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/types/acl.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`OrgRole\`)", "0"] - ], "public/app/types/alerting.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4383,29 +4002,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"] ], - "public/app/types/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`CoreEvents\`)", "0"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "2"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "3"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "4"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "5"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "6"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "7"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "8"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "9"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "10"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "11"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "12"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "13"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "14"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "15"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "16"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "17"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "18"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "19"], - [0, 0, 0, "Do not use export all (\`export * from ...\`)", "20"] - ], "public/app/types/jquery/jquery.d.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4475,6 +4071,15 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], + "packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], "packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], @@ -4490,12 +4095,27 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], + "packages/grafana-ui/src/components/Dropdown/ButtonSelect.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/FormField/FormField.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/InfoTooltip/InfoTooltip.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/List/List.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], "packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], "packages/grafana-ui/src/components/QueryField/QueryField.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], + "packages/grafana-ui/src/components/SecretFormField/SecretFormField.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], "packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], @@ -4508,6 +4128,9 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], + "packages/grafana-ui/src/components/Select/SelectPerf.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], "packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], @@ -4517,9 +4140,21 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], + "packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/ThemeDemos/BorderRadius.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], "packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], + "packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], "packages/grafana-ui/src/components/UnitPicker/UnitPicker.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], @@ -4531,6 +4166,9 @@ exports[`no undocumented stories`] = { ], "packages/grafana-ui/src/components/VizTooltip/SeriesTable.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] + ], + "packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx:5381": [ + [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ] }` }; diff --git a/.drone.yml b/.drone.yml index 9778f442148..bf621c24a1e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -289,7 +289,7 @@ steps: - commands: - npx wait-on@7.0.1 http://$HOST:$PORT - yarn playwright install --with-deps chromium - - yarn e2e:playwright + - yarn e2e:playwright --grep @plugins depends_on: - grafana-server - build-test-plugins @@ -761,7 +761,7 @@ steps: - commands: - npx wait-on@7.0.1 http://$HOST:$PORT - yarn playwright install --with-deps chromium - - yarn e2e:playwright + - yarn e2e:playwright --grep @plugins depends_on: - grafana-server - build-test-plugins @@ -2986,6 +2986,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 1198b1489e48a9ced211633a0325d112814553246847fc7320fb5ac2bcb32b7d +hmac: d20f1d6e2e8347701f82114ad352f53db57dc95b5b3831941fa93d063a92b9d8 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3e78adb40cb..b37938d86fd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -80,6 +80,7 @@ /apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad /apps/folder/ @grafana/grafana-app-platform-squad /apps/playlist/ @grafana/grafana-app-platform-squad +/apps/secret/ @grafana/grafana-operator-experience-squad /apps/investigations/ @fcjack @matryer @svennergr /apps/advisor/ @grafana/plugins-platform-backend /apps/iam/ @grafana/access-squad @@ -87,7 +88,6 @@ /pkg/apis/ @grafana/grafana-app-platform-squad /pkg/apis/query @grafana/grafana-datasources-core-services /pkg/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend -/pkg/apis/secret @grafana/grafana-operator-experience-squad /pkg/bus/ @grafana/grafana-search-and-storage /pkg/cmd/ @grafana/grafana-backend-group /pkg/cmd/grafana-cli/commands/install_command.go @grafana/plugins-platform-backend @@ -400,8 +400,10 @@ /public/app/core/internationalization/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources -/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend -/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend +/e2e-playwright/ @grafana/grafana-frontend-platform +/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources +/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend +/e2e-playwright/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend # Packages /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend @@ -477,6 +479,7 @@ /cypress.config.js @grafana/grafana-frontend-platform /.levignore.js @grafana/plugins-platform-frontend playwright.config.ts @grafana/plugins-platform-frontend +playwright.storybook.config.ts @grafana/grafana-frontend-platform # public folder /public/app/api/ @grafana/grafana-frontend-platform @@ -747,6 +750,7 @@ embed.go @grafana/grafana-as-code /pkg/kinds/ @grafana/grafana-as-code /pkg/registry/ @grafana/grafana-as-code /pkg/registry/apis/ @grafana/grafana-app-platform-squad +/pkg/registry/apis/folders @grafana/grafana-search-and-storage /pkg/registry/apis/query @grafana/grafana-datasources-core-services /pkg/registry/apis/secret @grafana/grafana-operator-experience-squad /pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend @@ -770,6 +774,7 @@ embed.go @grafana/grafana-as-code /.github/pr-checks.json @tolzhabayev /.github/pr-commands.json @tolzhabayev /.github/renovate.json5 @grafana/frontend-ops +/.github/actions/check-jobs/action.yml @grafana/grafana-frontend-platform /.github/actions/setup-enterprise/action.yml @grafana/grafana-backend-group /.github/actions/setup-grafana-bench/ @Proximyst /.github/actions/build-package @grafana/grafana-developer-enablement-squad @@ -796,6 +801,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/commands.yml @torkelo /.github/workflows/community-release.yml @grafana/grafana-developer-enablement-squad /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend +/.github/workflows/detect-plugin-extension-changes.yml @grafana/plugins-platform-frontend /.github/workflows/documentation-ci.yml @grafana/docs-tooling /.github/workflows/deploy-pr-preview.yml @grafana/docs-tooling /.github/workflows/feature-toggles-ci.yml @grafana/docs-tooling @@ -817,6 +823,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend /.github/workflows/stale.yml @grafana/grafana-developer-enablement-squad /.github/workflows/storybook-verification.yml @grafana/grafana-frontend-platform +/.github/workflows/storybook-verification-playwright.yml @grafana/grafana-frontend-platform /.github/workflows/update-make-docs.yml @grafana/docs-tooling /.github/workflows/scripts/kinds/verify-kinds.go @grafana/platform-monitoring /.github/workflows/scripts/create-security-branch/create-security-branch.sh @grafana/grafana-developer-enablement-squad @@ -835,6 +842,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform /.github/workflows/i18n-crowdin-create-tasks.yml @grafana/grafana-frontend-platform /.github/workflows/i18n-verify.yml @grafana/grafana-frontend-platform +/.github/workflows/deploy-storybook-preview.yml @grafana/grafana-frontend-platform /.github/workflows/scripts/crowdin/create-tasks.ts @grafana/grafana-frontend-platform /.github/workflows/pr-go-workspace-check.yml @grafana/grafana-app-platform-squad /.github/workflows/pr-dependabot-update-go-workspace.yml @grafana/grafana-app-platform-squad @@ -875,3 +883,7 @@ embed.go @grafana/grafana-as-code /conf/provisioning/datasources/ @grafana/plugins-platform-backend /conf/provisioning/plugins/ @grafana/plugins-platform-backend /conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team + +# Security +/relyance.yaml @grafana/security-team +/.github/workflows/relyance-scan.yml @grafana/security-team diff --git a/.github/actions/check-jobs/action.yml b/.github/actions/check-jobs/action.yml new file mode 100644 index 00000000000..b4551730b96 --- /dev/null +++ b/.github/actions/check-jobs/action.yml @@ -0,0 +1,48 @@ +name: Check jobs results +description: Checks if any jobs have failed and exits with error if failures are found. Use to check the results of matrix test runs. +inputs: + needs: + description: JSON string containing the needs context from the workflow + required: true + failure-message: + description: Custom message to display when failures are found + required: false + default: "One or more jobs have failed" + success-message: + description: Custom message to display when all jobs pass + required: false + default: "All jobs passed successfully" +outputs: + any-failed: + description: Whether any jobs failed + value: ${{ steps.check-jobs.outputs.any-failed }} + +runs: + using: "composite" + steps: + - name: Check test suites + id: check-jobs + shell: bash + env: + NEEDS: ${{ inputs.needs }} + FAILURE_MSG: ${{ inputs.failure-message }} + SUCCESS_MSG: ${{ inputs.success-message }} + run: | + set -euo pipefail + + # Print the needs context, debugging + echo "$NEEDS" | jq + + # Extract failures + FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')" + + # Check if there are any failures + if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then + echo "❌ $FAILURE_MSG" + echo "Failed suites:" + echo "$FAILURES" | jq -r 'to_entries[] | "- \(.key): \(.value)"' + echo "any-failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + + echo "✅ $SUCCESS_MSG" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 263a75f43ab..1c62ef810eb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,11 +8,11 @@ updates: directories: - "/" - "/apps/playlist" + - "/apps/secret" - "/apps/investigations" - "/pkg/aggregator" - "/pkg/apimachinery" - "/pkg/apis/folder" - - "/pkg/apis/secret" - "/pkg/apiserver" - "/pkg/build" - "/pkg/build/wire" diff --git a/.github/license_finder.yaml b/.github/license_finder.yaml index f2fc8a144c4..1b1f5e1df13 100644 --- a/.github/license_finder.yaml +++ b/.github/license_finder.yaml @@ -99,7 +99,7 @@ :versions: [] :when: 2025-05-03 13:10:00.000000000 Z - - :license - - github.com/grafana/grafana/pkg/apis/secret + - github.com/grafana/grafana/apps/secret - unknown - :who: Carl Bergquist :why: repository is owned by Grafana Labs diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml index 6b05b9863c8..c3c934ea2b6 100644 --- a/.github/workflows/core-plugins-build-and-release.yml +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -62,7 +62,7 @@ jobs: with: credentials_json: '${{ env.PLUGINS_GOOGLE_CREDENTIALS }}' - name: 'Set up Cloud SDK' - uses: 'google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a' + uses: 'google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a' - name: Setup nodejs environment uses: actions/setup-node@v4 with: diff --git a/.github/workflows/deploy-storybook-preview.yml b/.github/workflows/deploy-storybook-preview.yml new file mode 100644 index 00000000000..a1fecce67cb --- /dev/null +++ b/.github/workflows/deploy-storybook-preview.yml @@ -0,0 +1,93 @@ +name: Deploy Storybook preview + +on: + pull_request: + paths: + - 'packages/grafana-ui/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + deploy-storybook-preview: + name: Deploy Storybook preview + runs-on: ubuntu-latest + # Don't run from forks for the moment. If we find this useful we can do the workflow_run dance + # to make it work for forks. + if: github.event.pull_request.head.repo.fork == false + permissions: + contents: read + id-token: write + + env: + BUCKET_NAME: grafana-storybook-previews + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: | + node_modules + key: node_modules-${{ hashFiles('yarn.lock') }} + restore-keys: | + node_modules- + + - name: Install dependencies + env: + # If the PR isn't from a fork then don't use the slower yarn checks + YARN_ENABLE_HARDENED_MODE: ${{ github.event.pull_request.head.repo.fork == false && '1' || '0' }} + run: yarn install --immutable + + - name: Build storybook + run: yarn storybook:build + + # Create the GCS folder name for the preview. Creates a consistent name for all deploys for the PR. + # Matches format of `pr__`. + # Where `SANITIZED_BRANCH` is the branch name with only alphanumeric and hyphens, limited to 30 characters. + - name: Create deploy name + id: create-deploy-name + env: + BRANCH_NAME: ${{ github.event.pull_request.head.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Convert branch name to only contain alphanumeric and hyphens + SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr -cs "[:alnum:]-" "-" | sed "s/^-//;s/-$//") + + # Check if SANITIZED_BRANCH is empty and fail if it is + if [ -z "$SANITIZED_BRANCH" ]; then + echo "Error: Branch name resulted in empty string after sanitization" + exit 1 + fi + + echo "deploy-name=pr_${PR_NUMBER}_${SANITIZED_BRANCH:0:30}" >> "$GITHUB_OUTPUT" + + - name: Upload Storybook + uses: grafana/shared-workflows/actions/push-to-gcs@main + with: + environment: prod + bucket: ${{ env.BUCKET_NAME }} + bucket_path: ${{ steps.create-deploy-name.outputs.deploy-name }} + path: packages/grafana-ui/dist/storybook + service_account: github-gf-storybook-preview@grafanalabs-workload-identity.iam.gserviceaccount.com + parent: false + + - name: Write summary + env: + DEPLOY_NAME: ${{ steps.create-deploy-name.outputs.deploy-name }} + run: | + echo "## Storybook preview deployed! 🚀" >> $GITHUB_STEP_SUMMARY + echo "Check it out at https://storage.googleapis.com/${BUCKET_NAME}/${DEPLOY_NAME}/index.html" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index a4328970ff7..8a0d3ecc70d 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -158,13 +158,15 @@ jobs: - id: 'auth' uses: 'google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f' + if: github.event.pull_request.head.repo.full_name == github.repository with: workload_identity_provider: projects/304398677251/locations/global/workloadIdentityPools/github/providers/github-provider service_account: github-plugins-data-levitate@grafanalabs-workload-identity.iam.gserviceaccount.com project_id: 'grafanalabs-global' - name: 'Set up Cloud SDK' - uses: 'google-github-actions/setup-gcloud@6189d56e4096ee891640bb02ac264be376592d6a' + uses: 'google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a' + if: github.event.pull_request.head.repo.full_name == github.repository with: version: '>= 363.0.0' project_id: 'grafanalabs-global' @@ -175,6 +177,7 @@ jobs: run: ./scripts/check-breaking-changes.sh env: FORCE_COLOR: 3 + IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} # used in check-breaking-changes.sh and levitate-parse-json-report.js - name: Persisting the check output run: | @@ -199,6 +202,7 @@ jobs: permissions: contents: read id-token: write + if: github.event.pull_request.head.repo.full_name == github.repository steps: - id: get-secrets diff --git a/.github/workflows/detect-plugin-extension-changes.yml b/.github/workflows/detect-plugin-extension-changes.yml new file mode 100644 index 00000000000..b5ab96725d9 --- /dev/null +++ b/.github/workflows/detect-plugin-extension-changes.yml @@ -0,0 +1,156 @@ +--- +name: Detect Plugin Extension Changes + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +on: + pull_request: + branches: + - 'main' + paths: + - 'packages/**' + - 'public/**' + +env: + # Space-separated list of keywords referring to plugin extensions + PLUGIN_EXTENSION_KEYWORDS: "usePluginLinks, usePluginComponent, usePluginComponents, usePluginFunctions, PluginExtensionPoints" + +jobs: + detect-plugin-extension-changes: + name: Detect Plugin Extension Changes + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + if: github.event.pull_request.head.repo.full_name == github.repository + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check for plugin extension changes + id: check-changes + uses: actions/github-script@v7 + with: + script: | + const { execSync } = require('child_process'); + const fs = require('fs'); + + // Plugin extension keywords from environment + const keywords = process.env.PLUGIN_EXTENSION_KEYWORDS.split(','); + const baseSha = '${{ github.event.pull_request.base.sha }}'; + const headSha = '${{ github.event.pull_request.head.sha }}'; + + console.log('Checking for plugin extension changes...'); + console.log('Keywords:', keywords); + + // Get changed files in packages/ and public/ directories + let changedFiles = []; + const diffOutput = execSync( + `git diff --name-only ${baseSha}...${headSha} -- packages/ public/`, + { encoding: 'utf8' } + ).trim(); + + if (diffOutput) { + changedFiles = diffOutput.split('\n').filter(file => { + // Validate file path and ensure it's in target directories + return file.match(/^(packages\/|public\/)/) && + file.match(/^[a-zA-Z0-9._/-]+$/) && + fs.existsSync(file); + }); + } + + console.log('Changed files to check:', changedFiles); + + // Check each file for plugin extension keywords + const filesWithChanges = new Set(); + let hasPluginExtensionChanges = false; + + for (const file of changedFiles) { + try { + // Get the diff for this specific file + const fileDiff = execSync( + `git diff ${baseSha}...${headSha} -- "${file}"`, + { encoding: 'utf8' } + ); + + // Check if any keywords are in the diff + for (const keyword of keywords) { + if (fileDiff.includes(keyword)) { + console.log(`Found ${keyword} in ${file}`); + filesWithChanges.add(file); + hasPluginExtensionChanges = true; + break; // Found at least one keyword, move to next file + } + } + } catch (error) { + console.log(`Error checking file ${file}:`, error.message); + } + } + + // Set outputs + const filesArray = Array.from(filesWithChanges); + const formattedFiles = filesArray.length > 0 + ? '`' + filesArray.join('`\\n- `') + '`' + : ''; + + core.setOutput('plugin_extension_changes', hasPluginExtensionChanges.toString()); + core.setOutput('formatted_changed_files', formattedFiles); + + if (hasPluginExtensionChanges) { + console.log('The following files have changes that may affect plugin extensions:'); + console.log(filesArray); + } else { + console.log('No changes detected in core Grafana extensions or extension points.'); + } + + - name: Send Slack Message via Payload + id: slack + if: steps.check-changes.outputs.plugin_extension_changes == 'true' + uses: grafana/shared-workflows/actions/send-slack-message@0941e3408fa4789fec9062c44a2a9e1832146ba6 #v2.0.1 + with: + method: chat.postMessage + payload-templated: true + payload: | + { + "channel": "C031SLFH6G0", + "text": "Plugin Extension changes in core Grafana *PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }} :information_source:", + "icon_emoji": ":grot:", + "username": "Plugin Extension Bot", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Plugin Extensions:* possible changes to extension points in core Grafana." + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }}>\n*Job:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Job>" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*File(s) with changes:*\n- ${{ steps.check-changes.outputs.formatted_changed_files }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*What to do?*\nMake sure that:\n- All extension point ids start with `grafana/`\n- All extension point ids are exposed via \n- Core Grafana is not registering extensions to extension points offered by plugins" + } + } + ] + } \ No newline at end of file diff --git a/.github/workflows/frontend-lint.yml b/.github/workflows/frontend-lint.yml index e5ceb117eea..fb0362bb580 100644 --- a/.github/workflows/frontend-lint.yml +++ b/.github/workflows/frontend-lint.yml @@ -16,6 +16,7 @@ jobs: contents: read outputs: changed: ${{ steps.detect-changes.outputs.frontend }} + prettier: ${{ steps.detect-changes.outputs.frontend == 'true' || steps.detect-changes.outputs.docs == 'true' }} steps: - uses: actions/checkout@v4 with: @@ -34,7 +35,7 @@ jobs: id-token: write # Run this workflow only for PRs from forks; if it gets merged into `main` or `release-*`, # the `lint-frontend-prettier-enterprise` workflow will run instead - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.changed == 'true' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && needs.detect-changes.outputs.prettier == 'true' name: Lint runs-on: ubuntu-latest steps: @@ -55,7 +56,7 @@ jobs: contents: read id-token: write # Run this workflow for non-PR events (like pushes to `main` or `release-*`) OR for internal PRs (PRs not from forks) - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.changed == 'true' + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false && needs.detect-changes.outputs.prettier == 'true' name: Lint runs-on: ubuntu-latest steps: diff --git a/.github/workflows/i18n-crowdin-create-tasks.yml b/.github/workflows/i18n-crowdin-create-tasks.yml index 6ecaa883e01..e26d7806c5e 100644 --- a/.github/workflows/i18n-crowdin-create-tasks.yml +++ b/.github/workflows/i18n-crowdin-create-tasks.yml @@ -2,10 +2,9 @@ name: Crowdin automatic task management on: workflow_dispatch: - # once a week on Sunday at midnight - # TODO enable once we're ready to create tasks automatically - # schedule: - # - cron: "0 0 * * 0" + # once a month on the first day of the month at midnight + schedule: + - cron: "0 0 1 * *" jobs: create-tasks-in-crowdin: diff --git a/.github/workflows/i18n-crowdin-download.yml b/.github/workflows/i18n-crowdin-download.yml index 1c1f87efc40..e8e04c22029 100644 --- a/.github/workflows/i18n-crowdin-download.yml +++ b/.github/workflows/i18n-crowdin-download.yml @@ -7,6 +7,7 @@ on: jobs: download-sources-from-crowdin: + if: github.repository == 'grafana/grafana' uses: grafana/grafana-github-actions/.github/workflows/crowdin-download.yml@main with: crowdin_project_id: 5 diff --git a/.github/workflows/i18n-crowdin-upload.yml b/.github/workflows/i18n-crowdin-upload.yml index c3a48f97481..b61a7538bf0 100644 --- a/.github/workflows/i18n-crowdin-upload.yml +++ b/.github/workflows/i18n-crowdin-upload.yml @@ -14,6 +14,7 @@ on: jobs: upload-sources-to-crowdin: + if: github.repository == 'grafana/grafana' uses: grafana/grafana-github-actions/.github/workflows/crowdin-upload.yml@main with: crowdin_project_id: 5 diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index ec74716f7e3..5e7d116d0c2 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -7,12 +7,17 @@ on: - main - release-*.*.* -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} +# TODO: re-enable this before merging +# concurrency: +# group: ${{ github.workflow }}-${{ github.ref }} +# cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} permissions: {} +env: + ACTIONS_STEP_DEBUG: true + RUNNER_DEBUG: 1 + jobs: detect-changes: name: Detect whether code changed @@ -44,22 +49,56 @@ jobs: steps: - uses: actions/checkout@v4 with: - path: ./grafana persist-credentials: false - - uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + + # TODO: add a cleanup workflow to remove the cache when the PR is closed + # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy + # TODO: maybe we could just use the cache to store the build, instead of uploading as an artifact? + - uses: actions/cache@v4 + id: cache + with: + key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx') }}" + path: | + build-dir + + # If no cache hit, build Grafana + - name: Build Grafana + if: steps.cache.outputs.cache-hit != 'true' + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: verb: run - args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt - - run: mv "$(cat out.txt)" grafana.tar.gz - - run: echo "artifact=grafana-e2e-${{github.run_number}}" >> "$GITHUB_OUTPUT" + args: go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}" > out.txt + - name: Cat built artifact + if: steps.cache.outputs.cache-hit != 'true' + run: cat out.txt + - name: Move built artifact + if: steps.cache.outputs.cache-hit != 'true' + run: | + mkdir -p build-dir + mv "$(cat out.txt)" build-dir/grafana.tar.gz + + # If cache hit, validate the artifact is present + - name: Validate artifact + if: steps.cache.outputs.cache-hit == 'true' + run: | + if [ ! -f build-dir/grafana.tar.gz ]; then + echo "Error: build-dir/grafana.tar.gz not found in cache" + exit 1 + fi + + - name: Set artifact name + run: echo "artifact=grafana-server-${{github.run_number}}" >> "$GITHUB_OUTPUT" id: artifact - - uses: actions/upload-artifact@v4 + + - name: Upload artifact + uses: actions/upload-artifact@v4 id: upload with: retention-days: 1 name: ${{ steps.artifact.outputs.artifact }} - path: grafana.tar.gz + path: build-dir/grafana.tar.gz + # TODO: we won't need this when we only have playwright build-e2e-runner: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' @@ -159,6 +198,119 @@ jobs: path: videos retention-days: 1 + run-playwright-tests: + needs: + - build-grafana + name: Playwright E2E tests (${{ matrix.shard }}/${{ matrix.shardTotal }}) + runs-on: ubuntu-latest-8-cores + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-grafana.outputs.artifact }} + - name: Run E2E tests + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + with: + verb: run + args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report + - uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }} + path: ./blob-report + retention-days: 1 + + required-playwright-tests: + needs: + - run-playwright-tests + - build-grafana + if: ${{ !cancelled() }} + name: All Playwright tests complete + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: blobs + pattern: playwright-blob-* + merge-multiple: true + + - name: Check blob reports + run: | + if [ ! "$(ls -A ./blobs)" ]; then + echo "Error: No blob reports found in ./blobs directory" + echo "Did the Playwright tests run at all?" + exit 1 + fi + echo "Found blob reports in ./blobs:" + ls -lah ./blobs + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./blobs + + - name: Merge into JSON Report + env: + PLAYWRIGHT_JSON_OUTPUT_NAME: /tmp/playwright-results.json + run: npx playwright merge-reports --reporter=json ./blobs + + - name: Bench report + run: | + docker run --rm \ + --volume="/tmp/playwright-results.json:/home/bench/tests/playwright-results.json" \ + us-docker.pkg.dev/grafanalabs-global/docker-grafana-bench-prod/grafana-bench:v0.5.1 report \ + --grafana-url "http://localhost:3000" \ + --grafana-version "CI- ${{ github.sha }}" \ + --test-suite-name "FrontendCore" \ + --report-input playwright \ + --report-output log \ + --log-level DEBUG \ + /home/bench/tests/playwright-results.json + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: playwright-html-${{ github.run_number }} + path: playwright-report + retention-days: 7 + + - name: Check test suites + id: check-jobs + uses: ./.github/actions/check-jobs + continue-on-error: true # Failure will be reported on Show test results step + with: + needs: ${{ toJson(needs) }} + failure-message: "One or more E2E test suites have failed" + success-message: "All E2E test suites completed successfully" + + - name: Show test results + env: + FAILED: ${{ steps.check-jobs.outputs.any-failed }} + # sed removes the leading `../../src/` from the paths + run: | + npx playwright merge-reports --reporter list ./all-blob-reports | sed 's|\(\.\./\)\{1,\}src/|/|g' + if [ "$FAILED" = "true" ]; then + exit 1 + fi + run-a11y-test: needs: - build-grafana @@ -193,6 +345,7 @@ jobs: required-e2e-tests: needs: - run-e2e-tests + - build-grafana # a11y test is not listed on purpose: it is not an important E2E test. # It is also totally fine to fail right now. # always() is the best function here. @@ -203,13 +356,13 @@ jobs: name: All E2E tests complete runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check test suites - env: - NEEDS: ${{ toJson(needs) }} - run: | - FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')" - echo "$FAILURES" - if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then - exit 1 - fi - echo "All OK!" + uses: ./.github/actions/check-jobs + with: + needs: ${{ toJson(needs) }} + failure-message: "One or more E2E test suites have failed" + success-message: "All E2E test suites completed successfully" diff --git a/.github/workflows/publish-artifact.yml b/.github/workflows/publish-artifact.yml index 1cc650858a7..45aa1d79445 100644 --- a/.github/workflows/publish-artifact.yml +++ b/.github/workflows/publish-artifact.yml @@ -48,7 +48,7 @@ jobs: path: ./artifact - name: Log in to GCS id: login-to-gcs - uses: grafana/shared-workflows/actions/login-to-gcs@login-to-gcs/v0.2.1 + uses: grafana/shared-workflows/actions/login-to-gcs@main with: environment: ${{ inputs.environment }} service_account: ${{ inputs.service-account }} @@ -58,7 +58,7 @@ jobs: find ./artifact -mindepth 2 -maxdepth 2 -exec cp -r {} out/ \; ls -al out - name: Upload artifacts - uses: grafana/shared-workflows/actions/push-to-gcs@push-to-gcs-v0.2.0 + uses: grafana/shared-workflows/actions/push-to-gcs@main with: bucket: ${{ inputs.bucket }} environment: ${{ inputs.environment }} @@ -66,3 +66,4 @@ jobs: path: out bucket_path: ${{ inputs.bucket-path }} service_account: ${{ inputs.service-account }} + gzip: false diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 54c81f1091f..35b8a219d62 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -186,5 +186,5 @@ jobs: bucket: grafana-prerelease pattern: artifacts-* run-id: ${{ github.run_id }} - bucket-path: ${{ needs.setup.outputs.version }} + bucket-path: ${{ needs.setup.outputs.version }}_${{ github.run_id }} environment: prod diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 06a644f5843..2c43ae43de4 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -208,7 +208,7 @@ jobs: if: ${{ inputs.bump == true || inputs.bump == 'true' }} run: | git add package.json lerna.json yarn.lock packages public - test -e e2e/test-plugins && git add e2e/test-plugins + test -e e2e-playwright/test-plugins && git add e2e-playwright/test-plugins git commit -m "Update version to $VERSION" - name: Git push diff --git a/.github/workflows/relyance-scan.yml b/.github/workflows/relyance-scan.yml new file mode 100644 index 00000000000..29d68e00416 --- /dev/null +++ b/.github/workflows/relyance-scan.yml @@ -0,0 +1,33 @@ +name: Relyance Compliance Inspection +on: + schedule: + - cron: '0 0 * * *' # Run daily at 00:00 UTC + workflow_dispatch: # Allow for manual trigger + +jobs: + relyance-compliance-inspector: + permissions: + contents: read + id-token: write # Needed for Vault access + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Get API key + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@5d7e361bc7e0a183cde8afe9899fb7b596d2659b # get-vault-secrets-v1.2.0 + with: + repo_secrets: | + API_KEY=relyance:API_KEY + + - name: Run Relyance scan + env: + API_KEY: "${{ fromJSON(steps.vault-secrets.outputs.secrets).API_KEY }}" + run: | + docker pull gcr.io/relyance-ext/compliance_inspector:release && \ + docker run --rm -v ${{ github.workspace }}:/repo --env "API_KEY=${{ env.API_KEY }}" gcr.io/relyance-ext/compliance_inspector:release diff --git a/.github/workflows/run-dashboard-search-e2e.yml b/.github/workflows/run-dashboard-search-e2e.yml index 8554f7549a6..6eebaf9320a 100644 --- a/.github/workflows/run-dashboard-search-e2e.yml +++ b/.github/workflows/run-dashboard-search-e2e.yml @@ -64,7 +64,7 @@ jobs: tools/ public/ conf/ - e2e/test-plugins/ + e2e-playwright/test-plugins/ devenv/ key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }} # only rebuild grafana if search files have changed ( or dependencies ) @@ -116,7 +116,7 @@ jobs: tools/ public/ conf/ - e2e/test-plugins/ + e2e-playwright/test-plugins/ devenv/ key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }} - name: Set the step name diff --git a/.github/workflows/storybook-verification-playwright.yml b/.github/workflows/storybook-verification-playwright.yml new file mode 100644 index 00000000000..e3924a67a85 --- /dev/null +++ b/.github/workflows/storybook-verification-playwright.yml @@ -0,0 +1,47 @@ +name: Verify Storybook (Playwright) + +on: + pull_request: + paths: + - 'packages/grafana-ui/**' + - 'e2e-playwright/storybook/**' + - '!docs/**' + - '!*.md' + push: + branches: + - main + paths: + - 'packages/grafana-ui/**' + - 'e2e-playwright/storybook/**' + - '!docs/**' + - '!*.md' + +permissions: {} + +jobs: + verify-storybook: + name: Verify Storybook (Playwright) + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run Storybook and E2E tests + run: yarn e2e:playwright:storybook diff --git a/.gitignore b/.gitignore index ccc42239a87..7457c8148b3 100644 --- a/.gitignore +++ b/.gitignore @@ -237,4 +237,4 @@ public/app/plugins/**/dist/ # Mock service worker used for fake API responses in frontend development public/mockServiceWorker.js -/e2e/test-plugins/*/dist +/e2e-playwright/test-plugins/*/dist diff --git a/.golangci.yml b/.golangci.yml index 5bb4a5e3167..5a534ee8ec2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -83,6 +83,14 @@ linters: deny: - pkg: github.com/grafana/grafana/pkg desc: apps/playlist is not allowed to import grafana core + apps-secret: + list-mode: lax + files: + - ./apps/secret/* + - ./apps/secret/**/* + deny: + - pkg: github.com/grafana/grafana/pkg + desc: apps/secret is not allowed to import grafana core coreplugins: files: - '**/pkg/tsdb/grafana-pyroscope-datasource/*' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4527ac3487d..12a43e07e98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,56 @@ -# Contributing to Grafana +# Contribute to Grafana Thank you for your interest in contributing to Grafana! We welcome all people who want to contribute in a healthy and constructive manner within our community. To help us create a safe and positive community experience for all, we require all participants to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md). -This document is a guide to help you through the process of contributing to Grafana. +This document is a guide to help you through the process of making technical contributions to Grafana. -## Become a contributor +Whether you're a new contributer or a seasoned veteran we hope these resources help you connect with the community: -You can contribute to Grafana in several ways. Here are some examples: +Interact and be heard: -- Contribute to the Grafana codebase. -- Report and triage bugs. -- Develop community plugins and dashboards. -- Write technical documentation and blog posts, for users and contributors. -- Organize meetups and user groups in your local area. -- Help others by answering questions about Grafana. +- Forums: Do you have a problem, question, or curiosity? Visit our [forums](https://gra.fan/fromgithubtoforums) for a reservoir of knowledge- submit your own questions and answers! +- Meetups: Craving in-person connections without the long journeys? [Join your local Grafana & Friends meetup group](https://gra.fan/githubtomeetup)! +- Community Slack: Eager for real-time connections with fellow users? Begin a conversation on [Slack](https://gra.fan/githubtoslack). + Learn: +- YouTube: From getting started to exploring newer projects like Pyroscope and Beyla, the [Grafana YouTube channel](https://gra.fan/githubtoyoutube) has what you need to get started! +- Meetups: Join a [group near you](https://gra.fan/githubtomeetup) to learn from local experts and ask questions in real time. + Share your story: +- Meetups and blogs: We’d love to feature your OSS Grafana Labs use case or story at an upcoming Grafana & Friends meetup or on the Grafana blog! Submit your idea [here](https://gra.fan/githubtocca) and we’ll connect with you on next steps if accepted. + +## Make technical contributions + +We welcome your technical contributions! Here are some examples: + +- Contribute to the Grafana codebase- check out these [help-wanted issues](<(https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)>) +- Develop community [plugins](https://grafana.com/developers/plugin-tools) +- Report [bugs](https://github.com/grafana/grafana/issues/new?template=0-bug-report.yaml) +- [Triage issues](https://github.com/grafana/grafana/blob/4414b92e93440cc9ed0f281989ee71dc16216a15/contribute/triage-issues.md) +- Report [security vulnerabilities](https://github.com/grafana/grafana/security/policy) +- Submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md) +- Write [technical documentation](https://github.com/grafana/grafana/blob/4414b92e93440cc9ed0f281989ee71dc16216a15/contribute/documentation/README.md) **Please note:** We do not currently accept contributions for translations. Please do not submit pull requests translating grafana.json files - they will be rejected. We do accept contributions to mark up phrases for translation. See [Internationalization](contribute/internationalization.md). -For more ways to contribute, check out the [Open Source Guides](https://opensource.guide/how-to-contribute/). +### Your first contribution + +Unsure where to begin contributing to Grafana? Start by browsing issues labeled `beginner friendly` or `help wanted`. + +- [Beginner-friendly](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) issues are generally straightforward to complete. +- [Help wanted](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) issues are problems we would like the community to help us with regardless of complexity. + +If you're looking to make a code change, see how to set up your environment for [local development](contribute/developer-guide.md). + +When you're ready to contribute, it's time to [create a pull request](/contribute/create-pull-request.md). + +### Develop a plugin + +Developing a Grafana plugin is a fantastic way to share your unique ideas with the community, extend the platform’s capabilities, and make a real impact on how people visualize and understand their data. Check out our guide to creating [plugins](https://grafana.com/developers/plugin-tools) ### Report bugs Before submitting a new issue, try to make sure someone hasn't already reported the problem. Look through the [existing issues](https://github.com/grafana/grafana/issues) for similar issues. -Report a bug by submitting a [bug report](https://github.com/grafana/grafana/issues/new?labels=type%3A+bug&template=1-bug_report.md). Make sure that you provide as much information as possible on how to reproduce the bug. +Report a bug by submitting a [bug report](https://github.com/grafana/grafana/issues/new?template=0-bug-report.yaml). Make sure that you provide as much information as possible on how to reproduce the bug. Follow the issue template and add additional information that will help us replicate the problem. @@ -38,42 +65,25 @@ For a dashboard related issues: For authentication and alerting Grafana server logs are useful. -#### Security issues - -If you believe you've found a security vulnerability, please read our [security policy](https://github.com/grafana/grafana/security/policy) for more details. - -### Suggest enhancements - -If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request&projects=&template=1-feature_requests.md). - -We want to make Grafana accessible to even more people. Submit an [accessibility issue](https://github.com/grafana/grafana/issues/new?labels=type%3A+accessibility&template=3-accessibility.md) to help us understand what we can improve. - -### Write documentation - -To edit or write technical content, refer to [Contribute to our documentation](/contribute/documentation/README.md). We welcome your expertise and input as our body of technical content grows. - ### Triage issues If you don't have the knowledge or time to code, consider helping with _issue triage_. The community will thank you for saving them time by spending some of yours. Read more about the ways you can [Triage issues](/contribute/triage-issues.md). -### Answering questions +#### Security issues -If you have a question and you can't find the answer in the [documentation](https://grafana.com/docs/), the next step is to ask it on the [community site](https://community.grafana.com/). +If you believe you've found a security vulnerability, please read our [security policy](https://github.com/grafana/grafana/security/policy) for more details on reporting. -It's important to us to help these users, and we'd love your help. Sign up to our [community site](https://community.grafana.com/), and start helping other Grafana users by answering their questions. +### Suggest enhancements -### Your first contribution +If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). -Unsure where to begin contributing to Grafana? Start by browsing issues labeled `beginner friendly` or `help wanted`. +We want to make Grafana accessible to even more people. Submit an [accessibility issue](https://github.com/grafana/grafana/issues/new?template=2-accessibility.md) to help us understand what we can improve. -- [Beginner-friendly](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) issues are generally straightforward to complete. -- [Help wanted](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) issues are problems we would like the community to help us with regardless of complexity. +### Write documentation -If you're looking to make a code change, see how to set up your environment for [local development](contribute/developer-guide.md). - -When you're ready to contribute, it's time to [create a pull request](/contribute/create-pull-request.md). +To edit or write technical content, refer to [Contribute to our documentation](/contribute/documentation/README.md). We welcome your expertise and input as our body of technical content grows. #### Contributor License Agreement (CLA) diff --git a/Dockerfile b/Dockerfile index d98bc6db4e6..4de7a9b5b09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,6 @@ COPY .citools .citools # Include vendored dependencies COPY pkg/util/xorm pkg/util/xorm -COPY pkg/apis/secret pkg/apis/secret COPY pkg/apiserver pkg/apiserver COPY pkg/apimachinery pkg/apimachinery COPY pkg/build pkg/build @@ -83,6 +82,7 @@ COPY pkg/storage/unified/apistore pkg/storage/unified/apistore COPY pkg/semconv pkg/semconv COPY pkg/aggregator pkg/aggregator COPY apps/playlist apps/playlist +COPY apps/secret apps/secret COPY apps/investigations apps/investigations COPY apps/advisor apps/advisor COPY apps/dashboard apps/dashboard diff --git a/Makefile b/Makefile index 7973fd5f679..df23645fe0b 100644 --- a/Makefile +++ b/Makefile @@ -471,7 +471,7 @@ protobuf: ## Compile protobuf definitions go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 buf generate pkg/plugins/backendplugin/pluginextensionv2 --template pkg/plugins/backendplugin/pluginextensionv2/buf.gen.yaml - buf generate pkg/apis/secret/v0alpha1/decrypt --template pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml + buf generate apps/secret/decrypt/v1beta1 --template apps/secret/decrypt/v1beta1/buf.gen.yaml buf generate pkg/storage/unified/proto --template pkg/storage/unified/proto/buf.gen.yaml buf generate pkg/services/authz/proto/v1 --template pkg/services/authz/proto/v1/buf.gen.yaml buf generate pkg/services/ngalert/store/proto/v1 --template pkg/services/ngalert/store/proto/v1/buf.gen.yaml diff --git a/apps/advisor/Makefile b/apps/advisor/Makefile index 7a5f1c99365..b8b335bfa68 100644 --- a/apps/advisor/Makefile +++ b/apps/advisor/Makefile @@ -1,3 +1,22 @@ +APP_SDK_VERSION := v0.39.2 +APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) +APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk + +.PHONY: install-app-sdk +install-app-sdk: $(APP_SDK_BIN) ## Install the Grafana App SDK + +$(APP_SDK_BIN): + @echo "Installing Grafana App SDK version $(APP_SDK_VERSION)" + @mkdir -p $(APP_SDK_DIR) + # The only way to install specific versions of binaries using `go install` + # is by setting GOBIN to the directory you want to install the binary to. + GOBIN=$(APP_SDK_DIR) go install github.com/grafana/grafana-app-sdk/cmd/grafana-app-sdk@$(APP_SDK_VERSION) + @touch $@ + +.PHONY: update-app-sdk +update-app-sdk: ## Update the Grafana App SDK dependency in go.mod + go get github.com/grafana/grafana-app-sdk@$(APP_SDK_VERSION) + .PHONY: generate -generate: - @grafana-app-sdk generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none +generate: ## Run Grafana App SDK code generation + @$(APP_SDK_BIN) generate -g ./pkg/apis --grouping=group --postprocess --defencoding=none diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 45a7ed0b51e..65832aabb4b 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/advisor go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.39.0 - k8s.io/apimachinery v0.33.1 + github.com/grafana/grafana-app-sdk v0.39.2 + k8s.io/apimachinery v0.33.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -33,7 +33,7 @@ require ( github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // indirect - github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -79,9 +79,9 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/api v0.33.2 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index e7629867703..90b34992392 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -63,11 +63,14 @@ github.com/grafana/grafana-app-sdk v0.30.0/go.mod h1:jhfqNIovb+Mes2vdMf9iMCWQkp1 github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs= github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI= github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= github.com/grafana/grafana-app-sdk/logging v0.29.0 h1:mgbXaAf33aFwqwGVeaX30l8rkeAJH0iACgX5Rn6YkN4= github.com/grafana/grafana-app-sdk/logging v0.29.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= github.com/grafana/grafana-app-sdk/logging v0.30.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= github.com/grafana/grafana-app-sdk/logging v0.35.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= @@ -302,21 +305,25 @@ k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go index 4b385514d97..0150b059719 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_metadata_gen.go @@ -24,5 +24,8 @@ type CheckMetadata struct { // NewCheckMetadata creates a new CheckMetadata object. func NewCheckMetadata() *CheckMetadata { - return &CheckMetadata{} + return &CheckMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go index aca8c2876d6..5708bba88d6 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_object_gen.go @@ -18,8 +18,11 @@ import ( type Check struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - Spec CheckSpec `json:"spec" yaml:"spec"` - CheckStatus CheckStatus `json:"status" yaml:"status"` + + // Spec is the spec of the Check + Spec CheckSpec `json:"spec" yaml:"spec"` + + Status CheckStatus `json:"status" yaml:"status"` } func (o *Check) GetSpec() any { @@ -37,14 +40,14 @@ func (o *Check) SetSpec(spec any) error { func (o *Check) GetSubresources() map[string]any { return map[string]any{ - "status": o.CheckStatus, + "status": o.Status, } } func (o *Check) GetSubresource(name string) (any, bool) { switch name { case "status": - return o.CheckStatus, true + return o.Status, true default: return nil, false } @@ -57,7 +60,7 @@ func (o *Check) SetSubresource(name string, value any) error { if !ok { return fmt.Errorf("cannot set status type %#v, not of type CheckStatus", value) } - o.CheckStatus = cast + o.Status = cast return nil default: return fmt.Errorf("subresource '%s' does not exist", name) @@ -219,6 +222,20 @@ func (o *Check) DeepCopyObject() runtime.Object { return o.Copy() } +func (o *Check) DeepCopy() *Check { + cpy := &Check{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Check) DeepCopyInto(dst *Check) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + // Interface compliance compile-time check var _ resource.Object = &Check{} @@ -262,5 +279,41 @@ func (o *CheckList) SetItems(items []resource.Object) { } } +func (o *CheckList) DeepCopy() *CheckList { + cpy := &CheckList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *CheckList) DeepCopyInto(dst *CheckList) { + resource.CopyObjectInto(dst, o) +} + // Interface compliance compile-time check var _ resource.ListObject = &CheckList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *CheckSpec) DeepCopy() *CheckSpec { + cpy := &CheckSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *CheckSpec) DeepCopyInto(dst *CheckSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of CheckStatus +func (s *CheckStatus) DeepCopy() *CheckStatus { + cpy := &CheckStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies CheckStatus into another CheckStatus object +func (s *CheckStatus) DeepCopyInto(dst *CheckStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go index b77811fefc2..8716958ce02 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/check_status_gen.go @@ -3,16 +3,18 @@ package v0alpha1 // +k8s:openapi-gen=true -type CheckErrorLink struct { - // URL to a page with more information about the error - Url string `json:"url"` - // Human readable error message - Message string `json:"message"` +type CheckReport struct { + // Number of elements analyzed + Count int64 `json:"count"` + // List of failures + Failures []CheckReportFailure `json:"failures"` } -// NewCheckErrorLink creates a new CheckErrorLink object. -func NewCheckErrorLink() *CheckErrorLink { - return &CheckErrorLink{} +// NewCheckReport creates a new CheckReport object. +func NewCheckReport() *CheckReport { + return &CheckReport{ + Failures: []CheckReportFailure{}, + } } // +k8s:openapi-gen=true @@ -33,7 +35,22 @@ type CheckReportFailure struct { // NewCheckReportFailure creates a new CheckReportFailure object. func NewCheckReportFailure() *CheckReportFailure { - return &CheckReportFailure{} + return &CheckReportFailure{ + Links: []CheckErrorLink{}, + } +} + +// +k8s:openapi-gen=true +type CheckErrorLink struct { + // URL to a page with more information about the error + Url string `json:"url"` + // Human readable error message + Message string `json:"message"` +} + +// NewCheckErrorLink creates a new CheckErrorLink object. +func NewCheckErrorLink() *CheckErrorLink { + return &CheckErrorLink{} } // +k8s:openapi-gen=true @@ -56,7 +73,7 @@ func NewCheckstatusOperatorState() *CheckstatusOperatorState { // +k8s:openapi-gen=true type CheckStatus struct { - Report CheckV0alpha1StatusReport `json:"report"` + Report CheckReport `json:"report"` // operatorStates is a map of operator ID to operator state evaluations. // Any operator which consumes this kind SHOULD add its state evaluation information to this field. OperatorStates map[string]CheckstatusOperatorState `json:"operatorStates,omitempty"` @@ -67,7 +84,7 @@ type CheckStatus struct { // NewCheckStatus creates a new CheckStatus object. func NewCheckStatus() *CheckStatus { return &CheckStatus{ - Report: *NewCheckV0alpha1StatusReport(), + Report: *NewCheckReport(), } } @@ -87,16 +104,3 @@ const ( CheckStatusOperatorStateStateInProgress CheckStatusOperatorStateState = "in_progress" CheckStatusOperatorStateStateFailed CheckStatusOperatorStateState = "failed" ) - -// +k8s:openapi-gen=true -type CheckV0alpha1StatusReport struct { - // Number of elements analyzed - Count int64 `json:"count"` - // List of failures - Failures []CheckReportFailure `json:"failures"` -} - -// NewCheckV0alpha1StatusReport creates a new CheckV0alpha1StatusReport object. -func NewCheckV0alpha1StatusReport() *CheckV0alpha1StatusReport { - return &CheckV0alpha1StatusReport{} -} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go index 9998cffb37d..05e019ec5a7 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_metadata_gen.go @@ -24,5 +24,8 @@ type CheckTypeMetadata struct { // NewCheckTypeMetadata creates a new CheckTypeMetadata object. func NewCheckTypeMetadata() *CheckTypeMetadata { - return &CheckTypeMetadata{} + return &CheckTypeMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go index 80bd02b1cc8..22d4962c730 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_object_gen.go @@ -18,8 +18,11 @@ import ( type CheckType struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - Spec CheckTypeSpec `json:"spec" yaml:"spec"` - CheckTypeStatus CheckTypeStatus `json:"status" yaml:"status"` + + // Spec is the spec of the CheckType + Spec CheckTypeSpec `json:"spec" yaml:"spec"` + + Status CheckTypeStatus `json:"status" yaml:"status"` } func (o *CheckType) GetSpec() any { @@ -37,14 +40,14 @@ func (o *CheckType) SetSpec(spec any) error { func (o *CheckType) GetSubresources() map[string]any { return map[string]any{ - "status": o.CheckTypeStatus, + "status": o.Status, } } func (o *CheckType) GetSubresource(name string) (any, bool) { switch name { case "status": - return o.CheckTypeStatus, true + return o.Status, true default: return nil, false } @@ -57,7 +60,7 @@ func (o *CheckType) SetSubresource(name string, value any) error { if !ok { return fmt.Errorf("cannot set status type %#v, not of type CheckTypeStatus", value) } - o.CheckTypeStatus = cast + o.Status = cast return nil default: return fmt.Errorf("subresource '%s' does not exist", name) @@ -219,6 +222,20 @@ func (o *CheckType) DeepCopyObject() runtime.Object { return o.Copy() } +func (o *CheckType) DeepCopy() *CheckType { + cpy := &CheckType{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *CheckType) DeepCopyInto(dst *CheckType) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + // Interface compliance compile-time check var _ resource.Object = &CheckType{} @@ -262,5 +279,41 @@ func (o *CheckTypeList) SetItems(items []resource.Object) { } } +func (o *CheckTypeList) DeepCopy() *CheckTypeList { + cpy := &CheckTypeList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *CheckTypeList) DeepCopyInto(dst *CheckTypeList) { + resource.CopyObjectInto(dst, o) +} + // Interface compliance compile-time check var _ resource.ListObject = &CheckTypeList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *CheckTypeSpec) DeepCopy() *CheckTypeSpec { + cpy := &CheckTypeSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *CheckTypeSpec) DeepCopyInto(dst *CheckTypeSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of CheckTypeStatus +func (s *CheckTypeStatus) DeepCopy() *CheckTypeStatus { + cpy := &CheckTypeStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies CheckTypeStatus into another CheckTypeStatus object +func (s *CheckTypeStatus) DeepCopyInto(dst *CheckTypeStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go index 528643b69dd..d4d3f2d4f86 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/checktype_spec_gen.go @@ -23,5 +23,7 @@ type CheckTypeSpec struct { // NewCheckTypeSpec creates a new CheckTypeSpec object. func NewCheckTypeSpec() *CheckTypeSpec { - return &CheckTypeSpec{} + return &CheckTypeSpec{ + Steps: []CheckTypeStep{}, + } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go b/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go index 213249b4c58..80c8a0f0620 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/constants.go @@ -3,16 +3,16 @@ package v0alpha1 import "k8s.io/apimachinery/pkg/runtime/schema" const ( - // Group is the API group used by all kinds in this package - Group = "advisor.grafana.app" - // Version is the API version used by all kinds in this package - Version = "v0alpha1" + // APIGroup is the API group used by all kinds in this package + APIGroup = "advisor.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v0alpha1" ) var ( // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package GroupVersion = schema.GroupVersion{ - Group: Group, - Version: Version, + Group: APIGroup, + Version: APIVersion, } ) diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go index 84e85e7f5ba..21e61de7f45 100644 --- a/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/zz_openapi_gen.go @@ -15,6 +15,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.Check": schema_pkg_apis_advisor_v0alpha1_Check(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckErrorLink": schema_pkg_apis_advisor_v0alpha1_CheckErrorLink(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckList": schema_pkg_apis_advisor_v0alpha1_CheckList(ref), + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport": schema_pkg_apis_advisor_v0alpha1_CheckReport(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure": schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec": schema_pkg_apis_advisor_v0alpha1_CheckSpec(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckStatus": schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref), @@ -24,7 +25,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStatus": schema_pkg_apis_advisor_v0alpha1_CheckTypeStatus(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeStep": schema_pkg_apis_advisor_v0alpha1_CheckTypeStep(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypestatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref), - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport": schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref), "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState": schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref), } } @@ -57,8 +57,9 @@ func schema_pkg_apis_advisor_v0alpha1_Check(ref common.ReferenceCallback) common }, "spec": { SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec"), + Description: "Spec is the spec of the Check", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckSpec"), }, }, "status": { @@ -153,6 +154,43 @@ func schema_pkg_apis_advisor_v0alpha1_CheckList(ref common.ReferenceCallback) co } } +func schema_pkg_apis_advisor_v0alpha1_CheckReport(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "count": { + SchemaProps: spec.SchemaProps{ + Description: "Number of elements analyzed", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "failures": { + SchemaProps: spec.SchemaProps{ + Description: "List of failures", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"), + }, + }, + }, + }, + }, + }, + Required: []string{"count", "failures"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"}, + } +} + func schema_pkg_apis_advisor_v0alpha1_CheckReportFailure(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -258,7 +296,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback) "report": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport"), + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport"), }, }, "operatorStates": { @@ -296,7 +334,7 @@ func schema_pkg_apis_advisor_v0alpha1_CheckStatus(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckV0alpha1StatusReport", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState"}, + "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReport", "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckstatusOperatorState"}, } } @@ -328,8 +366,9 @@ func schema_pkg_apis_advisor_v0alpha1_CheckType(ref common.ReferenceCallback) co }, "spec": { SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"), + Description: "Spec is the spec of the CheckType", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckTypeSpec"), }, }, "status": { @@ -566,43 +605,6 @@ func schema_pkg_apis_advisor_v0alpha1_CheckTypestatusOperatorState(ref common.Re } } -func schema_pkg_apis_advisor_v0alpha1_CheckV0alpha1StatusReport(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "count": { - SchemaProps: spec.SchemaProps{ - Description: "Number of elements analyzed", - Default: 0, - Type: []string{"integer"}, - Format: "int64", - }, - }, - "failures": { - SchemaProps: spec.SchemaProps{ - Description: "List of failures", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"), - }, - }, - }, - }, - }, - }, - Required: []string{"count", "failures"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1.CheckReportFailure"}, - } -} - func schema_pkg_apis_advisor_v0alpha1_CheckstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 6f5d472dc54..eb596a274e2 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -7,15 +7,19 @@ package apis import ( "encoding/json" + "fmt" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + + v0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" ) var ( - rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + rawSchemaCheckv0alpha1 = []byte(`{"spec":{"properties":{"data":{"additionalProperties":{"type":"string"},"description":"Generic data input that a check can receive","type":"object"}},"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"report":{"properties":{"count":{"description":"Number of elements analyzed","type":"integer"},"failures":{"description":"List of failures","items":{"properties":{"item":{"description":"Human readable identifier of the item that failed","type":"string"},"itemID":{"description":"ID of the item that failed","type":"string"},"links":{"description":"Links to actions that can be taken to resolve the failure","items":{"properties":{"message":{"description":"Human readable error message","type":"string"},"url":{"description":"URL to a page with more information about the error","type":"string"}},"required":["url","message"],"type":"object"},"type":"array"},"moreInfo":{"description":"More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.","type":"string"},"severity":{"description":"Severity of the failure","enum":["high","low"],"type":"string"},"stepID":{"description":"Step ID that the failure is associated with","type":"string"}},"required":["severity","stepID","item","itemID","links"],"type":"object"},"type":"array"}},"required":["count","failures"],"type":"object"}},"required":["report"],"type":"object"}}`) versionSchemaCheckv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckv0alpha1, &versionSchemaCheckv0alpha1) - rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object","x-kubernetes-preserve-unknown-fields":true}}`) + rawSchemaCheckTypev0alpha1 = []byte(`{"spec":{"properties":{"name":{"type":"string"},"steps":{"items":{"properties":{"description":{"type":"string"},"resolution":{"type":"string"},"stepID":{"type":"string"},"title":{"type":"string"}},"required":["title","description","stepID","resolution"],"type":"object"},"type":"array"}},"required":["name","steps"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaCheckTypev0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCheckTypev0alpha1, &versionSchemaCheckTypev0alpha1) ) @@ -58,12 +62,6 @@ var appManifestData = app.ManifestData{ }, } -func jsonToMap(j string) map[string]any { - m := make(map[string]any) - json.Unmarshal([]byte(j), &j) - return m -} - func LocalManifest() app.Manifest { return app.NewEmbeddedManifest(appManifestData) } @@ -71,3 +69,15 @@ func LocalManifest() app.Manifest { func RemoteManifest() app.Manifest { return app.NewAPIServerManifest("advisor") } + +var kindVersionToGoType = map[string]resource.Kind{ + "Check/v0alpha1": v0alpha1.CheckKind(), + "CheckType/v0alpha1": v0alpha1.CheckTypeKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} diff --git a/apps/advisor/pkg/app/checks/utils.go b/apps/advisor/pkg/app/checks/utils.go index 0abcb8a603b..e58375ee64b 100644 --- a/apps/advisor/pkg/app/checks/utils.go +++ b/apps/advisor/pkg/app/checks/utils.go @@ -106,3 +106,25 @@ func SetStatusAnnotation(ctx context.Context, client resource.Client, obj resour }}, }, resource.PatchOptions{}, obj) } + +func SetAnnotations(ctx context.Context, client resource.Client, obj resource.Object, annotations map[string]string) error { + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/metadata/annotations", + Value: annotations, + }}, + }, resource.PatchOptions{}, obj) +} + +func SetStatus(ctx context.Context, client resource.Client, obj resource.Object, status any) error { + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/status", + Value: status, + }}, + }, resource.PatchOptions{ + Subresource: "status", + }, obj) +} diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index 83dba0cf42a..cf090b67260 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -78,28 +78,21 @@ func processCheck(ctx context.Context, log logging.Logger, client resource.Clien return fmt.Errorf("error running steps: %w", err) } - report := &advisorv0alpha1.CheckV0alpha1StatusReport{ + report := &advisorv0alpha1.CheckReport{ Failures: failures, Count: int64(len(items)), } + c.Status.Report = *report + err = checks.SetStatus(ctx, client, obj, c.Status) + if err != nil { + return err + } // Set the status annotation to processed and annotate the steps ignored annotations := checks.AddAnnotations(ctx, obj, map[string]string{ checks.StatusAnnotation: checks.StatusAnnotationProcessed, checks.IgnoreStepsAnnotationList: checkType.GetAnnotations()[checks.IgnoreStepsAnnotationList], }) - return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ - Operations: []resource.PatchOperation{ - { - Operation: resource.PatchOpAdd, - Path: "/status/report", - Value: *report, - }, { - Operation: resource.PatchOpAdd, - Path: "/metadata/annotations", - Value: annotations, - }, - }, - }, resource.PatchOptions{}, obj) + return checks.SetAnnotations(ctx, client, obj, annotations) } func processCheckRetry(ctx context.Context, log logging.Logger, client resource.Client, typesClient resource.Client, obj resource.Object, check checks.Check) error { @@ -157,7 +150,7 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource. } } // Pull failures from the report for the items to retry - c.CheckStatus.Report.Failures = slices.DeleteFunc(c.CheckStatus.Report.Failures, func(f advisorv0alpha1.CheckReportFailure) bool { + c.Status.Report.Failures = slices.DeleteFunc(c.Status.Report.Failures, func(f advisorv0alpha1.CheckReportFailure) bool { if f.ItemID == itemToRetry { for _, newFailure := range failures { if newFailure.StepID == f.StepID { @@ -171,19 +164,13 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource. // Failure not in the list of items to retry, keep it return false }) + err = checks.SetStatus(ctx, client, obj, c.Status) + if err != nil { + return err + } // Delete the retry annotation to mark the check as processed annotations := checks.DeleteAnnotations(ctx, obj, []string{checks.RetryAnnotation}) - return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ - Operations: []resource.PatchOperation{{ - Operation: resource.PatchOpAdd, - Path: "/status/report", - Value: c.CheckStatus.Report, - }, { - Operation: resource.PatchOpAdd, - Path: "/metadata/annotations", - Value: annotations, - }}, - }, resource.PatchOptions{}, obj) + return checks.SetAnnotations(ctx, client, obj, annotations) } func runStepsInParallel(ctx context.Context, log logging.Logger, spec *advisorv0alpha1.CheckSpec, steps []checks.Step, items []any) ([]advisorv0alpha1.CheckReportFailure, error) { diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index dc8c635f6e8..df4b016db93 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -95,9 +95,9 @@ func TestProcessMultipleCheckItems(t *testing.T) { err = processCheck(ctx, logging.DefaultLogger, client, typesClient, obj, check) assert.NoError(t, err) assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation]) - r := client.lastValue.(advisorv0alpha1.CheckV0alpha1StatusReport) - assert.Equal(t, r.Count, int64(100)) - assert.Len(t, r.Failures, 50) + r := client.values[0].(advisorv0alpha1.CheckStatus) + assert.Equal(t, r.Report.Count, int64(100)) + assert.Len(t, r.Report.Failures, 50) } func TestProcessCheck_AlreadyProcessed(t *testing.T) { @@ -231,7 +231,7 @@ func TestProcessCheckRetry_SkipMissingItem(t *testing.T) { checks.RetryAnnotation: "item", checks.StatusAnnotation: checks.StatusAnnotationProcessed, }) - obj.CheckStatus.Report.Failures = []advisorv0alpha1.CheckReportFailure{ + obj.Status.Report.Failures = []advisorv0alpha1.CheckReportFailure{ { ItemID: "item", StepID: "step", @@ -254,7 +254,7 @@ func TestProcessCheckRetry_SkipMissingItem(t *testing.T) { assert.NoError(t, err) assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation]) assert.Empty(t, obj.GetAnnotations()[checks.RetryAnnotation]) - assert.Empty(t, obj.CheckStatus.Report.Failures) + assert.Empty(t, obj.Status.Report.Failures) } func TestProcessCheckRetry_Success(t *testing.T) { @@ -263,7 +263,7 @@ func TestProcessCheckRetry_Success(t *testing.T) { checks.RetryAnnotation: "item", checks.StatusAnnotation: checks.StatusAnnotationProcessed, }) - obj.CheckStatus.Report.Failures = []advisorv0alpha1.CheckReportFailure{ + obj.Status.Report.Failures = []advisorv0alpha1.CheckReportFailure{ { ItemID: "item", StepID: "step", @@ -286,16 +286,16 @@ func TestProcessCheckRetry_Success(t *testing.T) { assert.NoError(t, err) assert.Equal(t, checks.StatusAnnotationProcessed, obj.GetAnnotations()[checks.StatusAnnotation]) assert.Empty(t, obj.GetAnnotations()[checks.RetryAnnotation]) - assert.Empty(t, obj.CheckStatus.Report.Failures) + assert.Empty(t, obj.Status.Report.Failures) } type mockClient struct { resource.Client - lastValue any + values []any } func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { - m.lastValue = req.Operations[0].Value + m.values = append(m.values, req.Operations[0].Value) return nil } diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 21243f3510a..f9ea5babd51 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,10 +3,10 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.39.0 - github.com/grafana/grafana-app-sdk/logging v0.38.2 - k8s.io/apimachinery v0.33.1 - k8s.io/apiserver v0.33.1 + github.com/grafana/grafana-app-sdk v0.39.2 + github.com/grafana/grafana-app-sdk/logging v0.39.1 + k8s.io/apimachinery v0.33.2 + k8s.io/apiserver v0.33.2 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -89,18 +89,18 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect - k8s.io/client-go v0.33.1 // indirect - k8s.io/component-base v0.33.1 // indirect + k8s.io/api v0.33.2 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect + k8s.io/client-go v0.33.2 // indirect + k8s.io/component-base v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 1ce129e856f..c906b01a1af 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -84,10 +84,10 @@ 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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yzbjm6T1T1w= -github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= 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= @@ -317,12 +317,12 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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= @@ -352,18 +352,18 @@ 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/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go index acda5c7d1c9..314d36b602a 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), - resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*Receiver) diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go index 339d0bd6d04..509bfc4aeca 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"), - resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*TemplateGroup) diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go index a6c809e0d3b..8c5459f0059 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"), - resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.name", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*TimeInterval) diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index 1a948f1b9a8..950c809c0ec 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -1,4 +1,4 @@ -APP_SDK_VERSION := v0.39.0 +APP_SDK_VERSION := v0.39.2 APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk @@ -60,3 +60,10 @@ post-generate-cleanup: ## Clean up the generated code @echo "// To sync changes, run: make generate in apps/dashboard" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @echo "" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @cat ./kinds/v2alpha1/dashboard_spec.cue >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue + + # Copy dashboard/v2alpha2 spec so we can use it for schema validation + @echo "// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY" > ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue + @echo "// Source: apps/dashboard/kinds/v2alpha2/dashboard_spec.cue" >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue + @echo "// To sync changes, run: make generate in apps/dashboard" >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue + @echo "" >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue + @cat ./kinds/v2alpha2/dashboard_spec.cue >> ./pkg/apis/dashboard/v2alpha2/dashboard_spec.cue diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 6d9028dccf9..837887e08e1 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -4,11 +4,11 @@ go 1.24.4 require ( cuelang.org/go v0.11.1 - github.com/grafana/grafana-app-sdk v0.39.0 + github.com/grafana/grafana-app-sdk v0.39.2 github.com/grafana/grafana-plugin-sdk-go v0.278.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/stretchr/testify v1.10.0 - k8s.io/apimachinery v0.33.1 + k8s.io/apimachinery v0.33.2 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -44,7 +44,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect @@ -116,14 +116,14 @@ require ( golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.34.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 412e87a6034..b73b3aab637 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -94,10 +94,10 @@ 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-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yzbjm6T1T1w= -github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana-plugin-sdk-go v0.278.0 h1:5/rIYparLi02pofdaag8wnjspMMVNCi8cZhC4cdC3Ho= github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= @@ -357,10 +357,10 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -379,12 +379,12 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/apps/dashboard/kinds/dashboard.cue b/apps/dashboard/kinds/dashboard.cue index b8aaf17bc7c..d48765539c8 100644 --- a/apps/dashboard/kinds/dashboard.cue +++ b/apps/dashboard/kinds/dashboard.cue @@ -3,7 +3,8 @@ package kinds import ( v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1beta1" - v2 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" + v2alpha1 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" + v2alpha2 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha2" ) // Status is the shared status of all dashboard versions. @@ -63,7 +64,13 @@ dashboard: { } "v2alpha1": { schema: { - spec: v2.DashboardSpec + spec: v2alpha1.DashboardSpec + status: DashboardStatus + } + } + "v2alpha2": { + schema: { + spec: v2alpha2.DashboardSpec status: DashboardStatus } } diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index ccce02984c8..8ab1891e626 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -734,6 +734,8 @@ QueryVariableSpec: { allValue?: string placeholder?: string allowCustomValue: bool | *true + staticOptions?: [...VariableOption] + staticOptionsOrder?: "before" | "after" | "sorted" } // Query variable kind diff --git a/apps/dashboard/kinds/v2alpha2/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha2/dashboard_spec.cue new file mode 100644 index 00000000000..4c1202caea7 --- /dev/null +++ b/apps/dashboard/kinds/v2alpha2/dashboard_spec.cue @@ -0,0 +1,964 @@ +package v2alpha2 + +DashboardSpec: { + annotations: [...AnnotationQueryKind] | *[] + + // Configuration of dashboard cursor sync behavior. + // "Off" for no shared crosshair or tooltip (default). + // "Crosshair" for shared crosshair. + // "Tooltip" for shared crosshair AND shared tooltip. + cursorSync: DashboardCursorSync + + // Description of dashboard. + description?: string + + // Whether a dashboard is editable or not. + editable?: bool | *true + + elements: [ElementReference.name]: Element | *{} + + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + + // Links with references to other dashboards or external websites. + links: [...DashboardLink] | *[] + + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data. + liveNow?: bool + + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + preload: bool | *false + + // Plugins only. The version of the dashboard installed together with the plugin. + // This is used to determine if the dashboard should be updated when the plugin is updated. + revision?: uint16 + + // Tags associated with dashboard. + tags: [...string] | *[] + + timeSettings: TimeSettingsSpec + + // Title of dashboard. + title: string + + // Configured template variables. + variables: [...VariableKind] | *[] +} + +// Supported dashboard elements +Element: PanelKind | LibraryPanelKind // |* more element types in the future + +LibraryPanelKind: { + kind: "LibraryPanel" + spec: LibraryPanelKindSpec +} + +LibraryPanelKindSpec: { + // Panel ID for the library panel in the dashboard + id: number + // Title for the library panel in the dashboard + title: string + + libraryPanel: LibraryPanelRef +} + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +LibraryPanelRef: { + // Library panel name + name: string + // Library panel uid + uid: string +} + +AnnotationPanelFilter: { + // Should the specified panels be included or excluded + exclude?: bool | *false + + // Panel IDs that should be included or excluded + ids: [...uint32] +} + +// "Off" for no shared crosshair or tooltip (default). +// "Crosshair" for shared crosshair. +// "Tooltip" for shared crosshair AND shared tooltip. +DashboardCursorSync: "Crosshair" | "Tooltip" | *"Off" + +// Links with references to other dashboards or external resources +DashboardLink: { + // Title to display with the link + title: string + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` + type: DashboardLinkType + // Icon name to be displayed with the link + icon: string + // Tooltip to display when the user hovers their mouse over it + tooltip: string + // Link URL. Only required/valid if the type is link + url?: string + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: [...string] | *[] + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: bool | *false + // If true, the link will be opened in a new tab + targetBlank: bool | *false + // If true, includes current template variables values in the link as query params + includeVars: bool | *false + // If true, includes current time range in the link as query params + keepTime: bool | *false +} + +DataSourceRef: { + // The plugin type-id + type?: string + + // Specific datasource instance + uid?: string +} + +// A topic is attached to DataFrame metadata in query results. +// This specifies where the data should be used. +DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") + +// Transformations allow to manipulate data returned by a query before the system applies a visualization. +// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, +// use the output of one transformation as the input to another transformation, etc. +DataTransformerConfig: { + // Unique identifier of transformer + id: string + // Disabled transformations are skipped + disabled?: bool + // Optional frame matcher. When missing it will be applied to all results + filter?: MatcherConfig + // Where to pull DataFrames from as input to transformation + topic?: DataTopic + // Options to be passed to the transformer + // Valid options depend on the transformer id + options: _ +} + +DataLink: { + title: string + url: string + targetBlank?: bool +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +FieldConfigSource: { + // Defaults are the options applied to all fields. + defaults: FieldConfig + // Overrides are the options applied to specific fields overriding the defaults. + overrides: [...{ + matcher: MatcherConfig + properties: [...DynamicConfigValue] + }] +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +FieldConfig: { + // The display value for this field. This supports template variables blank is auto + displayName?: string + + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string + + // Human readable field metadata + description?: string + + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string + + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool + + // True if data source field supports ad-hoc filters + filterable?: bool + + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + unit?: string + + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + decimals?: number + + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + min?: number + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + max?: number + + // Convert input values into a display string + mappings?: [...ValueMapping] + + // Map numeric values to states + thresholds?: ThresholdsConfig + + // Panel color configuration + color?: FieldColor + + // The behavior when clicking on a result + links?: [...] + + // Alternative to empty string + noValue?: string + + // custom is specified by the FieldConfig field + // in panel plugin schemas. + custom?: {...} +} + +DynamicConfigValue: { + id: string | *"" + value?: _ +} + +// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. +// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. +MatcherConfig: { + // The matcher id. This is used to find the matcher implementation from registry. + id: string | *"" + // The matcher options. This is specific to the matcher implementation. + options?: _ +} + +Threshold: { + value: number + color: string +} + +ThresholdsMode: "absolute" | "percentage" + +ThresholdsConfig: { + mode: ThresholdsMode + steps: [...Threshold] +} + +ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap + +// Supported value mapping types +// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. +MappingType: "value" | "range" | "regex" | "special" + +// Maps text values to a color or different display text and color. +// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +ValueMap: { + type: MappingType & "value" + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + options: [string]: ValueMappingResult +} + +// Maps numerical ranges to a display text and color. +// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +RangeMap: { + type: MappingType & "range" + // Range to match against and the result to apply when the value is within the range + options: { + // Min value of the range. It can be null which means -Infinity + from: float64 | null + // Max value of the range. It can be null which means +Infinity + to: float64 | null + // Config to apply when the value is within the range + result: ValueMappingResult + } +} + +// Maps regular expressions to replacement text and a color. +// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +RegexMap: { + type: MappingType & "regex" + // Regular expression to match against and the result to apply when the value matches the regex + options: { + // Regular expression to match against + pattern: string + // Config to apply when the value matches the regex + result: ValueMappingResult + } +} + +// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. +// See SpecialValueMatch to see the list of special values. +// For example, you can configure a special value mapping so that null values appear as N/A. +SpecialValueMap: { + type: MappingType & "special" + options: { + // Special value to match against + match: SpecialValueMatch + // Config to apply when the value matches the special value + result: ValueMappingResult + } +} + +// Special value types supported by the `SpecialValueMap` +SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cog(kind="enum",memberNames="True|False|Null|NaN|NullAndNaN|Empty") + +// Result used as replacement with text and color when the value matches +ValueMappingResult: { + // Text to display when the value matches + text?: string + // Text to use when the value matches + color?: string + // Icon to display when the value matches. Only specific visualizations. + icon?: string + // Position in the mapping array. Only used internally. + index?: int32 +} + +// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. +// Continuous color interpolates a color using the percentage of a value relative to min and max. +// Accepted values are: +// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold +// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations +// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations +// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode +// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode +// `continuous-YlRd`: Continuous Yellow-Red palette mode +// `continuous-BlPu`: Continuous Blue-Purple palette mode +// `continuous-YlBl`: Continuous Yellow-Blue palette mode +// `continuous-blues`: Continuous Blue palette mode +// `continuous-reds`: Continuous Red palette mode +// `continuous-greens`: Continuous Green palette mode +// `continuous-purples`: Continuous Purple palette mode +// `shades`: Shades of a single color. Specify a single color, useful in an override rule. +// `fixed`: Fixed color mode. Specify a single color, useful in an override rule. +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" + +// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. +FieldColorSeriesByMode: "min" | "max" | "last" + +// Map a field to a color. +FieldColor: { + // The main color scheme mode. + mode: FieldColorModeId + // The fixed color value for fixed or shades color modes. + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: FieldColorSeriesByMode +} + +// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) +DashboardLinkType: "link" | "dashboards" + +// --- Common types --- +Kind: { + kind: string + spec: _ + metadata?: _ +} + +// --- Kinds --- +VizConfigSpec: { + pluginVersion: string + options: [string]: _ + fieldConfig: FieldConfigSource +} + +VizConfigKind: { + // The kind of a VizConfigKind is the plugin ID + kind: string + spec: VizConfigSpec +} + +AnnotationQuerySpec: { + datasource?: DataSourceRef + query?: DataQueryKind + enable: bool + hide: bool + iconColor: string + name: string + builtIn?: bool | *false + filter?: AnnotationPanelFilter + legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties +} + +AnnotationQueryKind: { + kind: "AnnotationQuery" + spec: AnnotationQuerySpec +} + +QueryOptionsSpec: { + timeFrom?: string + maxDataPoints?: int + timeShift?: string + queryCachingTTL?: int + interval?: string + cacheTimeout?: string + hideTimeOverride?: bool +} + +DataQueryKind: { + // The kind of a DataQueryKind is the datasource type + kind: string + spec: [string]: _ +} + +PanelQuerySpec: { + query: DataQueryKind + datasource?: DataSourceRef + + refId: string + hidden: bool +} + +PanelQueryKind: { + kind: "PanelQuery" + spec: PanelQuerySpec +} + +TransformationKind: { + // The kind of a TransformationKind is the transformation ID + kind: string + spec: DataTransformerConfig +} + +QueryGroupSpec: { + queries: [...PanelQueryKind] + transformations: [...TransformationKind] + queryOptions: QueryOptionsSpec +} + +QueryGroupKind: { + kind: "QueryGroup" + spec: QueryGroupSpec +} + +TimeRangeOption: { + display: string | *"Last 6 hours" + from: string | *"now-6h" + to: string | *"now" +} + +// Time configuration +// It defines the default time config for the time picker, the refresh picker for the specific dashboard. +TimeSettingsSpec: { + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"browser" + // Start time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + from: string | *"now-6h" + // End time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + to: string | *"now" + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + autoRefresh: string | *"" // v1: refresh + // Interval options available in the refresh picker dropdown. + autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals + // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. + quickRanges?: [...TimeRangeOption] // v1: timepicker.quick_ranges , not exposed in the UI + // Whether timepicker is visible or not. + hideTimepicker: bool | *false // v1: timepicker.hidden + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: "saturday" | "monday" | "sunday" + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth: int | *0 + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + nowDelay?: string // v1: timepicker.nowDelay +} + +RepeatMode: "variable" // other repeat modes will be added in the future: label, frame + +RepeatOptions: { + mode: RepeatMode + value: string + direction?: "h" | "v" + maxPerRow?: int +} + +RowRepeatOptions: { + mode: RepeatMode + value: string +} + +TabRepeatOptions: { + mode: RepeatMode + value: string +} + +AutoGridRepeatOptions: { + mode: RepeatMode + value: string +} + +GridLayoutItemSpec: { + x: int + y: int + width: int + height: int + element: ElementReference // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference + repeat?: RepeatOptions +} + +GridLayoutItemKind: { + kind: "GridLayoutItem" + spec: GridLayoutItemSpec +} + +GridLayoutSpec: { + items: [...GridLayoutItemKind] +} + +GridLayoutKind: { + kind: "GridLayout" + spec: GridLayoutSpec +} + +RowsLayoutKind: { + kind: "RowsLayout" + spec: RowsLayoutSpec +} + +RowsLayoutSpec: { + rows: [...RowsLayoutRowKind] +} + +RowsLayoutRowKind: { + kind: "RowsLayoutRow" + spec: RowsLayoutRowSpec +} + +RowsLayoutRowSpec: { + title?: string + collapse?: bool + hideHeader?: bool + fillScreen?: bool + conditionalRendering?: ConditionalRenderingGroupKind + repeat?: RowRepeatOptions + layout: GridLayoutKind | AutoGridLayoutKind | TabsLayoutKind | RowsLayoutKind +} + +AutoGridLayoutKind: { + kind: "AutoGridLayout" + spec: AutoGridLayoutSpec +} + +AutoGridLayoutSpec: { + maxColumnCount?: number | *3 + columnWidthMode: "narrow" | *"standard" | "wide" | "custom" + columnWidth?: number + rowHeightMode: "short" | *"standard" | "tall" | "custom" + rowHeight?: number + fillScreen?: bool | *false + items: [...AutoGridLayoutItemKind] +} + +AutoGridLayoutItemKind: { + kind: "AutoGridLayoutItem" + spec: AutoGridLayoutItemSpec +} + +AutoGridLayoutItemSpec: { + element: ElementReference + repeat?: AutoGridRepeatOptions + conditionalRendering?: ConditionalRenderingGroupKind +} + +TabsLayoutKind: { + kind: "TabsLayout" + spec: TabsLayoutSpec +} + +TabsLayoutSpec: { + tabs: [...TabsLayoutTabKind] +} + +TabsLayoutTabKind: { + kind: "TabsLayoutTab" + spec: TabsLayoutTabSpec +} + +TabsLayoutTabSpec: { + title?: string + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + conditionalRendering?: ConditionalRenderingGroupKind + repeat?: TabRepeatOptions +} + +PanelSpec: { + id: number + title: string + description: string + links: [...DataLink] + data: QueryGroupKind + vizConfig: VizConfigKind + transparent?: bool +} + +PanelKind: { + kind: "Panel" + spec: PanelSpec +} + +ElementReference: { + kind: "ElementReference" + name: string +} + +// Start FIXME: variables - in CUE PR - this are things that should be added into the cue schema +// TODO: properties such as `hide`, `skipUrlSync`, `multi` are type boolean, and in the old schema they are conditional, +// should we make them conditional in the new schema as well? or should we make them required but default to false? + +// Variable types +VariableValue: VariableValueSingle | [...VariableValueSingle] + +VariableValueSingle: string | bool | number | CustomVariableValue + +// Custom formatter variable +CustomFormatterVariable: { + name: string + type: VariableType + multi: bool + includeAll: bool +} + +// Custom variable value +CustomVariableValue: { + // The format name or function used in the expression + formatter: *null | string | VariableCustomFormatterFn +} + +// Custom formatter function +VariableCustomFormatterFn: { + value: _ + legacyVariableModel: { + name: string + type: VariableType + multi: bool + includeAll: bool + } + legacyDefaultFormatter?: VariableCustomFormatterFn +} + +// Dashboard variable type +// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. +// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). +// `constant`: Define a hidden constant. +// `datasource`: Quickly change the data source for an entire dashboard. +// `interval`: Interval variables represent time spans. +// `textbox`: Display a free text input field with an optional default value. +// `custom`: Define the variable options manually using a comma-separated list. +// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables +VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | + "system" | "snapshot" + +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind + +// Sort variable options +// Accepted values are: +// `disabled`: No sorting +// `alphabeticalAsc`: Alphabetical ASC +// `alphabeticalDesc`: Alphabetical DESC +// `numericalAsc`: Numerical ASC +// `numericalDesc`: Numerical DESC +// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC +// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC +// `naturalAsc`: Natural ASC +// `naturalDesc`: Natural DESC +// VariableSort enum with default value +VariableSort: "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc" + +// Options to config when to refresh a variable +// `never`: Never refresh the variable +// `onDashboardLoad`: Queries the data source every time the dashboard loads. +// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. +VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" + +// Determine if the variable shows on dashboard +// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). +VariableHide: *"dontHide" | "hideLabel" | "hideVariable" + +// Determine the origin of the adhoc variable filter +FilterOrigin: "dashboard" + +// FIXME: should we introduce this? --- Variable value option +VariableValueOption: { + label: string + value: VariableValueSingle + group?: string +} + +// Variable option specification +VariableOption: { + // Whether the option is selected or not + selected?: bool + // Text to be displayed for the option + text: string | [...string] + // Value of the option + value: string | [...string] +} + +// Query variable specification +QueryVariableSpec: { + name: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + label?: string + hide: VariableHide + refresh: VariableRefresh + skipUrlSync: bool | *false + description?: string + datasource?: DataSourceRef + query: DataQueryKind + regex: string | *"" + sort: VariableSort + definition?: string + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + placeholder?: string + allowCustomValue: bool | *true + staticOptions?: [...VariableOption] + staticOptionsOrder?: "before" | "after" | "sorted" +} + +// Query variable kind +QueryVariableKind: { + kind: "QueryVariable" + spec: QueryVariableSpec +} + +// Text variable specification +TextVariableSpec: { + name: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + query: string | *"" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Text variable kind +TextVariableKind: { + kind: "TextVariable" + spec: TextVariableSpec +} + +// Constant variable specification +ConstantVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Constant variable kind +ConstantVariableKind: { + kind: "ConstantVariable" + spec: ConstantVariableSpec +} + +// Datasource variable specification +DatasourceVariableSpec: { + name: string | *"" + pluginId: string | *"" + refresh: VariableRefresh + regex: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string + allowCustomValue: bool | *true +} + +// Datasource variable kind +DatasourceVariableKind: { + kind: "DatasourceVariable" + spec: DatasourceVariableSpec +} + +// Interval variable specification +IntervalVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + auto: bool | *false + auto_min: string | *"" + auto_count: int | *0 + refresh: VariableRefresh + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Interval variable kind +IntervalVariableKind: { + kind: "IntervalVariable" + spec: IntervalVariableSpec +} + +// Custom variable specification +CustomVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string + allowCustomValue: bool | *true +} + +// Custom variable kind +CustomVariableKind: { + kind: "CustomVariable" + spec: CustomVariableSpec +} + +// GroupBy variable specification +GroupByVariableSpec: { + name: string | *"" + datasource?: DataSourceRef + defaultValue?: VariableOption + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + multi: bool | *false + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Group variable kind +GroupByVariableKind: { + kind: "GroupByVariable" + spec: GroupByVariableSpec +} + +// Adhoc variable specification +AdhocVariableSpec: { + name: string | *"" + datasource?: DataSourceRef + baseFilters: [...AdHocFilterWithLabels] | *[] + filters: [...AdHocFilterWithLabels] | *[] + defaultKeys: [...MetricFindValue] | *[] + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string + allowCustomValue: bool | *true +} + +// Define the MetricFindValue type +MetricFindValue: { + text: string + value?: string | number + group?: string + expandable?: bool +} + +// Define the AdHocFilterWithLabels type +AdHocFilterWithLabels: { + key: string + operator: string + value: string + values?: [...string] + keyLabel?: string + valueLabels?: [...string] + forceEdit?: bool + origin?: FilterOrigin + // @deprecated + condition?: string +} + +// Adhoc variable kind +AdhocVariableKind: { + kind: "AdhocVariable" + spec: AdhocVariableSpec +} + +ConditionalRenderingGroupKind: { + kind: "ConditionalRenderingGroup" + spec: ConditionalRenderingGroupSpec +} + +ConditionalRenderingGroupSpec: { + visibility: "show" | "hide" + condition: "and" | "or" + items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind] +} + +ConditionalRenderingVariableKind: { + kind: "ConditionalRenderingVariable" + spec: ConditionalRenderingVariableSpec +} + +ConditionalRenderingVariableSpec: { + variable: string + operator: "equals" | "notEquals" + value: string +} + +ConditionalRenderingDataKind: { + kind: "ConditionalRenderingData" + spec: ConditionalRenderingDataSpec +} + +ConditionalRenderingDataSpec: { + value: bool +} + +ConditionalRenderingTimeRangeSizeKind: { + kind: "ConditionalRenderingTimeRangeSize" + spec: ConditionalRenderingTimeRangeSizeSpec +} + +ConditionalRenderingTimeRangeSizeSpec: { + value: string +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index b9fd38146b7..de90006e396 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -219,6 +219,10 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Additional static options for query variable + staticOptions?: [...#VariableOption] + // Ordering of static options in relation to options returned from data source for query variable + staticOptionsOrder?: "before" | "after" | "sorted" ... } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index b9fd38146b7..de90006e396 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -219,6 +219,10 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Additional static options for query variable + staticOptions?: [...#VariableOption] + // Ordering of static options in relation to options returned from data source for query variable + staticOptionsOrder?: "before" | "after" | "sorted" ... } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 98fc8804de6..160adf4e7bf 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -738,6 +738,8 @@ QueryVariableSpec: { allValue?: string placeholder?: string allowCustomValue: bool | *true + staticOptions?: [...VariableOption] + staticOptionsOrder?: "before" | "after" | "sorted" } // Query variable kind diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 5b599cb414e..9ddbe89502f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -763,7 +763,9 @@ type DashboardRepeatOptions struct { // NewDashboardRepeatOptions creates a new DashboardRepeatOptions object. func NewDashboardRepeatOptions() *DashboardRepeatOptions { - return &DashboardRepeatOptions{} + return &DashboardRepeatOptions{ + Mode: DashboardRepeatMode, + } } // other repeat modes will be added in the future: label, frame @@ -938,7 +940,9 @@ type DashboardRowRepeatOptions struct { // NewDashboardRowRepeatOptions creates a new DashboardRowRepeatOptions object. func NewDashboardRowRepeatOptions() *DashboardRowRepeatOptions { - return &DashboardRowRepeatOptions{} + return &DashboardRowRepeatOptions{ + Mode: DashboardRepeatMode, + } } // +k8s:openapi-gen=true @@ -1011,7 +1015,9 @@ type DashboardAutoGridRepeatOptions struct { // NewDashboardAutoGridRepeatOptions creates a new DashboardAutoGridRepeatOptions object. func NewDashboardAutoGridRepeatOptions() *DashboardAutoGridRepeatOptions { - return &DashboardAutoGridRepeatOptions{} + return &DashboardAutoGridRepeatOptions{ + Mode: DashboardRepeatMode, + } } // +k8s:openapi-gen=true @@ -1077,7 +1083,9 @@ type DashboardTabRepeatOptions struct { // NewDashboardTabRepeatOptions creates a new DashboardTabRepeatOptions object. func NewDashboardTabRepeatOptions() *DashboardTabRepeatOptions { - return &DashboardTabRepeatOptions{} + return &DashboardTabRepeatOptions{ + Mode: DashboardRepeatMode, + } } // Links with references to other dashboards or external resources @@ -1214,24 +1222,26 @@ func NewDashboardQueryVariableKind() *DashboardQueryVariableKind { // Query variable specification // +k8s:openapi-gen=true type DashboardQueryVariableSpec struct { - Name string `json:"name"` - Current DashboardVariableOption `json:"current"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - Refresh DashboardVariableRefresh `json:"refresh"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` - Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` - Query DashboardDataQueryKind `json:"query"` - Regex string `json:"regex"` - Sort DashboardVariableSort `json:"sort"` - Definition *string `json:"definition,omitempty"` - Options []DashboardVariableOption `json:"options"` - Multi bool `json:"multi"` - IncludeAll bool `json:"includeAll"` - AllValue *string `json:"allValue,omitempty"` - Placeholder *string `json:"placeholder,omitempty"` - AllowCustomValue bool `json:"allowCustomValue"` + Name string `json:"name"` + Current DashboardVariableOption `json:"current"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + Refresh DashboardVariableRefresh `json:"refresh"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + Query DashboardDataQueryKind `json:"query"` + Regex string `json:"regex"` + Sort DashboardVariableSort `json:"sort"` + Definition *string `json:"definition,omitempty"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + IncludeAll bool `json:"includeAll"` + AllValue *string `json:"allValue,omitempty"` + Placeholder *string `json:"placeholder,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` + StaticOptions []DashboardVariableOption `json:"staticOptions,omitempty"` + StaticOptionsOrder *DashboardQueryVariableSpecStaticOptionsOrder `json:"staticOptionsOrder,omitempty"` } // NewDashboardQueryVariableSpec creates a new DashboardQueryVariableSpec object. @@ -1687,7 +1697,9 @@ type DashboardAdHocFilterWithLabels struct { // NewDashboardAdHocFilterWithLabels creates a new DashboardAdHocFilterWithLabels object. func NewDashboardAdHocFilterWithLabels() *DashboardAdHocFilterWithLabels { - return &DashboardAdHocFilterWithLabels{} + return &DashboardAdHocFilterWithLabels{ + Origin: DashboardFilterOrigin, + } } // Determine the origin of the adhoc variable filter @@ -1880,6 +1892,15 @@ const ( DashboardTimeSettingsSpecWeekStartSunday DashboardTimeSettingsSpecWeekStart = "sunday" ) +// +k8s:openapi-gen=true +type DashboardQueryVariableSpecStaticOptionsOrder string + +const ( + DashboardQueryVariableSpecStaticOptionsOrderBefore DashboardQueryVariableSpecStaticOptionsOrder = "before" + DashboardQueryVariableSpecStaticOptionsOrderAfter DashboardQueryVariableSpecStaticOptionsOrder = "after" + DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted" +) + // +k8s:openapi-gen=true type DashboardPanelKindOrLibraryPanelKind struct { PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"` diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 85b5a6f65cb..ba0a80e050b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -3327,6 +3327,25 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref common.Re Format: "", }, }, + "staticOptions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption"), + }, + }, + }, + }, + }, + "staticOptionsOrder": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"name", "current", "hide", "refresh", "skipUrlSync", "query", "regex", "sort", "options", "multi", "includeAll", "allowCustomValue"}, }, diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index d0755a7cd1f..95eb59de600 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -20,6 +20,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryGroupSpec,Queries API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryGroupSpec,Transformations API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableSpec,StaticOptions API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardRowsLayoutSpec,Rows API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardSpec,Annotations API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardSpec,Links diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/constants.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/constants.go new file mode 100644 index 00000000000..3942a399896 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/constants.go @@ -0,0 +1,18 @@ +package v2alpha2 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "dashboard.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v2alpha2" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_codec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_codec_gen.go new file mode 100644 index 00000000000..9c679dcafd7 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v2alpha2 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// DashboardJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type DashboardJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*DashboardJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*DashboardJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &DashboardJSONCodec{} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_metadata_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_metadata_gen.go new file mode 100644 index 00000000000..62e187af661 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v2alpha2 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type DashboardMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewDashboardMetadata creates a new DashboardMetadata object. +func NewDashboardMetadata() *DashboardMetadata { + return &DashboardMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_object_gen.go new file mode 100644 index 00000000000..641ba8ce808 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v2alpha2 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Dashboard struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Dashboard + Spec DashboardSpec `json:"spec" yaml:"spec"` + + Status DashboardStatus `json:"status" yaml:"status"` +} + +func (o *Dashboard) GetSpec() any { + return o.Spec +} + +func (o *Dashboard) SetSpec(spec any) error { + cast, ok := spec.(DashboardSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Dashboard) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Dashboard) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Dashboard) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(DashboardStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type DashboardStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Dashboard) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Dashboard) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Dashboard) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Dashboard) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Dashboard) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Dashboard) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Dashboard) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Dashboard) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Dashboard) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Dashboard) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Dashboard) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Dashboard) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Dashboard) DeepCopy() *Dashboard { + cpy := &Dashboard{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Dashboard) DeepCopyInto(dst *Dashboard) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Dashboard{} + +// +k8s:openapi-gen=true +type DashboardList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Dashboard `json:"items" yaml:"items"` +} + +func (o *DashboardList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *DashboardList) Copy() resource.ListObject { + cpy := &DashboardList{ + TypeMeta: o.TypeMeta, + Items: make([]Dashboard, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Dashboard); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *DashboardList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *DashboardList) SetItems(items []resource.Object) { + o.Items = make([]Dashboard, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Dashboard) + } +} + +func (o *DashboardList) DeepCopy() *DashboardList { + cpy := &DashboardList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *DashboardList) DeepCopyInto(dst *DashboardList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &DashboardList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *DashboardSpec) DeepCopy() *DashboardSpec { + cpy := &DashboardSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *DashboardSpec) DeepCopyInto(dst *DashboardSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of DashboardStatus +func (s *DashboardStatus) DeepCopy() *DashboardStatus { + cpy := &DashboardStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies DashboardStatus into another DashboardStatus object +func (s *DashboardStatus) DeepCopyInto(dst *DashboardStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_schema_gen.go new file mode 100644 index 00000000000..da9ae612d71 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v2alpha2 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaDashboard = resource.NewSimpleSchema("dashboard.grafana.app", "v2alpha2", &Dashboard{}, &DashboardList{}, resource.WithKind("Dashboard"), + resource.WithPlural("dashboards"), resource.WithScope(resource.NamespacedScope)) + kindDashboard = resource.Kind{ + Schema: schemaDashboard, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &DashboardJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func DashboardKind() resource.Kind { + return kindDashboard +} + +// Schema returns a resource.SimpleSchema representation of Dashboard +func DashboardSchema() *resource.SimpleSchema { + return schemaDashboard +} + +// Interface compliance checks +var _ resource.Schema = kindDashboard diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec.cue new file mode 100644 index 00000000000..4fdce729b2c --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec.cue @@ -0,0 +1,968 @@ +// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY +// Source: apps/dashboard/kinds/v2alpha2/dashboard_spec.cue +// To sync changes, run: make generate in apps/dashboard + +package v2alpha2 + +DashboardSpec: { + annotations: [...AnnotationQueryKind] | *[] + + // Configuration of dashboard cursor sync behavior. + // "Off" for no shared crosshair or tooltip (default). + // "Crosshair" for shared crosshair. + // "Tooltip" for shared crosshair AND shared tooltip. + cursorSync: DashboardCursorSync + + // Description of dashboard. + description?: string + + // Whether a dashboard is editable or not. + editable?: bool | *true + + elements: [ElementReference.name]: Element | *{} + + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + + // Links with references to other dashboards or external websites. + links: [...DashboardLink] | *[] + + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data. + liveNow?: bool + + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + preload: bool | *false + + // Plugins only. The version of the dashboard installed together with the plugin. + // This is used to determine if the dashboard should be updated when the plugin is updated. + revision?: uint16 + + // Tags associated with dashboard. + tags: [...string] | *[] + + timeSettings: TimeSettingsSpec + + // Title of dashboard. + title: string + + // Configured template variables. + variables: [...VariableKind] | *[] +} + +// Supported dashboard elements +Element: PanelKind | LibraryPanelKind // |* more element types in the future + +LibraryPanelKind: { + kind: "LibraryPanel" + spec: LibraryPanelKindSpec +} + +LibraryPanelKindSpec: { + // Panel ID for the library panel in the dashboard + id: number + // Title for the library panel in the dashboard + title: string + + libraryPanel: LibraryPanelRef +} + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +LibraryPanelRef: { + // Library panel name + name: string + // Library panel uid + uid: string +} + +AnnotationPanelFilter: { + // Should the specified panels be included or excluded + exclude?: bool | *false + + // Panel IDs that should be included or excluded + ids: [...uint32] +} + +// "Off" for no shared crosshair or tooltip (default). +// "Crosshair" for shared crosshair. +// "Tooltip" for shared crosshair AND shared tooltip. +DashboardCursorSync: "Crosshair" | "Tooltip" | *"Off" + +// Links with references to other dashboards or external resources +DashboardLink: { + // Title to display with the link + title: string + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` + type: DashboardLinkType + // Icon name to be displayed with the link + icon: string + // Tooltip to display when the user hovers their mouse over it + tooltip: string + // Link URL. Only required/valid if the type is link + url?: string + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: [...string] | *[] + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: bool | *false + // If true, the link will be opened in a new tab + targetBlank: bool | *false + // If true, includes current template variables values in the link as query params + includeVars: bool | *false + // If true, includes current time range in the link as query params + keepTime: bool | *false +} + +DataSourceRef: { + // The plugin type-id + type?: string + + // Specific datasource instance + uid?: string +} + +// A topic is attached to DataFrame metadata in query results. +// This specifies where the data should be used. +DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") + +// Transformations allow to manipulate data returned by a query before the system applies a visualization. +// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, +// use the output of one transformation as the input to another transformation, etc. +DataTransformerConfig: { + // Unique identifier of transformer + id: string + // Disabled transformations are skipped + disabled?: bool + // Optional frame matcher. When missing it will be applied to all results + filter?: MatcherConfig + // Where to pull DataFrames from as input to transformation + topic?: DataTopic + // Options to be passed to the transformer + // Valid options depend on the transformer id + options: _ +} + +DataLink: { + title: string + url: string + targetBlank?: bool +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +FieldConfigSource: { + // Defaults are the options applied to all fields. + defaults: FieldConfig + // Overrides are the options applied to specific fields overriding the defaults. + overrides: [...{ + matcher: MatcherConfig + properties: [...DynamicConfigValue] + }] +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +FieldConfig: { + // The display value for this field. This supports template variables blank is auto + displayName?: string + + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string + + // Human readable field metadata + description?: string + + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string + + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool + + // True if data source field supports ad-hoc filters + filterable?: bool + + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + unit?: string + + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + decimals?: number + + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + min?: number + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + max?: number + + // Convert input values into a display string + mappings?: [...ValueMapping] + + // Map numeric values to states + thresholds?: ThresholdsConfig + + // Panel color configuration + color?: FieldColor + + // The behavior when clicking on a result + links?: [...] + + // Alternative to empty string + noValue?: string + + // custom is specified by the FieldConfig field + // in panel plugin schemas. + custom?: {...} +} + +DynamicConfigValue: { + id: string | *"" + value?: _ +} + +// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. +// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. +MatcherConfig: { + // The matcher id. This is used to find the matcher implementation from registry. + id: string | *"" + // The matcher options. This is specific to the matcher implementation. + options?: _ +} + +Threshold: { + value: number + color: string +} + +ThresholdsMode: "absolute" | "percentage" + +ThresholdsConfig: { + mode: ThresholdsMode + steps: [...Threshold] +} + +ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap + +// Supported value mapping types +// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. +MappingType: "value" | "range" | "regex" | "special" + +// Maps text values to a color or different display text and color. +// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +ValueMap: { + type: MappingType & "value" + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + options: [string]: ValueMappingResult +} + +// Maps numerical ranges to a display text and color. +// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +RangeMap: { + type: MappingType & "range" + // Range to match against and the result to apply when the value is within the range + options: { + // Min value of the range. It can be null which means -Infinity + from: float64 | null + // Max value of the range. It can be null which means +Infinity + to: float64 | null + // Config to apply when the value is within the range + result: ValueMappingResult + } +} + +// Maps regular expressions to replacement text and a color. +// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +RegexMap: { + type: MappingType & "regex" + // Regular expression to match against and the result to apply when the value matches the regex + options: { + // Regular expression to match against + pattern: string + // Config to apply when the value matches the regex + result: ValueMappingResult + } +} + +// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. +// See SpecialValueMatch to see the list of special values. +// For example, you can configure a special value mapping so that null values appear as N/A. +SpecialValueMap: { + type: MappingType & "special" + options: { + // Special value to match against + match: SpecialValueMatch + // Config to apply when the value matches the special value + result: ValueMappingResult + } +} + +// Special value types supported by the `SpecialValueMap` +SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cog(kind="enum",memberNames="True|False|Null|NaN|NullAndNaN|Empty") + +// Result used as replacement with text and color when the value matches +ValueMappingResult: { + // Text to display when the value matches + text?: string + // Text to use when the value matches + color?: string + // Icon to display when the value matches. Only specific visualizations. + icon?: string + // Position in the mapping array. Only used internally. + index?: int32 +} + +// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. +// Continuous color interpolates a color using the percentage of a value relative to min and max. +// Accepted values are: +// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold +// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations +// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations +// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode +// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode +// `continuous-YlRd`: Continuous Yellow-Red palette mode +// `continuous-BlPu`: Continuous Blue-Purple palette mode +// `continuous-YlBl`: Continuous Yellow-Blue palette mode +// `continuous-blues`: Continuous Blue palette mode +// `continuous-reds`: Continuous Red palette mode +// `continuous-greens`: Continuous Green palette mode +// `continuous-purples`: Continuous Purple palette mode +// `shades`: Shades of a single color. Specify a single color, useful in an override rule. +// `fixed`: Fixed color mode. Specify a single color, useful in an override rule. +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" + +// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. +FieldColorSeriesByMode: "min" | "max" | "last" + +// Map a field to a color. +FieldColor: { + // The main color scheme mode. + mode: FieldColorModeId + // The fixed color value for fixed or shades color modes. + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: FieldColorSeriesByMode +} + +// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) +DashboardLinkType: "link" | "dashboards" + +// --- Common types --- +Kind: { + kind: string + spec: _ + metadata?: _ +} + +// --- Kinds --- +VizConfigSpec: { + pluginVersion: string + options: [string]: _ + fieldConfig: FieldConfigSource +} + +VizConfigKind: { + // The kind of a VizConfigKind is the plugin ID + kind: string + spec: VizConfigSpec +} + +AnnotationQuerySpec: { + datasource?: DataSourceRef + query?: DataQueryKind + enable: bool + hide: bool + iconColor: string + name: string + builtIn?: bool | *false + filter?: AnnotationPanelFilter + legacyOptions?: [string]: _ //Catch-all field for datasource-specific properties +} + +AnnotationQueryKind: { + kind: "AnnotationQuery" + spec: AnnotationQuerySpec +} + +QueryOptionsSpec: { + timeFrom?: string + maxDataPoints?: int + timeShift?: string + queryCachingTTL?: int + interval?: string + cacheTimeout?: string + hideTimeOverride?: bool +} + +DataQueryKind: { + // The kind of a DataQueryKind is the datasource type + kind: string + spec: [string]: _ +} + +PanelQuerySpec: { + query: DataQueryKind + datasource?: DataSourceRef + + refId: string + hidden: bool +} + +PanelQueryKind: { + kind: "PanelQuery" + spec: PanelQuerySpec +} + +TransformationKind: { + // The kind of a TransformationKind is the transformation ID + kind: string + spec: DataTransformerConfig +} + +QueryGroupSpec: { + queries: [...PanelQueryKind] + transformations: [...TransformationKind] + queryOptions: QueryOptionsSpec +} + +QueryGroupKind: { + kind: "QueryGroup" + spec: QueryGroupSpec +} + +TimeRangeOption: { + display: string | *"Last 6 hours" + from: string | *"now-6h" + to: string | *"now" +} + +// Time configuration +// It defines the default time config for the time picker, the refresh picker for the specific dashboard. +TimeSettingsSpec: { + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"browser" + // Start time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + from: string | *"now-6h" + // End time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + to: string | *"now" + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + autoRefresh: string | *"" // v1: refresh + // Interval options available in the refresh picker dropdown. + autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals + // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. + quickRanges?: [...TimeRangeOption] // v1: timepicker.quick_ranges , not exposed in the UI + // Whether timepicker is visible or not. + hideTimepicker: bool | *false // v1: timepicker.hidden + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: "saturday" | "monday" | "sunday" + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth: int | *0 + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + nowDelay?: string // v1: timepicker.nowDelay +} + +RepeatMode: "variable" // other repeat modes will be added in the future: label, frame + +RepeatOptions: { + mode: RepeatMode + value: string + direction?: "h" | "v" + maxPerRow?: int +} + +RowRepeatOptions: { + mode: RepeatMode + value: string +} + +TabRepeatOptions: { + mode: RepeatMode + value: string +} + +AutoGridRepeatOptions: { + mode: RepeatMode + value: string +} + +GridLayoutItemSpec: { + x: int + y: int + width: int + height: int + element: ElementReference // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference + repeat?: RepeatOptions +} + +GridLayoutItemKind: { + kind: "GridLayoutItem" + spec: GridLayoutItemSpec +} + +GridLayoutSpec: { + items: [...GridLayoutItemKind] +} + +GridLayoutKind: { + kind: "GridLayout" + spec: GridLayoutSpec +} + +RowsLayoutKind: { + kind: "RowsLayout" + spec: RowsLayoutSpec +} + +RowsLayoutSpec: { + rows: [...RowsLayoutRowKind] +} + +RowsLayoutRowKind: { + kind: "RowsLayoutRow" + spec: RowsLayoutRowSpec +} + +RowsLayoutRowSpec: { + title?: string + collapse?: bool + hideHeader?: bool + fillScreen?: bool + conditionalRendering?: ConditionalRenderingGroupKind + repeat?: RowRepeatOptions + layout: GridLayoutKind | AutoGridLayoutKind | TabsLayoutKind | RowsLayoutKind +} + +AutoGridLayoutKind: { + kind: "AutoGridLayout" + spec: AutoGridLayoutSpec +} + +AutoGridLayoutSpec: { + maxColumnCount?: number | *3 + columnWidthMode: "narrow" | *"standard" | "wide" | "custom" + columnWidth?: number + rowHeightMode: "short" | *"standard" | "tall" | "custom" + rowHeight?: number + fillScreen?: bool | *false + items: [...AutoGridLayoutItemKind] +} + +AutoGridLayoutItemKind: { + kind: "AutoGridLayoutItem" + spec: AutoGridLayoutItemSpec +} + +AutoGridLayoutItemSpec: { + element: ElementReference + repeat?: AutoGridRepeatOptions + conditionalRendering?: ConditionalRenderingGroupKind +} + +TabsLayoutKind: { + kind: "TabsLayout" + spec: TabsLayoutSpec +} + +TabsLayoutSpec: { + tabs: [...TabsLayoutTabKind] +} + +TabsLayoutTabKind: { + kind: "TabsLayoutTab" + spec: TabsLayoutTabSpec +} + +TabsLayoutTabSpec: { + title?: string + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + conditionalRendering?: ConditionalRenderingGroupKind + repeat?: TabRepeatOptions +} + +PanelSpec: { + id: number + title: string + description: string + links: [...DataLink] + data: QueryGroupKind + vizConfig: VizConfigKind + transparent?: bool +} + +PanelKind: { + kind: "Panel" + spec: PanelSpec +} + +ElementReference: { + kind: "ElementReference" + name: string +} + +// Start FIXME: variables - in CUE PR - this are things that should be added into the cue schema +// TODO: properties such as `hide`, `skipUrlSync`, `multi` are type boolean, and in the old schema they are conditional, +// should we make them conditional in the new schema as well? or should we make them required but default to false? + +// Variable types +VariableValue: VariableValueSingle | [...VariableValueSingle] + +VariableValueSingle: string | bool | number | CustomVariableValue + +// Custom formatter variable +CustomFormatterVariable: { + name: string + type: VariableType + multi: bool + includeAll: bool +} + +// Custom variable value +CustomVariableValue: { + // The format name or function used in the expression + formatter: *null | string | VariableCustomFormatterFn +} + +// Custom formatter function +VariableCustomFormatterFn: { + value: _ + legacyVariableModel: { + name: string + type: VariableType + multi: bool + includeAll: bool + } + legacyDefaultFormatter?: VariableCustomFormatterFn +} + +// Dashboard variable type +// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. +// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). +// `constant`: Define a hidden constant. +// `datasource`: Quickly change the data source for an entire dashboard. +// `interval`: Interval variables represent time spans. +// `textbox`: Display a free text input field with an optional default value. +// `custom`: Define the variable options manually using a comma-separated list. +// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables +VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | + "system" | "snapshot" + +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind + +// Sort variable options +// Accepted values are: +// `disabled`: No sorting +// `alphabeticalAsc`: Alphabetical ASC +// `alphabeticalDesc`: Alphabetical DESC +// `numericalAsc`: Numerical ASC +// `numericalDesc`: Numerical DESC +// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC +// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC +// `naturalAsc`: Natural ASC +// `naturalDesc`: Natural DESC +// VariableSort enum with default value +VariableSort: "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc" + +// Options to config when to refresh a variable +// `never`: Never refresh the variable +// `onDashboardLoad`: Queries the data source every time the dashboard loads. +// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. +VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" + +// Determine if the variable shows on dashboard +// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). +VariableHide: *"dontHide" | "hideLabel" | "hideVariable" + +// Determine the origin of the adhoc variable filter +FilterOrigin: "dashboard" + +// FIXME: should we introduce this? --- Variable value option +VariableValueOption: { + label: string + value: VariableValueSingle + group?: string +} + +// Variable option specification +VariableOption: { + // Whether the option is selected or not + selected?: bool + // Text to be displayed for the option + text: string | [...string] + // Value of the option + value: string | [...string] +} + +// Query variable specification +QueryVariableSpec: { + name: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + label?: string + hide: VariableHide + refresh: VariableRefresh + skipUrlSync: bool | *false + description?: string + datasource?: DataSourceRef + query: DataQueryKind + regex: string | *"" + sort: VariableSort + definition?: string + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + placeholder?: string + allowCustomValue: bool | *true + staticOptions?: [...VariableOption] + staticOptionsOrder?: "before" | "after" | "sorted" +} + +// Query variable kind +QueryVariableKind: { + kind: "QueryVariable" + spec: QueryVariableSpec +} + +// Text variable specification +TextVariableSpec: { + name: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + query: string | *"" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Text variable kind +TextVariableKind: { + kind: "TextVariable" + spec: TextVariableSpec +} + +// Constant variable specification +ConstantVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Constant variable kind +ConstantVariableKind: { + kind: "ConstantVariable" + spec: ConstantVariableSpec +} + +// Datasource variable specification +DatasourceVariableSpec: { + name: string | *"" + pluginId: string | *"" + refresh: VariableRefresh + regex: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string + allowCustomValue: bool | *true +} + +// Datasource variable kind +DatasourceVariableKind: { + kind: "DatasourceVariable" + spec: DatasourceVariableSpec +} + +// Interval variable specification +IntervalVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + auto: bool | *false + auto_min: string | *"" + auto_count: int | *0 + refresh: VariableRefresh + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Interval variable kind +IntervalVariableKind: { + kind: "IntervalVariable" + spec: IntervalVariableSpec +} + +// Custom variable specification +CustomVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string + allowCustomValue: bool | *true +} + +// Custom variable kind +CustomVariableKind: { + kind: "CustomVariable" + spec: CustomVariableSpec +} + +// GroupBy variable specification +GroupByVariableSpec: { + name: string | *"" + datasource?: DataSourceRef + defaultValue?: VariableOption + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + multi: bool | *false + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Group variable kind +GroupByVariableKind: { + kind: "GroupByVariable" + spec: GroupByVariableSpec +} + +// Adhoc variable specification +AdhocVariableSpec: { + name: string | *"" + datasource?: DataSourceRef + baseFilters: [...AdHocFilterWithLabels] | *[] + filters: [...AdHocFilterWithLabels] | *[] + defaultKeys: [...MetricFindValue] | *[] + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string + allowCustomValue: bool | *true +} + +// Define the MetricFindValue type +MetricFindValue: { + text: string + value?: string | number + group?: string + expandable?: bool +} + +// Define the AdHocFilterWithLabels type +AdHocFilterWithLabels: { + key: string + operator: string + value: string + values?: [...string] + keyLabel?: string + valueLabels?: [...string] + forceEdit?: bool + origin?: FilterOrigin + // @deprecated + condition?: string +} + +// Adhoc variable kind +AdhocVariableKind: { + kind: "AdhocVariable" + spec: AdhocVariableSpec +} + +ConditionalRenderingGroupKind: { + kind: "ConditionalRenderingGroup" + spec: ConditionalRenderingGroupSpec +} + +ConditionalRenderingGroupSpec: { + visibility: "show" | "hide" + condition: "and" | "or" + items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind] +} + +ConditionalRenderingVariableKind: { + kind: "ConditionalRenderingVariable" + spec: ConditionalRenderingVariableSpec +} + +ConditionalRenderingVariableSpec: { + variable: string + operator: "equals" | "notEquals" + value: string +} + +ConditionalRenderingDataKind: { + kind: "ConditionalRenderingData" + spec: ConditionalRenderingDataSpec +} + +ConditionalRenderingDataSpec: { + value: bool +} + +ConditionalRenderingTimeRangeSizeKind: { + kind: "ConditionalRenderingTimeRangeSize" + spec: ConditionalRenderingTimeRangeSizeSpec +} + +ConditionalRenderingTimeRangeSizeSpec: { + value: string +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec_gen.go new file mode 100644 index 00000000000..32b90053977 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_spec_gen.go @@ -0,0 +1,2540 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v2alpha2 + +import ( + json "encoding/json" + errors "errors" +) + +// +k8s:openapi-gen=true +type DashboardAnnotationQueryKind struct { + Kind string `json:"kind"` + Spec DashboardAnnotationQuerySpec `json:"spec"` +} + +// NewDashboardAnnotationQueryKind creates a new DashboardAnnotationQueryKind object. +func NewDashboardAnnotationQueryKind() *DashboardAnnotationQueryKind { + return &DashboardAnnotationQueryKind{ + Kind: "AnnotationQuery", + Spec: *NewDashboardAnnotationQuerySpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardAnnotationQuerySpec struct { + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + Query *DashboardDataQueryKind `json:"query,omitempty"` + Enable bool `json:"enable"` + Hide bool `json:"hide"` + IconColor string `json:"iconColor"` + Name string `json:"name"` + BuiltIn *bool `json:"builtIn,omitempty"` + Filter *DashboardAnnotationPanelFilter `json:"filter,omitempty"` + // Catch-all field for datasource-specific properties + LegacyOptions map[string]interface{} `json:"legacyOptions,omitempty"` +} + +// NewDashboardAnnotationQuerySpec creates a new DashboardAnnotationQuerySpec object. +func NewDashboardAnnotationQuerySpec() *DashboardAnnotationQuerySpec { + return &DashboardAnnotationQuerySpec{ + BuiltIn: (func(input bool) *bool { return &input })(false), + } +} + +// +k8s:openapi-gen=true +type DashboardDataSourceRef struct { + // The plugin type-id + Type *string `json:"type,omitempty"` + // Specific datasource instance + Uid *string `json:"uid,omitempty"` +} + +// NewDashboardDataSourceRef creates a new DashboardDataSourceRef object. +func NewDashboardDataSourceRef() *DashboardDataSourceRef { + return &DashboardDataSourceRef{} +} + +// +k8s:openapi-gen=true +type DashboardDataQueryKind struct { + // The kind of a DataQueryKind is the datasource type + Kind string `json:"kind"` + Spec map[string]interface{} `json:"spec"` +} + +// NewDashboardDataQueryKind creates a new DashboardDataQueryKind object. +func NewDashboardDataQueryKind() *DashboardDataQueryKind { + return &DashboardDataQueryKind{ + Spec: map[string]interface{}{}, + } +} + +// +k8s:openapi-gen=true +type DashboardAnnotationPanelFilter struct { + // Should the specified panels be included or excluded + Exclude *bool `json:"exclude,omitempty"` + // Panel IDs that should be included or excluded + Ids []uint32 `json:"ids"` +} + +// NewDashboardAnnotationPanelFilter creates a new DashboardAnnotationPanelFilter object. +func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter { + return &DashboardAnnotationPanelFilter{ + Exclude: (func(input bool) *bool { return &input })(false), + Ids: []uint32{}, + } +} + +// "Off" for no shared crosshair or tooltip (default). +// "Crosshair" for shared crosshair. +// "Tooltip" for shared crosshair AND shared tooltip. +// +k8s:openapi-gen=true +type DashboardDashboardCursorSync string + +const ( + DashboardDashboardCursorSyncCrosshair DashboardDashboardCursorSync = "Crosshair" + DashboardDashboardCursorSyncTooltip DashboardDashboardCursorSync = "Tooltip" + DashboardDashboardCursorSyncOff DashboardDashboardCursorSync = "Off" +) + +// Supported dashboard elements +// |* more element types in the future +// +k8s:openapi-gen=true +type DashboardElement = DashboardPanelKindOrLibraryPanelKind + +// NewDashboardElement creates a new DashboardElement object. +func NewDashboardElement() *DashboardElement { + return NewDashboardPanelKindOrLibraryPanelKind() +} + +// +k8s:openapi-gen=true +type DashboardPanelKind struct { + Kind string `json:"kind"` + Spec DashboardPanelSpec `json:"spec"` +} + +// NewDashboardPanelKind creates a new DashboardPanelKind object. +func NewDashboardPanelKind() *DashboardPanelKind { + return &DashboardPanelKind{ + Kind: "Panel", + Spec: *NewDashboardPanelSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardPanelSpec struct { + Id float64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Links []DashboardDataLink `json:"links"` + Data DashboardQueryGroupKind `json:"data"` + VizConfig DashboardVizConfigKind `json:"vizConfig"` + Transparent *bool `json:"transparent,omitempty"` +} + +// NewDashboardPanelSpec creates a new DashboardPanelSpec object. +func NewDashboardPanelSpec() *DashboardPanelSpec { + return &DashboardPanelSpec{ + Links: []DashboardDataLink{}, + Data: *NewDashboardQueryGroupKind(), + VizConfig: *NewDashboardVizConfigKind(), + } +} + +// +k8s:openapi-gen=true +type DashboardDataLink struct { + Title string `json:"title"` + Url string `json:"url"` + TargetBlank *bool `json:"targetBlank,omitempty"` +} + +// NewDashboardDataLink creates a new DashboardDataLink object. +func NewDashboardDataLink() *DashboardDataLink { + return &DashboardDataLink{} +} + +// +k8s:openapi-gen=true +type DashboardQueryGroupKind struct { + Kind string `json:"kind"` + Spec DashboardQueryGroupSpec `json:"spec"` +} + +// NewDashboardQueryGroupKind creates a new DashboardQueryGroupKind object. +func NewDashboardQueryGroupKind() *DashboardQueryGroupKind { + return &DashboardQueryGroupKind{ + Kind: "QueryGroup", + Spec: *NewDashboardQueryGroupSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardQueryGroupSpec struct { + Queries []DashboardPanelQueryKind `json:"queries"` + Transformations []DashboardTransformationKind `json:"transformations"` + QueryOptions DashboardQueryOptionsSpec `json:"queryOptions"` +} + +// NewDashboardQueryGroupSpec creates a new DashboardQueryGroupSpec object. +func NewDashboardQueryGroupSpec() *DashboardQueryGroupSpec { + return &DashboardQueryGroupSpec{ + Queries: []DashboardPanelQueryKind{}, + Transformations: []DashboardTransformationKind{}, + QueryOptions: *NewDashboardQueryOptionsSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardPanelQueryKind struct { + Kind string `json:"kind"` + Spec DashboardPanelQuerySpec `json:"spec"` +} + +// NewDashboardPanelQueryKind creates a new DashboardPanelQueryKind object. +func NewDashboardPanelQueryKind() *DashboardPanelQueryKind { + return &DashboardPanelQueryKind{ + Kind: "PanelQuery", + Spec: *NewDashboardPanelQuerySpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardPanelQuerySpec struct { + Query DashboardDataQueryKind `json:"query"` + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + RefId string `json:"refId"` + Hidden bool `json:"hidden"` +} + +// NewDashboardPanelQuerySpec creates a new DashboardPanelQuerySpec object. +func NewDashboardPanelQuerySpec() *DashboardPanelQuerySpec { + return &DashboardPanelQuerySpec{ + Query: *NewDashboardDataQueryKind(), + } +} + +// +k8s:openapi-gen=true +type DashboardTransformationKind struct { + // The kind of a TransformationKind is the transformation ID + Kind string `json:"kind"` + Spec DashboardDataTransformerConfig `json:"spec"` +} + +// NewDashboardTransformationKind creates a new DashboardTransformationKind object. +func NewDashboardTransformationKind() *DashboardTransformationKind { + return &DashboardTransformationKind{ + Spec: *NewDashboardDataTransformerConfig(), + } +} + +// Transformations allow to manipulate data returned by a query before the system applies a visualization. +// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, +// use the output of one transformation as the input to another transformation, etc. +// +k8s:openapi-gen=true +type DashboardDataTransformerConfig struct { + // Unique identifier of transformer + Id string `json:"id"` + // Disabled transformations are skipped + Disabled *bool `json:"disabled,omitempty"` + // Optional frame matcher. When missing it will be applied to all results + Filter *DashboardMatcherConfig `json:"filter,omitempty"` + // Where to pull DataFrames from as input to transformation + Topic *DashboardDataTopic `json:"topic,omitempty"` + // Options to be passed to the transformer + // Valid options depend on the transformer id + Options interface{} `json:"options"` +} + +// NewDashboardDataTransformerConfig creates a new DashboardDataTransformerConfig object. +func NewDashboardDataTransformerConfig() *DashboardDataTransformerConfig { + return &DashboardDataTransformerConfig{} +} + +// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. +// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. +// +k8s:openapi-gen=true +type DashboardMatcherConfig struct { + // The matcher id. This is used to find the matcher implementation from registry. + Id string `json:"id"` + // The matcher options. This is specific to the matcher implementation. + Options interface{} `json:"options,omitempty"` +} + +// NewDashboardMatcherConfig creates a new DashboardMatcherConfig object. +func NewDashboardMatcherConfig() *DashboardMatcherConfig { + return &DashboardMatcherConfig{ + Id: "", + } +} + +// A topic is attached to DataFrame metadata in query results. +// This specifies where the data should be used. +// +k8s:openapi-gen=true +type DashboardDataTopic string + +const ( + DashboardDataTopicSeries DashboardDataTopic = "series" + DashboardDataTopicAnnotations DashboardDataTopic = "annotations" + DashboardDataTopicAlertStates DashboardDataTopic = "alertStates" +) + +// +k8s:openapi-gen=true +type DashboardQueryOptionsSpec struct { + TimeFrom *string `json:"timeFrom,omitempty"` + MaxDataPoints *int64 `json:"maxDataPoints,omitempty"` + TimeShift *string `json:"timeShift,omitempty"` + QueryCachingTTL *int64 `json:"queryCachingTTL,omitempty"` + Interval *string `json:"interval,omitempty"` + CacheTimeout *string `json:"cacheTimeout,omitempty"` + HideTimeOverride *bool `json:"hideTimeOverride,omitempty"` +} + +// NewDashboardQueryOptionsSpec creates a new DashboardQueryOptionsSpec object. +func NewDashboardQueryOptionsSpec() *DashboardQueryOptionsSpec { + return &DashboardQueryOptionsSpec{} +} + +// +k8s:openapi-gen=true +type DashboardVizConfigKind struct { + // The kind of a VizConfigKind is the plugin ID + Kind string `json:"kind"` + Spec DashboardVizConfigSpec `json:"spec"` +} + +// NewDashboardVizConfigKind creates a new DashboardVizConfigKind object. +func NewDashboardVizConfigKind() *DashboardVizConfigKind { + return &DashboardVizConfigKind{ + Spec: *NewDashboardVizConfigSpec(), + } +} + +// --- Kinds --- +// +k8s:openapi-gen=true +type DashboardVizConfigSpec struct { + PluginVersion string `json:"pluginVersion"` + Options map[string]interface{} `json:"options"` + FieldConfig DashboardFieldConfigSource `json:"fieldConfig"` +} + +// NewDashboardVizConfigSpec creates a new DashboardVizConfigSpec object. +func NewDashboardVizConfigSpec() *DashboardVizConfigSpec { + return &DashboardVizConfigSpec{ + Options: map[string]interface{}{}, + FieldConfig: *NewDashboardFieldConfigSource(), + } +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +// +k8s:openapi-gen=true +type DashboardFieldConfigSource struct { + // Defaults are the options applied to all fields. + Defaults DashboardFieldConfig `json:"defaults"` + // Overrides are the options applied to specific fields overriding the defaults. + Overrides []DashboardV2alpha2FieldConfigSourceOverrides `json:"overrides"` +} + +// NewDashboardFieldConfigSource creates a new DashboardFieldConfigSource object. +func NewDashboardFieldConfigSource() *DashboardFieldConfigSource { + return &DashboardFieldConfigSource{ + Defaults: *NewDashboardFieldConfig(), + Overrides: []DashboardV2alpha2FieldConfigSourceOverrides{}, + } +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +// +k8s:openapi-gen=true +type DashboardFieldConfig struct { + // The display value for this field. This supports template variables blank is auto + DisplayName *string `json:"displayName,omitempty"` + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + DisplayNameFromDS *string `json:"displayNameFromDS,omitempty"` + // Human readable field metadata + Description *string `json:"description,omitempty"` + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + Path *string `json:"path,omitempty"` + // True if data source can write a value to the path. Auth/authz are supported separately + Writeable *bool `json:"writeable,omitempty"` + // True if data source field supports ad-hoc filters + Filterable *bool `json:"filterable,omitempty"` + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + Unit *string `json:"unit,omitempty"` + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + Decimals *float64 `json:"decimals,omitempty"` + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + Min *float64 `json:"min,omitempty"` + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + Max *float64 `json:"max,omitempty"` + // Convert input values into a display string + Mappings []DashboardValueMapping `json:"mappings,omitempty"` + // Map numeric values to states + Thresholds *DashboardThresholdsConfig `json:"thresholds,omitempty"` + // Panel color configuration + Color *DashboardFieldColor `json:"color,omitempty"` + // The behavior when clicking on a result + Links []interface{} `json:"links,omitempty"` + // Alternative to empty string + NoValue *string `json:"noValue,omitempty"` + // custom is specified by the FieldConfig field + // in panel plugin schemas. + Custom map[string]interface{} `json:"custom,omitempty"` +} + +// NewDashboardFieldConfig creates a new DashboardFieldConfig object. +func NewDashboardFieldConfig() *DashboardFieldConfig { + return &DashboardFieldConfig{} +} + +// +k8s:openapi-gen=true +type DashboardValueMapping = DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap + +// NewDashboardValueMapping creates a new DashboardValueMapping object. +func NewDashboardValueMapping() *DashboardValueMapping { + return NewDashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap() +} + +// Maps text values to a color or different display text and color. +// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// +k8s:openapi-gen=true +type DashboardValueMap struct { + Type DashboardMappingType `json:"type"` + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + Options map[string]DashboardValueMappingResult `json:"options"` +} + +// NewDashboardValueMap creates a new DashboardValueMap object. +func NewDashboardValueMap() *DashboardValueMap { + return &DashboardValueMap{ + Type: DashboardMappingTypeValue, + Options: map[string]DashboardValueMappingResult{}, + } +} + +// Supported value mapping types +// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. +// +k8s:openapi-gen=true +type DashboardMappingType string + +const ( + DashboardMappingTypeValue DashboardMappingType = "value" + DashboardMappingTypeRange DashboardMappingType = "range" + DashboardMappingTypeRegex DashboardMappingType = "regex" + DashboardMappingTypeSpecial DashboardMappingType = "special" +) + +// Result used as replacement with text and color when the value matches +// +k8s:openapi-gen=true +type DashboardValueMappingResult struct { + // Text to display when the value matches + Text *string `json:"text,omitempty"` + // Text to use when the value matches + Color *string `json:"color,omitempty"` + // Icon to display when the value matches. Only specific visualizations. + Icon *string `json:"icon,omitempty"` + // Position in the mapping array. Only used internally. + Index *int32 `json:"index,omitempty"` +} + +// NewDashboardValueMappingResult creates a new DashboardValueMappingResult object. +func NewDashboardValueMappingResult() *DashboardValueMappingResult { + return &DashboardValueMappingResult{} +} + +// Maps numerical ranges to a display text and color. +// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// +k8s:openapi-gen=true +type DashboardRangeMap struct { + Type DashboardMappingType `json:"type"` + // Range to match against and the result to apply when the value is within the range + Options DashboardV2alpha2RangeMapOptions `json:"options"` +} + +// NewDashboardRangeMap creates a new DashboardRangeMap object. +func NewDashboardRangeMap() *DashboardRangeMap { + return &DashboardRangeMap{ + Type: DashboardMappingTypeRange, + Options: *NewDashboardV2alpha2RangeMapOptions(), + } +} + +// Maps regular expressions to replacement text and a color. +// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// +k8s:openapi-gen=true +type DashboardRegexMap struct { + Type DashboardMappingType `json:"type"` + // Regular expression to match against and the result to apply when the value matches the regex + Options DashboardV2alpha2RegexMapOptions `json:"options"` +} + +// NewDashboardRegexMap creates a new DashboardRegexMap object. +func NewDashboardRegexMap() *DashboardRegexMap { + return &DashboardRegexMap{ + Type: DashboardMappingTypeRegex, + Options: *NewDashboardV2alpha2RegexMapOptions(), + } +} + +// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. +// See SpecialValueMatch to see the list of special values. +// For example, you can configure a special value mapping so that null values appear as N/A. +// +k8s:openapi-gen=true +type DashboardSpecialValueMap struct { + Type DashboardMappingType `json:"type"` + Options DashboardV2alpha2SpecialValueMapOptions `json:"options"` +} + +// NewDashboardSpecialValueMap creates a new DashboardSpecialValueMap object. +func NewDashboardSpecialValueMap() *DashboardSpecialValueMap { + return &DashboardSpecialValueMap{ + Type: DashboardMappingTypeSpecial, + Options: *NewDashboardV2alpha2SpecialValueMapOptions(), + } +} + +// Special value types supported by the `SpecialValueMap` +// +k8s:openapi-gen=true +type DashboardSpecialValueMatch string + +const ( + DashboardSpecialValueMatchTrue DashboardSpecialValueMatch = "true" + DashboardSpecialValueMatchFalse DashboardSpecialValueMatch = "false" + DashboardSpecialValueMatchNull DashboardSpecialValueMatch = "null" + DashboardSpecialValueMatchNaN DashboardSpecialValueMatch = "nan" + DashboardSpecialValueMatchNullAndNaN DashboardSpecialValueMatch = "null+nan" + DashboardSpecialValueMatchEmpty DashboardSpecialValueMatch = "empty" +) + +// +k8s:openapi-gen=true +type DashboardThresholdsConfig struct { + Mode DashboardThresholdsMode `json:"mode"` + Steps []DashboardThreshold `json:"steps"` +} + +// NewDashboardThresholdsConfig creates a new DashboardThresholdsConfig object. +func NewDashboardThresholdsConfig() *DashboardThresholdsConfig { + return &DashboardThresholdsConfig{ + Steps: []DashboardThreshold{}, + } +} + +// +k8s:openapi-gen=true +type DashboardThresholdsMode string + +const ( + DashboardThresholdsModeAbsolute DashboardThresholdsMode = "absolute" + DashboardThresholdsModePercentage DashboardThresholdsMode = "percentage" +) + +// +k8s:openapi-gen=true +type DashboardThreshold struct { + Value float64 `json:"value"` + Color string `json:"color"` +} + +// NewDashboardThreshold creates a new DashboardThreshold object. +func NewDashboardThreshold() *DashboardThreshold { + return &DashboardThreshold{} +} + +// Map a field to a color. +// +k8s:openapi-gen=true +type DashboardFieldColor struct { + // The main color scheme mode. + Mode DashboardFieldColorModeId `json:"mode"` + // The fixed color value for fixed or shades color modes. + FixedColor *string `json:"fixedColor,omitempty"` + // Some visualizations need to know how to assign a series color from by value color schemes. + SeriesBy *DashboardFieldColorSeriesByMode `json:"seriesBy,omitempty"` +} + +// NewDashboardFieldColor creates a new DashboardFieldColor object. +func NewDashboardFieldColor() *DashboardFieldColor { + return &DashboardFieldColor{} +} + +// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. +// Continuous color interpolates a color using the percentage of a value relative to min and max. +// Accepted values are: +// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold +// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations +// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations +// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode +// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode +// `continuous-YlRd`: Continuous Yellow-Red palette mode +// `continuous-BlPu`: Continuous Blue-Purple palette mode +// `continuous-YlBl`: Continuous Yellow-Blue palette mode +// `continuous-blues`: Continuous Blue palette mode +// `continuous-reds`: Continuous Red palette mode +// `continuous-greens`: Continuous Green palette mode +// `continuous-purples`: Continuous Purple palette mode +// `shades`: Shades of a single color. Specify a single color, useful in an override rule. +// `fixed`: Fixed color mode. Specify a single color, useful in an override rule. +// +k8s:openapi-gen=true +type DashboardFieldColorModeId string + +const ( + DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds" + DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic" + DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name" + DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd" + DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr" + DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd" + DashboardFieldColorModeIdContinuousYlRd DashboardFieldColorModeId = "continuous-YlRd" + DashboardFieldColorModeIdContinuousBlPu DashboardFieldColorModeId = "continuous-BlPu" + DashboardFieldColorModeIdContinuousYlBl DashboardFieldColorModeId = "continuous-YlBl" + DashboardFieldColorModeIdContinuousBlues DashboardFieldColorModeId = "continuous-blues" + DashboardFieldColorModeIdContinuousReds DashboardFieldColorModeId = "continuous-reds" + DashboardFieldColorModeIdContinuousGreens DashboardFieldColorModeId = "continuous-greens" + DashboardFieldColorModeIdContinuousPurples DashboardFieldColorModeId = "continuous-purples" + DashboardFieldColorModeIdFixed DashboardFieldColorModeId = "fixed" + DashboardFieldColorModeIdShades DashboardFieldColorModeId = "shades" +) + +// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. +// +k8s:openapi-gen=true +type DashboardFieldColorSeriesByMode string + +const ( + DashboardFieldColorSeriesByModeMin DashboardFieldColorSeriesByMode = "min" + DashboardFieldColorSeriesByModeMax DashboardFieldColorSeriesByMode = "max" + DashboardFieldColorSeriesByModeLast DashboardFieldColorSeriesByMode = "last" +) + +// +k8s:openapi-gen=true +type DashboardDynamicConfigValue struct { + Id string `json:"id"` + Value interface{} `json:"value,omitempty"` +} + +// NewDashboardDynamicConfigValue creates a new DashboardDynamicConfigValue object. +func NewDashboardDynamicConfigValue() *DashboardDynamicConfigValue { + return &DashboardDynamicConfigValue{ + Id: "", + } +} + +// +k8s:openapi-gen=true +type DashboardLibraryPanelKind struct { + Kind string `json:"kind"` + Spec DashboardLibraryPanelKindSpec `json:"spec"` +} + +// NewDashboardLibraryPanelKind creates a new DashboardLibraryPanelKind object. +func NewDashboardLibraryPanelKind() *DashboardLibraryPanelKind { + return &DashboardLibraryPanelKind{ + Kind: "LibraryPanel", + Spec: *NewDashboardLibraryPanelKindSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardLibraryPanelKindSpec struct { + // Panel ID for the library panel in the dashboard + Id float64 `json:"id"` + // Title for the library panel in the dashboard + Title string `json:"title"` + LibraryPanel DashboardLibraryPanelRef `json:"libraryPanel"` +} + +// NewDashboardLibraryPanelKindSpec creates a new DashboardLibraryPanelKindSpec object. +func NewDashboardLibraryPanelKindSpec() *DashboardLibraryPanelKindSpec { + return &DashboardLibraryPanelKindSpec{ + LibraryPanel: *NewDashboardLibraryPanelRef(), + } +} + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +// +k8s:openapi-gen=true +type DashboardLibraryPanelRef struct { + // Library panel name + Name string `json:"name"` + // Library panel uid + Uid string `json:"uid"` +} + +// NewDashboardLibraryPanelRef creates a new DashboardLibraryPanelRef object. +func NewDashboardLibraryPanelRef() *DashboardLibraryPanelRef { + return &DashboardLibraryPanelRef{} +} + +// +k8s:openapi-gen=true +type DashboardGridLayoutKind struct { + Kind string `json:"kind"` + Spec DashboardGridLayoutSpec `json:"spec"` +} + +// NewDashboardGridLayoutKind creates a new DashboardGridLayoutKind object. +func NewDashboardGridLayoutKind() *DashboardGridLayoutKind { + return &DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: *NewDashboardGridLayoutSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardGridLayoutSpec struct { + Items []DashboardGridLayoutItemKind `json:"items"` +} + +// NewDashboardGridLayoutSpec creates a new DashboardGridLayoutSpec object. +func NewDashboardGridLayoutSpec() *DashboardGridLayoutSpec { + return &DashboardGridLayoutSpec{ + Items: []DashboardGridLayoutItemKind{}, + } +} + +// +k8s:openapi-gen=true +type DashboardGridLayoutItemKind struct { + Kind string `json:"kind"` + Spec DashboardGridLayoutItemSpec `json:"spec"` +} + +// NewDashboardGridLayoutItemKind creates a new DashboardGridLayoutItemKind object. +func NewDashboardGridLayoutItemKind() *DashboardGridLayoutItemKind { + return &DashboardGridLayoutItemKind{ + Kind: "GridLayoutItem", + Spec: *NewDashboardGridLayoutItemSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardGridLayoutItemSpec struct { + X int64 `json:"x"` + Y int64 `json:"y"` + Width int64 `json:"width"` + Height int64 `json:"height"` + // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference + Element DashboardElementReference `json:"element"` + Repeat *DashboardRepeatOptions `json:"repeat,omitempty"` +} + +// NewDashboardGridLayoutItemSpec creates a new DashboardGridLayoutItemSpec object. +func NewDashboardGridLayoutItemSpec() *DashboardGridLayoutItemSpec { + return &DashboardGridLayoutItemSpec{ + Element: *NewDashboardElementReference(), + } +} + +// +k8s:openapi-gen=true +type DashboardElementReference struct { + Kind string `json:"kind"` + Name string `json:"name"` +} + +// NewDashboardElementReference creates a new DashboardElementReference object. +func NewDashboardElementReference() *DashboardElementReference { + return &DashboardElementReference{ + Kind: "ElementReference", + } +} + +// +k8s:openapi-gen=true +type DashboardRepeatOptions struct { + Mode string `json:"mode"` + Value string `json:"value"` + Direction *DashboardRepeatOptionsDirection `json:"direction,omitempty"` + MaxPerRow *int64 `json:"maxPerRow,omitempty"` +} + +// NewDashboardRepeatOptions creates a new DashboardRepeatOptions object. +func NewDashboardRepeatOptions() *DashboardRepeatOptions { + return &DashboardRepeatOptions{ + Mode: DashboardRepeatMode, + } +} + +// other repeat modes will be added in the future: label, frame +// +k8s:openapi-gen=true +const DashboardRepeatMode = "variable" + +// +k8s:openapi-gen=true +type DashboardRowsLayoutKind struct { + Kind string `json:"kind"` + Spec DashboardRowsLayoutSpec `json:"spec"` +} + +// NewDashboardRowsLayoutKind creates a new DashboardRowsLayoutKind object. +func NewDashboardRowsLayoutKind() *DashboardRowsLayoutKind { + return &DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: *NewDashboardRowsLayoutSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardRowsLayoutSpec struct { + Rows []DashboardRowsLayoutRowKind `json:"rows"` +} + +// NewDashboardRowsLayoutSpec creates a new DashboardRowsLayoutSpec object. +func NewDashboardRowsLayoutSpec() *DashboardRowsLayoutSpec { + return &DashboardRowsLayoutSpec{ + Rows: []DashboardRowsLayoutRowKind{}, + } +} + +// +k8s:openapi-gen=true +type DashboardRowsLayoutRowKind struct { + Kind string `json:"kind"` + Spec DashboardRowsLayoutRowSpec `json:"spec"` +} + +// NewDashboardRowsLayoutRowKind creates a new DashboardRowsLayoutRowKind object. +func NewDashboardRowsLayoutRowKind() *DashboardRowsLayoutRowKind { + return &DashboardRowsLayoutRowKind{ + Kind: "RowsLayoutRow", + Spec: *NewDashboardRowsLayoutRowSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardRowsLayoutRowSpec struct { + Title *string `json:"title,omitempty"` + Collapse *bool `json:"collapse,omitempty"` + HideHeader *bool `json:"hideHeader,omitempty"` + FillScreen *bool `json:"fillScreen,omitempty"` + ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"` + Repeat *DashboardRowRepeatOptions `json:"repeat,omitempty"` + Layout DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind `json:"layout"` +} + +// NewDashboardRowsLayoutRowSpec creates a new DashboardRowsLayoutRowSpec object. +func NewDashboardRowsLayoutRowSpec() *DashboardRowsLayoutRowSpec { + return &DashboardRowsLayoutRowSpec{ + Layout: *NewDashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(), + } +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingGroupKind struct { + Kind string `json:"kind"` + Spec DashboardConditionalRenderingGroupSpec `json:"spec"` +} + +// NewDashboardConditionalRenderingGroupKind creates a new DashboardConditionalRenderingGroupKind object. +func NewDashboardConditionalRenderingGroupKind() *DashboardConditionalRenderingGroupKind { + return &DashboardConditionalRenderingGroupKind{ + Kind: "ConditionalRenderingGroup", + Spec: *NewDashboardConditionalRenderingGroupSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingGroupSpec struct { + Visibility DashboardConditionalRenderingGroupSpecVisibility `json:"visibility"` + Condition DashboardConditionalRenderingGroupSpecCondition `json:"condition"` + Items []DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind `json:"items"` +} + +// NewDashboardConditionalRenderingGroupSpec creates a new DashboardConditionalRenderingGroupSpec object. +func NewDashboardConditionalRenderingGroupSpec() *DashboardConditionalRenderingGroupSpec { + return &DashboardConditionalRenderingGroupSpec{ + Items: []DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind{}, + } +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingVariableKind struct { + Kind string `json:"kind"` + Spec DashboardConditionalRenderingVariableSpec `json:"spec"` +} + +// NewDashboardConditionalRenderingVariableKind creates a new DashboardConditionalRenderingVariableKind object. +func NewDashboardConditionalRenderingVariableKind() *DashboardConditionalRenderingVariableKind { + return &DashboardConditionalRenderingVariableKind{ + Kind: "ConditionalRenderingVariable", + Spec: *NewDashboardConditionalRenderingVariableSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingVariableSpec struct { + Variable string `json:"variable"` + Operator DashboardConditionalRenderingVariableSpecOperator `json:"operator"` + Value string `json:"value"` +} + +// NewDashboardConditionalRenderingVariableSpec creates a new DashboardConditionalRenderingVariableSpec object. +func NewDashboardConditionalRenderingVariableSpec() *DashboardConditionalRenderingVariableSpec { + return &DashboardConditionalRenderingVariableSpec{} +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingDataKind struct { + Kind string `json:"kind"` + Spec DashboardConditionalRenderingDataSpec `json:"spec"` +} + +// NewDashboardConditionalRenderingDataKind creates a new DashboardConditionalRenderingDataKind object. +func NewDashboardConditionalRenderingDataKind() *DashboardConditionalRenderingDataKind { + return &DashboardConditionalRenderingDataKind{ + Kind: "ConditionalRenderingData", + Spec: *NewDashboardConditionalRenderingDataSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingDataSpec struct { + Value bool `json:"value"` +} + +// NewDashboardConditionalRenderingDataSpec creates a new DashboardConditionalRenderingDataSpec object. +func NewDashboardConditionalRenderingDataSpec() *DashboardConditionalRenderingDataSpec { + return &DashboardConditionalRenderingDataSpec{} +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingTimeRangeSizeKind struct { + Kind string `json:"kind"` + Spec DashboardConditionalRenderingTimeRangeSizeSpec `json:"spec"` +} + +// NewDashboardConditionalRenderingTimeRangeSizeKind creates a new DashboardConditionalRenderingTimeRangeSizeKind object. +func NewDashboardConditionalRenderingTimeRangeSizeKind() *DashboardConditionalRenderingTimeRangeSizeKind { + return &DashboardConditionalRenderingTimeRangeSizeKind{ + Kind: "ConditionalRenderingTimeRangeSize", + Spec: *NewDashboardConditionalRenderingTimeRangeSizeSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingTimeRangeSizeSpec struct { + Value string `json:"value"` +} + +// NewDashboardConditionalRenderingTimeRangeSizeSpec creates a new DashboardConditionalRenderingTimeRangeSizeSpec object. +func NewDashboardConditionalRenderingTimeRangeSizeSpec() *DashboardConditionalRenderingTimeRangeSizeSpec { + return &DashboardConditionalRenderingTimeRangeSizeSpec{} +} + +// +k8s:openapi-gen=true +type DashboardRowRepeatOptions struct { + Mode string `json:"mode"` + Value string `json:"value"` +} + +// NewDashboardRowRepeatOptions creates a new DashboardRowRepeatOptions object. +func NewDashboardRowRepeatOptions() *DashboardRowRepeatOptions { + return &DashboardRowRepeatOptions{ + Mode: DashboardRepeatMode, + } +} + +// +k8s:openapi-gen=true +type DashboardAutoGridLayoutKind struct { + Kind string `json:"kind"` + Spec DashboardAutoGridLayoutSpec `json:"spec"` +} + +// NewDashboardAutoGridLayoutKind creates a new DashboardAutoGridLayoutKind object. +func NewDashboardAutoGridLayoutKind() *DashboardAutoGridLayoutKind { + return &DashboardAutoGridLayoutKind{ + Kind: "AutoGridLayout", + Spec: *NewDashboardAutoGridLayoutSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardAutoGridLayoutSpec struct { + MaxColumnCount *float64 `json:"maxColumnCount,omitempty"` + ColumnWidthMode DashboardAutoGridLayoutSpecColumnWidthMode `json:"columnWidthMode"` + ColumnWidth *float64 `json:"columnWidth,omitempty"` + RowHeightMode DashboardAutoGridLayoutSpecRowHeightMode `json:"rowHeightMode"` + RowHeight *float64 `json:"rowHeight,omitempty"` + FillScreen *bool `json:"fillScreen,omitempty"` + Items []DashboardAutoGridLayoutItemKind `json:"items"` +} + +// NewDashboardAutoGridLayoutSpec creates a new DashboardAutoGridLayoutSpec object. +func NewDashboardAutoGridLayoutSpec() *DashboardAutoGridLayoutSpec { + return &DashboardAutoGridLayoutSpec{ + MaxColumnCount: (func(input float64) *float64 { return &input })(3), + FillScreen: (func(input bool) *bool { return &input })(false), + Items: []DashboardAutoGridLayoutItemKind{}, + } +} + +// +k8s:openapi-gen=true +type DashboardAutoGridLayoutItemKind struct { + Kind string `json:"kind"` + Spec DashboardAutoGridLayoutItemSpec `json:"spec"` +} + +// NewDashboardAutoGridLayoutItemKind creates a new DashboardAutoGridLayoutItemKind object. +func NewDashboardAutoGridLayoutItemKind() *DashboardAutoGridLayoutItemKind { + return &DashboardAutoGridLayoutItemKind{ + Kind: "AutoGridLayoutItem", + Spec: *NewDashboardAutoGridLayoutItemSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardAutoGridLayoutItemSpec struct { + Element DashboardElementReference `json:"element"` + Repeat *DashboardAutoGridRepeatOptions `json:"repeat,omitempty"` + ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"` +} + +// NewDashboardAutoGridLayoutItemSpec creates a new DashboardAutoGridLayoutItemSpec object. +func NewDashboardAutoGridLayoutItemSpec() *DashboardAutoGridLayoutItemSpec { + return &DashboardAutoGridLayoutItemSpec{ + Element: *NewDashboardElementReference(), + } +} + +// +k8s:openapi-gen=true +type DashboardAutoGridRepeatOptions struct { + Mode string `json:"mode"` + Value string `json:"value"` +} + +// NewDashboardAutoGridRepeatOptions creates a new DashboardAutoGridRepeatOptions object. +func NewDashboardAutoGridRepeatOptions() *DashboardAutoGridRepeatOptions { + return &DashboardAutoGridRepeatOptions{ + Mode: DashboardRepeatMode, + } +} + +// +k8s:openapi-gen=true +type DashboardTabsLayoutKind struct { + Kind string `json:"kind"` + Spec DashboardTabsLayoutSpec `json:"spec"` +} + +// NewDashboardTabsLayoutKind creates a new DashboardTabsLayoutKind object. +func NewDashboardTabsLayoutKind() *DashboardTabsLayoutKind { + return &DashboardTabsLayoutKind{ + Kind: "TabsLayout", + Spec: *NewDashboardTabsLayoutSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardTabsLayoutSpec struct { + Tabs []DashboardTabsLayoutTabKind `json:"tabs"` +} + +// NewDashboardTabsLayoutSpec creates a new DashboardTabsLayoutSpec object. +func NewDashboardTabsLayoutSpec() *DashboardTabsLayoutSpec { + return &DashboardTabsLayoutSpec{ + Tabs: []DashboardTabsLayoutTabKind{}, + } +} + +// +k8s:openapi-gen=true +type DashboardTabsLayoutTabKind struct { + Kind string `json:"kind"` + Spec DashboardTabsLayoutTabSpec `json:"spec"` +} + +// NewDashboardTabsLayoutTabKind creates a new DashboardTabsLayoutTabKind object. +func NewDashboardTabsLayoutTabKind() *DashboardTabsLayoutTabKind { + return &DashboardTabsLayoutTabKind{ + Kind: "TabsLayoutTab", + Spec: *NewDashboardTabsLayoutTabSpec(), + } +} + +// +k8s:openapi-gen=true +type DashboardTabsLayoutTabSpec struct { + Title *string `json:"title,omitempty"` + Layout DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind `json:"layout"` + ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"` + Repeat *DashboardTabRepeatOptions `json:"repeat,omitempty"` +} + +// NewDashboardTabsLayoutTabSpec creates a new DashboardTabsLayoutTabSpec object. +func NewDashboardTabsLayoutTabSpec() *DashboardTabsLayoutTabSpec { + return &DashboardTabsLayoutTabSpec{ + Layout: *NewDashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(), + } +} + +// +k8s:openapi-gen=true +type DashboardTabRepeatOptions struct { + Mode string `json:"mode"` + Value string `json:"value"` +} + +// NewDashboardTabRepeatOptions creates a new DashboardTabRepeatOptions object. +func NewDashboardTabRepeatOptions() *DashboardTabRepeatOptions { + return &DashboardTabRepeatOptions{ + Mode: DashboardRepeatMode, + } +} + +// Links with references to other dashboards or external resources +// +k8s:openapi-gen=true +type DashboardDashboardLink struct { + // Title to display with the link + Title string `json:"title"` + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` + Type DashboardDashboardLinkType `json:"type"` + // Icon name to be displayed with the link + Icon string `json:"icon"` + // Tooltip to display when the user hovers their mouse over it + Tooltip string `json:"tooltip"` + // Link URL. Only required/valid if the type is link + Url *string `json:"url,omitempty"` + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + Tags []string `json:"tags"` + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + AsDropdown bool `json:"asDropdown"` + // If true, the link will be opened in a new tab + TargetBlank bool `json:"targetBlank"` + // If true, includes current template variables values in the link as query params + IncludeVars bool `json:"includeVars"` + // If true, includes current time range in the link as query params + KeepTime bool `json:"keepTime"` +} + +// NewDashboardDashboardLink creates a new DashboardDashboardLink object. +func NewDashboardDashboardLink() *DashboardDashboardLink { + return &DashboardDashboardLink{ + Tags: []string{}, + AsDropdown: false, + TargetBlank: false, + IncludeVars: false, + KeepTime: false, + } +} + +// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) +// +k8s:openapi-gen=true +type DashboardDashboardLinkType string + +const ( + DashboardDashboardLinkTypeLink DashboardDashboardLinkType = "link" + DashboardDashboardLinkTypeDashboards DashboardDashboardLinkType = "dashboards" +) + +// Time configuration +// It defines the default time config for the time picker, the refresh picker for the specific dashboard. +// +k8s:openapi-gen=true +type DashboardTimeSettingsSpec struct { + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + Timezone *string `json:"timezone,omitempty"` + // Start time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + From string `json:"from"` + // End time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + To string `json:"to"` + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + // v1: refresh + AutoRefresh string `json:"autoRefresh"` + // Interval options available in the refresh picker dropdown. + // v1: timepicker.refresh_intervals + AutoRefreshIntervals []string `json:"autoRefreshIntervals"` + // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. + // v1: timepicker.quick_ranges , not exposed in the UI + QuickRanges []DashboardTimeRangeOption `json:"quickRanges,omitempty"` + // Whether timepicker is visible or not. + // v1: timepicker.hidden + HideTimepicker bool `json:"hideTimepicker"` + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + WeekStart *DashboardTimeSettingsSpecWeekStart `json:"weekStart,omitempty"` + // The month that the fiscal year starts on. 0 = January, 11 = December + FiscalYearStartMonth int64 `json:"fiscalYearStartMonth"` + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + // v1: timepicker.nowDelay + NowDelay *string `json:"nowDelay,omitempty"` +} + +// NewDashboardTimeSettingsSpec creates a new DashboardTimeSettingsSpec object. +func NewDashboardTimeSettingsSpec() *DashboardTimeSettingsSpec { + return &DashboardTimeSettingsSpec{ + Timezone: (func(input string) *string { return &input })("browser"), + From: "now-6h", + To: "now", + AutoRefresh: "", + AutoRefreshIntervals: []string{"5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"}, + HideTimepicker: false, + FiscalYearStartMonth: 0, + } +} + +// +k8s:openapi-gen=true +type DashboardTimeRangeOption struct { + Display string `json:"display"` + From string `json:"from"` + To string `json:"to"` +} + +// NewDashboardTimeRangeOption creates a new DashboardTimeRangeOption object. +func NewDashboardTimeRangeOption() *DashboardTimeRangeOption { + return &DashboardTimeRangeOption{ + Display: "Last 6 hours", + From: "now-6h", + To: "now", + } +} + +// +k8s:openapi-gen=true +type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind + +// NewDashboardVariableKind creates a new DashboardVariableKind object. +func NewDashboardVariableKind() *DashboardVariableKind { + return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() +} + +// Query variable kind +// +k8s:openapi-gen=true +type DashboardQueryVariableKind struct { + Kind string `json:"kind"` + Spec DashboardQueryVariableSpec `json:"spec"` +} + +// NewDashboardQueryVariableKind creates a new DashboardQueryVariableKind object. +func NewDashboardQueryVariableKind() *DashboardQueryVariableKind { + return &DashboardQueryVariableKind{ + Kind: "QueryVariable", + Spec: *NewDashboardQueryVariableSpec(), + } +} + +// Query variable specification +// +k8s:openapi-gen=true +type DashboardQueryVariableSpec struct { + Name string `json:"name"` + Current DashboardVariableOption `json:"current"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + Refresh DashboardVariableRefresh `json:"refresh"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + Query DashboardDataQueryKind `json:"query"` + Regex string `json:"regex"` + Sort DashboardVariableSort `json:"sort"` + Definition *string `json:"definition,omitempty"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + IncludeAll bool `json:"includeAll"` + AllValue *string `json:"allValue,omitempty"` + Placeholder *string `json:"placeholder,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` + StaticOptions []DashboardVariableOption `json:"staticOptions,omitempty"` + StaticOptionsOrder *DashboardQueryVariableSpecStaticOptionsOrder `json:"staticOptionsOrder,omitempty"` +} + +// NewDashboardQueryVariableSpec creates a new DashboardQueryVariableSpec object. +func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec { + return &DashboardQueryVariableSpec{ + Name: "", + Current: DashboardVariableOption{ + Text: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + Value: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + }, + Hide: DashboardVariableHideDontHide, + Refresh: DashboardVariableRefreshNever, + SkipUrlSync: false, + Query: *NewDashboardDataQueryKind(), + Regex: "", + Options: []DashboardVariableOption{}, + Multi: false, + IncludeAll: false, + AllowCustomValue: true, + } +} + +// Variable option specification +// +k8s:openapi-gen=true +type DashboardVariableOption struct { + // Whether the option is selected or not + Selected *bool `json:"selected,omitempty"` + // Text to be displayed for the option + Text DashboardStringOrArrayOfString `json:"text"` + // Value of the option + Value DashboardStringOrArrayOfString `json:"value"` +} + +// NewDashboardVariableOption creates a new DashboardVariableOption object. +func NewDashboardVariableOption() *DashboardVariableOption { + return &DashboardVariableOption{ + Text: *NewDashboardStringOrArrayOfString(), + Value: *NewDashboardStringOrArrayOfString(), + } +} + +// Determine if the variable shows on dashboard +// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). +// +k8s:openapi-gen=true +type DashboardVariableHide string + +const ( + DashboardVariableHideDontHide DashboardVariableHide = "dontHide" + DashboardVariableHideHideLabel DashboardVariableHide = "hideLabel" + DashboardVariableHideHideVariable DashboardVariableHide = "hideVariable" +) + +// Options to config when to refresh a variable +// `never`: Never refresh the variable +// `onDashboardLoad`: Queries the data source every time the dashboard loads. +// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. +// +k8s:openapi-gen=true +type DashboardVariableRefresh string + +const ( + DashboardVariableRefreshNever DashboardVariableRefresh = "never" + DashboardVariableRefreshOnDashboardLoad DashboardVariableRefresh = "onDashboardLoad" + DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged" +) + +// Sort variable options +// Accepted values are: +// `disabled`: No sorting +// `alphabeticalAsc`: Alphabetical ASC +// `alphabeticalDesc`: Alphabetical DESC +// `numericalAsc`: Numerical ASC +// `numericalDesc`: Numerical DESC +// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC +// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC +// `naturalAsc`: Natural ASC +// `naturalDesc`: Natural DESC +// VariableSort enum with default value +// +k8s:openapi-gen=true +type DashboardVariableSort string + +const ( + DashboardVariableSortDisabled DashboardVariableSort = "disabled" + DashboardVariableSortAlphabeticalAsc DashboardVariableSort = "alphabeticalAsc" + DashboardVariableSortAlphabeticalDesc DashboardVariableSort = "alphabeticalDesc" + DashboardVariableSortNumericalAsc DashboardVariableSort = "numericalAsc" + DashboardVariableSortNumericalDesc DashboardVariableSort = "numericalDesc" + DashboardVariableSortAlphabeticalCaseInsensitiveAsc DashboardVariableSort = "alphabeticalCaseInsensitiveAsc" + DashboardVariableSortAlphabeticalCaseInsensitiveDesc DashboardVariableSort = "alphabeticalCaseInsensitiveDesc" + DashboardVariableSortNaturalAsc DashboardVariableSort = "naturalAsc" + DashboardVariableSortNaturalDesc DashboardVariableSort = "naturalDesc" +) + +// Text variable kind +// +k8s:openapi-gen=true +type DashboardTextVariableKind struct { + Kind string `json:"kind"` + Spec DashboardTextVariableSpec `json:"spec"` +} + +// NewDashboardTextVariableKind creates a new DashboardTextVariableKind object. +func NewDashboardTextVariableKind() *DashboardTextVariableKind { + return &DashboardTextVariableKind{ + Kind: "TextVariable", + Spec: *NewDashboardTextVariableSpec(), + } +} + +// Text variable specification +// +k8s:openapi-gen=true +type DashboardTextVariableSpec struct { + Name string `json:"name"` + Current DashboardVariableOption `json:"current"` + Query string `json:"query"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardTextVariableSpec creates a new DashboardTextVariableSpec object. +func NewDashboardTextVariableSpec() *DashboardTextVariableSpec { + return &DashboardTextVariableSpec{ + Name: "", + Current: DashboardVariableOption{ + Text: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + Value: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + }, + Query: "", + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + +// Constant variable kind +// +k8s:openapi-gen=true +type DashboardConstantVariableKind struct { + Kind string `json:"kind"` + Spec DashboardConstantVariableSpec `json:"spec"` +} + +// NewDashboardConstantVariableKind creates a new DashboardConstantVariableKind object. +func NewDashboardConstantVariableKind() *DashboardConstantVariableKind { + return &DashboardConstantVariableKind{ + Kind: "ConstantVariable", + Spec: *NewDashboardConstantVariableSpec(), + } +} + +// Constant variable specification +// +k8s:openapi-gen=true +type DashboardConstantVariableSpec struct { + Name string `json:"name"` + Query string `json:"query"` + Current DashboardVariableOption `json:"current"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardConstantVariableSpec creates a new DashboardConstantVariableSpec object. +func NewDashboardConstantVariableSpec() *DashboardConstantVariableSpec { + return &DashboardConstantVariableSpec{ + Name: "", + Query: "", + Current: DashboardVariableOption{ + Text: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + Value: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + }, + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + +// Datasource variable kind +// +k8s:openapi-gen=true +type DashboardDatasourceVariableKind struct { + Kind string `json:"kind"` + Spec DashboardDatasourceVariableSpec `json:"spec"` +} + +// NewDashboardDatasourceVariableKind creates a new DashboardDatasourceVariableKind object. +func NewDashboardDatasourceVariableKind() *DashboardDatasourceVariableKind { + return &DashboardDatasourceVariableKind{ + Kind: "DatasourceVariable", + Spec: *NewDashboardDatasourceVariableSpec(), + } +} + +// Datasource variable specification +// +k8s:openapi-gen=true +type DashboardDatasourceVariableSpec struct { + Name string `json:"name"` + PluginId string `json:"pluginId"` + Refresh DashboardVariableRefresh `json:"refresh"` + Regex string `json:"regex"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + IncludeAll bool `json:"includeAll"` + AllValue *string `json:"allValue,omitempty"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` +} + +// NewDashboardDatasourceVariableSpec creates a new DashboardDatasourceVariableSpec object. +func NewDashboardDatasourceVariableSpec() *DashboardDatasourceVariableSpec { + return &DashboardDatasourceVariableSpec{ + Name: "", + PluginId: "", + Refresh: DashboardVariableRefreshNever, + Regex: "", + Current: DashboardVariableOption{ + Text: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + Value: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + }, + Options: []DashboardVariableOption{}, + Multi: false, + IncludeAll: false, + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + AllowCustomValue: true, + } +} + +// Interval variable kind +// +k8s:openapi-gen=true +type DashboardIntervalVariableKind struct { + Kind string `json:"kind"` + Spec DashboardIntervalVariableSpec `json:"spec"` +} + +// NewDashboardIntervalVariableKind creates a new DashboardIntervalVariableKind object. +func NewDashboardIntervalVariableKind() *DashboardIntervalVariableKind { + return &DashboardIntervalVariableKind{ + Kind: "IntervalVariable", + Spec: *NewDashboardIntervalVariableSpec(), + } +} + +// Interval variable specification +// +k8s:openapi-gen=true +type DashboardIntervalVariableSpec struct { + Name string `json:"name"` + Query string `json:"query"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Auto bool `json:"auto"` + AutoMin string `json:"auto_min"` + AutoCount int64 `json:"auto_count"` + Refresh DashboardVariableRefresh `json:"refresh"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardIntervalVariableSpec creates a new DashboardIntervalVariableSpec object. +func NewDashboardIntervalVariableSpec() *DashboardIntervalVariableSpec { + return &DashboardIntervalVariableSpec{ + Name: "", + Query: "", + Current: DashboardVariableOption{ + Text: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + Value: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + }, + Options: []DashboardVariableOption{}, + Auto: false, + AutoMin: "", + AutoCount: 0, + Refresh: DashboardVariableRefreshNever, + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + +// Custom variable kind +// +k8s:openapi-gen=true +type DashboardCustomVariableKind struct { + Kind string `json:"kind"` + Spec DashboardCustomVariableSpec `json:"spec"` +} + +// NewDashboardCustomVariableKind creates a new DashboardCustomVariableKind object. +func NewDashboardCustomVariableKind() *DashboardCustomVariableKind { + return &DashboardCustomVariableKind{ + Kind: "CustomVariable", + Spec: *NewDashboardCustomVariableSpec(), + } +} + +// Custom variable specification +// +k8s:openapi-gen=true +type DashboardCustomVariableSpec struct { + Name string `json:"name"` + Query string `json:"query"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + IncludeAll bool `json:"includeAll"` + AllValue *string `json:"allValue,omitempty"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` +} + +// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object. +func NewDashboardCustomVariableSpec() *DashboardCustomVariableSpec { + return &DashboardCustomVariableSpec{ + Name: "", + Query: "", + Current: *NewDashboardVariableOption(), + Options: []DashboardVariableOption{}, + Multi: false, + IncludeAll: false, + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + AllowCustomValue: true, + } +} + +// Group variable kind +// +k8s:openapi-gen=true +type DashboardGroupByVariableKind struct { + Kind string `json:"kind"` + Spec DashboardGroupByVariableSpec `json:"spec"` +} + +// NewDashboardGroupByVariableKind creates a new DashboardGroupByVariableKind object. +func NewDashboardGroupByVariableKind() *DashboardGroupByVariableKind { + return &DashboardGroupByVariableKind{ + Kind: "GroupByVariable", + Spec: *NewDashboardGroupByVariableSpec(), + } +} + +// GroupBy variable specification +// +k8s:openapi-gen=true +type DashboardGroupByVariableSpec struct { + Name string `json:"name"` + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + DefaultValue *DashboardVariableOption `json:"defaultValue,omitempty"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardGroupByVariableSpec creates a new DashboardGroupByVariableSpec object. +func NewDashboardGroupByVariableSpec() *DashboardGroupByVariableSpec { + return &DashboardGroupByVariableSpec{ + Name: "", + Current: DashboardVariableOption{ + Text: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + Value: DashboardStringOrArrayOfString{ + String: (func(input string) *string { return &input })(""), + }, + }, + Options: []DashboardVariableOption{}, + Multi: false, + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + +// Adhoc variable kind +// +k8s:openapi-gen=true +type DashboardAdhocVariableKind struct { + Kind string `json:"kind"` + Spec DashboardAdhocVariableSpec `json:"spec"` +} + +// NewDashboardAdhocVariableKind creates a new DashboardAdhocVariableKind object. +func NewDashboardAdhocVariableKind() *DashboardAdhocVariableKind { + return &DashboardAdhocVariableKind{ + Kind: "AdhocVariable", + Spec: *NewDashboardAdhocVariableSpec(), + } +} + +// Adhoc variable specification +// +k8s:openapi-gen=true +type DashboardAdhocVariableSpec struct { + Name string `json:"name"` + Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` + BaseFilters []DashboardAdHocFilterWithLabels `json:"baseFilters"` + Filters []DashboardAdHocFilterWithLabels `json:"filters"` + DefaultKeys []DashboardMetricFindValue `json:"defaultKeys"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` +} + +// NewDashboardAdhocVariableSpec creates a new DashboardAdhocVariableSpec object. +func NewDashboardAdhocVariableSpec() *DashboardAdhocVariableSpec { + return &DashboardAdhocVariableSpec{ + Name: "", + BaseFilters: []DashboardAdHocFilterWithLabels{}, + Filters: []DashboardAdHocFilterWithLabels{}, + DefaultKeys: []DashboardMetricFindValue{}, + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + AllowCustomValue: true, + } +} + +// Define the AdHocFilterWithLabels type +// +k8s:openapi-gen=true +type DashboardAdHocFilterWithLabels struct { + Key string `json:"key"` + Operator string `json:"operator"` + Value string `json:"value"` + Values []string `json:"values,omitempty"` + KeyLabel *string `json:"keyLabel,omitempty"` + ValueLabels []string `json:"valueLabels,omitempty"` + ForceEdit *bool `json:"forceEdit,omitempty"` + Origin string `json:"origin,omitempty"` + // @deprecated + Condition *string `json:"condition,omitempty"` +} + +// NewDashboardAdHocFilterWithLabels creates a new DashboardAdHocFilterWithLabels object. +func NewDashboardAdHocFilterWithLabels() *DashboardAdHocFilterWithLabels { + return &DashboardAdHocFilterWithLabels{ + Origin: DashboardFilterOrigin, + } +} + +// Determine the origin of the adhoc variable filter +// +k8s:openapi-gen=true +const DashboardFilterOrigin = "dashboard" + +// Define the MetricFindValue type +// +k8s:openapi-gen=true +type DashboardMetricFindValue struct { + Text string `json:"text"` + Value *DashboardStringOrFloat64 `json:"value,omitempty"` + Group *string `json:"group,omitempty"` + Expandable *bool `json:"expandable,omitempty"` +} + +// NewDashboardMetricFindValue creates a new DashboardMetricFindValue object. +func NewDashboardMetricFindValue() *DashboardMetricFindValue { + return &DashboardMetricFindValue{} +} + +// +k8s:openapi-gen=true +type DashboardSpec struct { + Annotations []DashboardAnnotationQueryKind `json:"annotations"` + // Configuration of dashboard cursor sync behavior. + // "Off" for no shared crosshair or tooltip (default). + // "Crosshair" for shared crosshair. + // "Tooltip" for shared crosshair AND shared tooltip. + CursorSync DashboardDashboardCursorSync `json:"cursorSync"` + // Description of dashboard. + Description *string `json:"description,omitempty"` + // Whether a dashboard is editable or not. + Editable *bool `json:"editable,omitempty"` + Elements map[string]DashboardElement `json:"elements"` + Layout DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind `json:"layout"` + // Links with references to other dashboards or external websites. + Links []DashboardDashboardLink `json:"links"` + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data. + LiveNow *bool `json:"liveNow,omitempty"` + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + Preload bool `json:"preload"` + // Plugins only. The version of the dashboard installed together with the plugin. + // This is used to determine if the dashboard should be updated when the plugin is updated. + Revision *uint16 `json:"revision,omitempty"` + // Tags associated with dashboard. + Tags []string `json:"tags"` + TimeSettings DashboardTimeSettingsSpec `json:"timeSettings"` + // Title of dashboard. + Title string `json:"title"` + // Configured template variables. + Variables []DashboardVariableKind `json:"variables"` +} + +// NewDashboardSpec creates a new DashboardSpec object. +func NewDashboardSpec() *DashboardSpec { + return &DashboardSpec{ + Annotations: []DashboardAnnotationQueryKind{}, + CursorSync: DashboardDashboardCursorSyncOff, + Editable: (func(input bool) *bool { return &input })(true), + Elements: map[string]DashboardElement{}, + Layout: *NewDashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(), + Links: []DashboardDashboardLink{}, + Preload: false, + Tags: []string{}, + TimeSettings: *NewDashboardTimeSettingsSpec(), + Variables: []DashboardVariableKind{}, + } +} + +// +k8s:openapi-gen=true +type DashboardV2alpha2FieldConfigSourceOverrides struct { + Matcher DashboardMatcherConfig `json:"matcher"` + Properties []DashboardDynamicConfigValue `json:"properties"` +} + +// NewDashboardV2alpha2FieldConfigSourceOverrides creates a new DashboardV2alpha2FieldConfigSourceOverrides object. +func NewDashboardV2alpha2FieldConfigSourceOverrides() *DashboardV2alpha2FieldConfigSourceOverrides { + return &DashboardV2alpha2FieldConfigSourceOverrides{ + Matcher: *NewDashboardMatcherConfig(), + Properties: []DashboardDynamicConfigValue{}, + } +} + +// +k8s:openapi-gen=true +type DashboardV2alpha2RangeMapOptions struct { + // Min value of the range. It can be null which means -Infinity + From *float64 `json:"from"` + // Max value of the range. It can be null which means +Infinity + To *float64 `json:"to"` + // Config to apply when the value is within the range + Result DashboardValueMappingResult `json:"result"` +} + +// NewDashboardV2alpha2RangeMapOptions creates a new DashboardV2alpha2RangeMapOptions object. +func NewDashboardV2alpha2RangeMapOptions() *DashboardV2alpha2RangeMapOptions { + return &DashboardV2alpha2RangeMapOptions{ + Result: *NewDashboardValueMappingResult(), + } +} + +// +k8s:openapi-gen=true +type DashboardV2alpha2RegexMapOptions struct { + // Regular expression to match against + Pattern string `json:"pattern"` + // Config to apply when the value matches the regex + Result DashboardValueMappingResult `json:"result"` +} + +// NewDashboardV2alpha2RegexMapOptions creates a new DashboardV2alpha2RegexMapOptions object. +func NewDashboardV2alpha2RegexMapOptions() *DashboardV2alpha2RegexMapOptions { + return &DashboardV2alpha2RegexMapOptions{ + Result: *NewDashboardValueMappingResult(), + } +} + +// +k8s:openapi-gen=true +type DashboardV2alpha2SpecialValueMapOptions struct { + // Special value to match against + Match DashboardSpecialValueMatch `json:"match"` + // Config to apply when the value matches the special value + Result DashboardValueMappingResult `json:"result"` +} + +// NewDashboardV2alpha2SpecialValueMapOptions creates a new DashboardV2alpha2SpecialValueMapOptions object. +func NewDashboardV2alpha2SpecialValueMapOptions() *DashboardV2alpha2SpecialValueMapOptions { + return &DashboardV2alpha2SpecialValueMapOptions{ + Result: *NewDashboardValueMappingResult(), + } +} + +// +k8s:openapi-gen=true +type DashboardRepeatOptionsDirection string + +const ( + DashboardRepeatOptionsDirectionH DashboardRepeatOptionsDirection = "h" + DashboardRepeatOptionsDirectionV DashboardRepeatOptionsDirection = "v" +) + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingGroupSpecVisibility string + +const ( + DashboardConditionalRenderingGroupSpecVisibilityShow DashboardConditionalRenderingGroupSpecVisibility = "show" + DashboardConditionalRenderingGroupSpecVisibilityHide DashboardConditionalRenderingGroupSpecVisibility = "hide" +) + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingGroupSpecCondition string + +const ( + DashboardConditionalRenderingGroupSpecConditionAnd DashboardConditionalRenderingGroupSpecCondition = "and" + DashboardConditionalRenderingGroupSpecConditionOr DashboardConditionalRenderingGroupSpecCondition = "or" +) + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingVariableSpecOperator string + +const ( + DashboardConditionalRenderingVariableSpecOperatorEquals DashboardConditionalRenderingVariableSpecOperator = "equals" + DashboardConditionalRenderingVariableSpecOperatorNotEquals DashboardConditionalRenderingVariableSpecOperator = "notEquals" +) + +// +k8s:openapi-gen=true +type DashboardAutoGridLayoutSpecColumnWidthMode string + +const ( + DashboardAutoGridLayoutSpecColumnWidthModeNarrow DashboardAutoGridLayoutSpecColumnWidthMode = "narrow" + DashboardAutoGridLayoutSpecColumnWidthModeStandard DashboardAutoGridLayoutSpecColumnWidthMode = "standard" + DashboardAutoGridLayoutSpecColumnWidthModeWide DashboardAutoGridLayoutSpecColumnWidthMode = "wide" + DashboardAutoGridLayoutSpecColumnWidthModeCustom DashboardAutoGridLayoutSpecColumnWidthMode = "custom" +) + +// +k8s:openapi-gen=true +type DashboardAutoGridLayoutSpecRowHeightMode string + +const ( + DashboardAutoGridLayoutSpecRowHeightModeShort DashboardAutoGridLayoutSpecRowHeightMode = "short" + DashboardAutoGridLayoutSpecRowHeightModeStandard DashboardAutoGridLayoutSpecRowHeightMode = "standard" + DashboardAutoGridLayoutSpecRowHeightModeTall DashboardAutoGridLayoutSpecRowHeightMode = "tall" + DashboardAutoGridLayoutSpecRowHeightModeCustom DashboardAutoGridLayoutSpecRowHeightMode = "custom" +) + +// +k8s:openapi-gen=true +type DashboardTimeSettingsSpecWeekStart string + +const ( + DashboardTimeSettingsSpecWeekStartSaturday DashboardTimeSettingsSpecWeekStart = "saturday" + DashboardTimeSettingsSpecWeekStartMonday DashboardTimeSettingsSpecWeekStart = "monday" + DashboardTimeSettingsSpecWeekStartSunday DashboardTimeSettingsSpecWeekStart = "sunday" +) + +// +k8s:openapi-gen=true +type DashboardQueryVariableSpecStaticOptionsOrder string + +const ( + DashboardQueryVariableSpecStaticOptionsOrderBefore DashboardQueryVariableSpecStaticOptionsOrder = "before" + DashboardQueryVariableSpecStaticOptionsOrderAfter DashboardQueryVariableSpecStaticOptionsOrder = "after" + DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted" +) + +// +k8s:openapi-gen=true +type DashboardPanelKindOrLibraryPanelKind struct { + PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"` + LibraryPanelKind *DashboardLibraryPanelKind `json:"LibraryPanelKind,omitempty"` +} + +// NewDashboardPanelKindOrLibraryPanelKind creates a new DashboardPanelKindOrLibraryPanelKind object. +func NewDashboardPanelKindOrLibraryPanelKind() *DashboardPanelKindOrLibraryPanelKind { + return &DashboardPanelKindOrLibraryPanelKind{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardPanelKindOrLibraryPanelKind` as JSON. +func (resource DashboardPanelKindOrLibraryPanelKind) MarshalJSON() ([]byte, error) { + if resource.PanelKind != nil { + return json.Marshal(resource.PanelKind) + } + if resource.LibraryPanelKind != nil { + return json.Marshal(resource.LibraryPanelKind) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardPanelKindOrLibraryPanelKind` from JSON. +func (resource *DashboardPanelKindOrLibraryPanelKind) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. + parsedAsMap := make(map[string]interface{}) + if err := json.Unmarshal(raw, &parsedAsMap); err != nil { + return err + } + + discriminator, found := parsedAsMap["kind"] + if !found { + return nil + } + + switch discriminator { + case "LibraryPanel": + var dashboardLibraryPanelKind DashboardLibraryPanelKind + if err := json.Unmarshal(raw, &dashboardLibraryPanelKind); err != nil { + return err + } + + resource.LibraryPanelKind = &dashboardLibraryPanelKind + return nil + case "Panel": + var dashboardPanelKind DashboardPanelKind + if err := json.Unmarshal(raw, &dashboardPanelKind); err != nil { + return err + } + + resource.PanelKind = &dashboardPanelKind + return nil + } + + return nil +} + +// +k8s:openapi-gen=true +type DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap struct { + ValueMap *DashboardValueMap `json:"ValueMap,omitempty"` + RangeMap *DashboardRangeMap `json:"RangeMap,omitempty"` + RegexMap *DashboardRegexMap `json:"RegexMap,omitempty"` + SpecialValueMap *DashboardSpecialValueMap `json:"SpecialValueMap,omitempty"` +} + +// NewDashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap creates a new DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap object. +func NewDashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap() *DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap { + return &DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap` as JSON. +func (resource DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap) MarshalJSON() ([]byte, error) { + if resource.ValueMap != nil { + return json.Marshal(resource.ValueMap) + } + if resource.RangeMap != nil { + return json.Marshal(resource.RangeMap) + } + if resource.RegexMap != nil { + return json.Marshal(resource.RegexMap) + } + if resource.SpecialValueMap != nil { + return json.Marshal(resource.SpecialValueMap) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap` from JSON. +func (resource *DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. + parsedAsMap := make(map[string]interface{}) + if err := json.Unmarshal(raw, &parsedAsMap); err != nil { + return err + } + + discriminator, found := parsedAsMap["type"] + if !found { + return nil + } + + switch discriminator { + case "range": + var dashboardRangeMap DashboardRangeMap + if err := json.Unmarshal(raw, &dashboardRangeMap); err != nil { + return err + } + + resource.RangeMap = &dashboardRangeMap + return nil + case "regex": + var dashboardRegexMap DashboardRegexMap + if err := json.Unmarshal(raw, &dashboardRegexMap); err != nil { + return err + } + + resource.RegexMap = &dashboardRegexMap + return nil + case "special": + var dashboardSpecialValueMap DashboardSpecialValueMap + if err := json.Unmarshal(raw, &dashboardSpecialValueMap); err != nil { + return err + } + + resource.SpecialValueMap = &dashboardSpecialValueMap + return nil + case "value": + var dashboardValueMap DashboardValueMap + if err := json.Unmarshal(raw, &dashboardValueMap); err != nil { + return err + } + + resource.ValueMap = &dashboardValueMap + return nil + } + + return nil +} + +// +k8s:openapi-gen=true +type DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind struct { + GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,omitempty"` + AutoGridLayoutKind *DashboardAutoGridLayoutKind `json:"AutoGridLayoutKind,omitempty"` + TabsLayoutKind *DashboardTabsLayoutKind `json:"TabsLayoutKind,omitempty"` + RowsLayoutKind *DashboardRowsLayoutKind `json:"RowsLayoutKind,omitempty"` +} + +// NewDashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind creates a new DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind object. +func NewDashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind() *DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind { + return &DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind` as JSON. +func (resource DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind) MarshalJSON() ([]byte, error) { + if resource.GridLayoutKind != nil { + return json.Marshal(resource.GridLayoutKind) + } + if resource.AutoGridLayoutKind != nil { + return json.Marshal(resource.AutoGridLayoutKind) + } + if resource.TabsLayoutKind != nil { + return json.Marshal(resource.TabsLayoutKind) + } + if resource.RowsLayoutKind != nil { + return json.Marshal(resource.RowsLayoutKind) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind` from JSON. +func (resource *DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. + parsedAsMap := make(map[string]interface{}) + if err := json.Unmarshal(raw, &parsedAsMap); err != nil { + return err + } + + discriminator, found := parsedAsMap["kind"] + if !found { + return nil + } + + switch discriminator { + case "AutoGridLayout": + var dashboardAutoGridLayoutKind DashboardAutoGridLayoutKind + if err := json.Unmarshal(raw, &dashboardAutoGridLayoutKind); err != nil { + return err + } + + resource.AutoGridLayoutKind = &dashboardAutoGridLayoutKind + return nil + case "GridLayout": + var dashboardGridLayoutKind DashboardGridLayoutKind + if err := json.Unmarshal(raw, &dashboardGridLayoutKind); err != nil { + return err + } + + resource.GridLayoutKind = &dashboardGridLayoutKind + return nil + case "RowsLayout": + var dashboardRowsLayoutKind DashboardRowsLayoutKind + if err := json.Unmarshal(raw, &dashboardRowsLayoutKind); err != nil { + return err + } + + resource.RowsLayoutKind = &dashboardRowsLayoutKind + return nil + case "TabsLayout": + var dashboardTabsLayoutKind DashboardTabsLayoutKind + if err := json.Unmarshal(raw, &dashboardTabsLayoutKind); err != nil { + return err + } + + resource.TabsLayoutKind = &dashboardTabsLayoutKind + return nil + } + + return nil +} + +// +k8s:openapi-gen=true +type DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind struct { + ConditionalRenderingVariableKind *DashboardConditionalRenderingVariableKind `json:"ConditionalRenderingVariableKind,omitempty"` + ConditionalRenderingDataKind *DashboardConditionalRenderingDataKind `json:"ConditionalRenderingDataKind,omitempty"` + ConditionalRenderingTimeRangeSizeKind *DashboardConditionalRenderingTimeRangeSizeKind `json:"ConditionalRenderingTimeRangeSizeKind,omitempty"` +} + +// NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind creates a new DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind object. +func NewDashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind() *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind { + return &DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind` as JSON. +func (resource DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind) MarshalJSON() ([]byte, error) { + if resource.ConditionalRenderingVariableKind != nil { + return json.Marshal(resource.ConditionalRenderingVariableKind) + } + if resource.ConditionalRenderingDataKind != nil { + return json.Marshal(resource.ConditionalRenderingDataKind) + } + if resource.ConditionalRenderingTimeRangeSizeKind != nil { + return json.Marshal(resource.ConditionalRenderingTimeRangeSizeKind) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind` from JSON. +func (resource *DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. + parsedAsMap := make(map[string]interface{}) + if err := json.Unmarshal(raw, &parsedAsMap); err != nil { + return err + } + + discriminator, found := parsedAsMap["kind"] + if !found { + return nil + } + + switch discriminator { + case "ConditionalRenderingData": + var dashboardConditionalRenderingDataKind DashboardConditionalRenderingDataKind + if err := json.Unmarshal(raw, &dashboardConditionalRenderingDataKind); err != nil { + return err + } + + resource.ConditionalRenderingDataKind = &dashboardConditionalRenderingDataKind + return nil + case "ConditionalRenderingTimeRangeSize": + var dashboardConditionalRenderingTimeRangeSizeKind DashboardConditionalRenderingTimeRangeSizeKind + if err := json.Unmarshal(raw, &dashboardConditionalRenderingTimeRangeSizeKind); err != nil { + return err + } + + resource.ConditionalRenderingTimeRangeSizeKind = &dashboardConditionalRenderingTimeRangeSizeKind + return nil + case "ConditionalRenderingVariable": + var dashboardConditionalRenderingVariableKind DashboardConditionalRenderingVariableKind + if err := json.Unmarshal(raw, &dashboardConditionalRenderingVariableKind); err != nil { + return err + } + + resource.ConditionalRenderingVariableKind = &dashboardConditionalRenderingVariableKind + return nil + } + + return nil +} + +// +k8s:openapi-gen=true +type DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind struct { + GridLayoutKind *DashboardGridLayoutKind `json:"GridLayoutKind,omitempty"` + RowsLayoutKind *DashboardRowsLayoutKind `json:"RowsLayoutKind,omitempty"` + AutoGridLayoutKind *DashboardAutoGridLayoutKind `json:"AutoGridLayoutKind,omitempty"` + TabsLayoutKind *DashboardTabsLayoutKind `json:"TabsLayoutKind,omitempty"` +} + +// NewDashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind creates a new DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind object. +func NewDashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind() *DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind { + return &DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind` as JSON. +func (resource DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) MarshalJSON() ([]byte, error) { + if resource.GridLayoutKind != nil { + return json.Marshal(resource.GridLayoutKind) + } + if resource.RowsLayoutKind != nil { + return json.Marshal(resource.RowsLayoutKind) + } + if resource.AutoGridLayoutKind != nil { + return json.Marshal(resource.AutoGridLayoutKind) + } + if resource.TabsLayoutKind != nil { + return json.Marshal(resource.TabsLayoutKind) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind` from JSON. +func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. + parsedAsMap := make(map[string]interface{}) + if err := json.Unmarshal(raw, &parsedAsMap); err != nil { + return err + } + + discriminator, found := parsedAsMap["kind"] + if !found { + return nil + } + + switch discriminator { + case "AutoGridLayout": + var dashboardAutoGridLayoutKind DashboardAutoGridLayoutKind + if err := json.Unmarshal(raw, &dashboardAutoGridLayoutKind); err != nil { + return err + } + + resource.AutoGridLayoutKind = &dashboardAutoGridLayoutKind + return nil + case "GridLayout": + var dashboardGridLayoutKind DashboardGridLayoutKind + if err := json.Unmarshal(raw, &dashboardGridLayoutKind); err != nil { + return err + } + + resource.GridLayoutKind = &dashboardGridLayoutKind + return nil + case "RowsLayout": + var dashboardRowsLayoutKind DashboardRowsLayoutKind + if err := json.Unmarshal(raw, &dashboardRowsLayoutKind); err != nil { + return err + } + + resource.RowsLayoutKind = &dashboardRowsLayoutKind + return nil + case "TabsLayout": + var dashboardTabsLayoutKind DashboardTabsLayoutKind + if err := json.Unmarshal(raw, &dashboardTabsLayoutKind); err != nil { + return err + } + + resource.TabsLayoutKind = &dashboardTabsLayoutKind + return nil + } + + return nil +} + +// +k8s:openapi-gen=true +type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind struct { + QueryVariableKind *DashboardQueryVariableKind `json:"QueryVariableKind,omitempty"` + TextVariableKind *DashboardTextVariableKind `json:"TextVariableKind,omitempty"` + ConstantVariableKind *DashboardConstantVariableKind `json:"ConstantVariableKind,omitempty"` + DatasourceVariableKind *DashboardDatasourceVariableKind `json:"DatasourceVariableKind,omitempty"` + IntervalVariableKind *DashboardIntervalVariableKind `json:"IntervalVariableKind,omitempty"` + CustomVariableKind *DashboardCustomVariableKind `json:"CustomVariableKind,omitempty"` + GroupByVariableKind *DashboardGroupByVariableKind `json:"GroupByVariableKind,omitempty"` + AdhocVariableKind *DashboardAdhocVariableKind `json:"AdhocVariableKind,omitempty"` +} + +// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind object. +func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind { + return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` as JSON. +func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) MarshalJSON() ([]byte, error) { + if resource.QueryVariableKind != nil { + return json.Marshal(resource.QueryVariableKind) + } + if resource.TextVariableKind != nil { + return json.Marshal(resource.TextVariableKind) + } + if resource.ConstantVariableKind != nil { + return json.Marshal(resource.ConstantVariableKind) + } + if resource.DatasourceVariableKind != nil { + return json.Marshal(resource.DatasourceVariableKind) + } + if resource.IntervalVariableKind != nil { + return json.Marshal(resource.IntervalVariableKind) + } + if resource.CustomVariableKind != nil { + return json.Marshal(resource.CustomVariableKind) + } + if resource.GroupByVariableKind != nil { + return json.Marshal(resource.GroupByVariableKind) + } + if resource.AdhocVariableKind != nil { + return json.Marshal(resource.AdhocVariableKind) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` from JSON. +func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + // FIXME: this is wasteful, we need to find a more efficient way to unmarshal this. + parsedAsMap := make(map[string]interface{}) + if err := json.Unmarshal(raw, &parsedAsMap); err != nil { + return err + } + + discriminator, found := parsedAsMap["kind"] + if !found { + return nil + } + + switch discriminator { + case "AdhocVariable": + var dashboardAdhocVariableKind DashboardAdhocVariableKind + if err := json.Unmarshal(raw, &dashboardAdhocVariableKind); err != nil { + return err + } + + resource.AdhocVariableKind = &dashboardAdhocVariableKind + return nil + case "ConstantVariable": + var dashboardConstantVariableKind DashboardConstantVariableKind + if err := json.Unmarshal(raw, &dashboardConstantVariableKind); err != nil { + return err + } + + resource.ConstantVariableKind = &dashboardConstantVariableKind + return nil + case "CustomVariable": + var dashboardCustomVariableKind DashboardCustomVariableKind + if err := json.Unmarshal(raw, &dashboardCustomVariableKind); err != nil { + return err + } + + resource.CustomVariableKind = &dashboardCustomVariableKind + return nil + case "DatasourceVariable": + var dashboardDatasourceVariableKind DashboardDatasourceVariableKind + if err := json.Unmarshal(raw, &dashboardDatasourceVariableKind); err != nil { + return err + } + + resource.DatasourceVariableKind = &dashboardDatasourceVariableKind + return nil + case "GroupByVariable": + var dashboardGroupByVariableKind DashboardGroupByVariableKind + if err := json.Unmarshal(raw, &dashboardGroupByVariableKind); err != nil { + return err + } + + resource.GroupByVariableKind = &dashboardGroupByVariableKind + return nil + case "IntervalVariable": + var dashboardIntervalVariableKind DashboardIntervalVariableKind + if err := json.Unmarshal(raw, &dashboardIntervalVariableKind); err != nil { + return err + } + + resource.IntervalVariableKind = &dashboardIntervalVariableKind + return nil + case "QueryVariable": + var dashboardQueryVariableKind DashboardQueryVariableKind + if err := json.Unmarshal(raw, &dashboardQueryVariableKind); err != nil { + return err + } + + resource.QueryVariableKind = &dashboardQueryVariableKind + return nil + case "TextVariable": + var dashboardTextVariableKind DashboardTextVariableKind + if err := json.Unmarshal(raw, &dashboardTextVariableKind); err != nil { + return err + } + + resource.TextVariableKind = &dashboardTextVariableKind + return nil + } + + return nil +} + +// +k8s:openapi-gen=true +type DashboardStringOrArrayOfString struct { + String *string `json:"String,omitempty"` + ArrayOfString []string `json:"ArrayOfString,omitempty"` +} + +// NewDashboardStringOrArrayOfString creates a new DashboardStringOrArrayOfString object. +func NewDashboardStringOrArrayOfString() *DashboardStringOrArrayOfString { + return &DashboardStringOrArrayOfString{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardStringOrArrayOfString` as JSON. +func (resource DashboardStringOrArrayOfString) MarshalJSON() ([]byte, error) { + if resource.String != nil { + return json.Marshal(resource.String) + } + + if resource.ArrayOfString != nil { + return json.Marshal(resource.ArrayOfString) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardStringOrArrayOfString` from JSON. +func (resource *DashboardStringOrArrayOfString) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + var errList []error + + // String + var String string + if err := json.Unmarshal(raw, &String); err != nil { + errList = append(errList, err) + resource.String = nil + } else { + resource.String = &String + return nil + } + + // ArrayOfString + var ArrayOfString []string + if err := json.Unmarshal(raw, &ArrayOfString); err != nil { + errList = append(errList, err) + resource.ArrayOfString = nil + } else { + resource.ArrayOfString = ArrayOfString + return nil + } + + return errors.Join(errList...) +} + +// +k8s:openapi-gen=true +type DashboardStringOrFloat64 struct { + String *string `json:"String,omitempty"` + Float64 *float64 `json:"Float64,omitempty"` +} + +// NewDashboardStringOrFloat64 creates a new DashboardStringOrFloat64 object. +func NewDashboardStringOrFloat64() *DashboardStringOrFloat64 { + return &DashboardStringOrFloat64{} +} + +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardStringOrFloat64` as JSON. +func (resource DashboardStringOrFloat64) MarshalJSON() ([]byte, error) { + if resource.String != nil { + return json.Marshal(resource.String) + } + + if resource.Float64 != nil { + return json.Marshal(resource.Float64) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardStringOrFloat64` from JSON. +func (resource *DashboardStringOrFloat64) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + + var errList []error + + // String + var String string + if err := json.Unmarshal(raw, &String); err != nil { + errList = append(errList, err) + resource.String = nil + } else { + resource.String = &String + return nil + } + + // Float64 + var Float64 float64 + if err := json.Unmarshal(raw, &Float64); err != nil { + errList = append(errList, err) + resource.Float64 = nil + } else { + resource.Float64 = &Float64 + return nil + } + + return errors.Join(errList...) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_status_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_status_gen.go new file mode 100644 index 00000000000..322f9f072ec --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/dashboard_status_gen.go @@ -0,0 +1,34 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v2alpha2 + +// ConversionStatus is the status of the conversion of the dashboard. +// +k8s:openapi-gen=true +type DashboardConversionStatus struct { + // Whether from another version has failed. + // If true, means that the dashboard is not valid, + // and the caller should instead fetch the stored version. + Failed bool `json:"failed"` + // The version which was stored when the dashboard was created / updated. + // Fetching this version should always succeed. + StoredVersion string `json:"storedVersion"` + // The error message from the conversion. + // Empty if the conversion has not failed. + Error string `json:"error"` +} + +// NewDashboardConversionStatus creates a new DashboardConversionStatus object. +func NewDashboardConversionStatus() *DashboardConversionStatus { + return &DashboardConversionStatus{} +} + +// +k8s:openapi-gen=true +type DashboardStatus struct { + // Optional conversion status. + Conversion *DashboardConversionStatus `json:"conversion,omitempty"` +} + +// NewDashboardStatus creates a new DashboardStatus object. +func NewDashboardStatus() *DashboardStatus { + return &DashboardStatus{} +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/doc.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/doc.go new file mode 100644 index 00000000000..95dba55d4cd --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/doc.go @@ -0,0 +1,10 @@ +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta +// +k8s:conversion-gen=github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard +// +groupName=dashboard.grafana.app + +// NOTE (@radiohead): we do not use package-wide deepcopy generation +// because grafana-app-sdk already provides deepcopy functions. +// Kinds which are not generated by the SDK are explicitly opted in to deepcopy generation. + +package v2alpha2 // import "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/register.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/register.go new file mode 100644 index 00000000000..0a68d910fb1 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/register.go @@ -0,0 +1,103 @@ +package v2alpha2 + +import ( + "fmt" + "time" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + GROUP = "dashboard.grafana.app" + VERSION = "v2alpha2" + APIVERSION = GROUP + "/" + VERSION +) + +var DashboardResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "dashboards", "dashboard", "Dashboard", + func() runtime.Object { return &Dashboard{} }, + func() runtime.Object { return &DashboardList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The dashboard name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + dash, ok := obj.(*Dashboard) + if ok { + if dash != nil { + return []interface{}{ + dash.Name, + dash.Spec.Title, + dash.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected dashboard") + }, + }, +) + +var LibraryPanelResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "librarypanels", "librarypanel", "LibraryPanel", + func() runtime.Object { return &LibraryPanel{} }, + func() runtime.Object { return &LibraryPanelList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Description: "The dashboard name"}, + {Name: "Type", Type: "string", Description: "the panel type"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + panel, ok := obj.(*LibraryPanel) + if ok { + if panel != nil { + return []interface{}{ + panel.Name, + panel.Spec.Title, + panel.Spec.Type, + panel.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected library panel") + }, + }, +) + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme + schemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} +) + +func init() { + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(schemeGroupVersion, + &Dashboard{}, + &DashboardList{}, + &DashboardWithAccessInfo{}, + &DashboardVersionList{}, + &VersionsQueryOptions{}, + &LibraryPanel{}, + &LibraryPanelList{}, + &metav1.PartialObjectMetadata{}, + &metav1.PartialObjectMetadataList{}, + ) + metav1.AddToGroupVersion(scheme, schemeGroupVersion) + return nil +} + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/types.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/types.go new file mode 100644 index 00000000000..b40a86fe94e --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/types.go @@ -0,0 +1,149 @@ +package v2alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DashboardVersionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []DashboardVersionInfo `json:"items"` +} + +// +k8s:deepcopy-gen=true +type DashboardVersionInfo struct { + // The internal ID for this version (will be replaced with resourceVersion) + Version int `json:"version"` + + // If the dashboard came from a previous version, it is set here + ParentVersion int `json:"parentVersion,omitempty"` + + // The creation timestamp for this version + Created int64 `json:"created"` + + // The user who created this version + CreatedBy string `json:"createdBy,omitempty"` + + // Message passed while saving the version + Message string `json:"message,omitempty"` +} + +// +k8s:deepcopy-gen=true +// +k8s:conversion-gen:explicit-from=net/url.Values +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type VersionsQueryOptions struct { + metav1.TypeMeta `json:",inline"` + + // Path is the URL path + // +optional + Path string `json:"path,omitempty"` + + // +optional + Version int64 `json:"version,omitempty"` +} + +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LibraryPanel struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Panel properties + Spec LibraryPanelSpec `json:"spec"` + + // Status will show errors + Status *LibraryPanelStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LibraryPanelList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LibraryPanel `json:"items"` +} + +// +k8s:deepcopy-gen=true +type LibraryPanelSpec struct { + // The panel type + Type string `json:"type"` + + // The panel type + PluginVersion string `json:"pluginVersion,omitempty"` + + // The panel title + Title string `json:"title,omitempty"` + + // Library panel description + Description string `json:"description,omitempty"` + + // The options schema depends on the panel type + Options common.Unstructured `json:"options"` + + // The fieldConfig schema depends on the panel type + FieldConfig common.Unstructured `json:"fieldConfig"` + + // The default datasource type + Datasource *data.DataSourceRef `json:"datasource,omitempty"` + + // The datasource queries + // +listType=set + Targets []data.DataQuery `json:"targets,omitempty"` +} + +// +k8s:deepcopy-gen=true +type LibraryPanelStatus struct { + // Translation warnings (mostly things that were in SQL columns but not found in the saved body) + Warnings []string `json:"warnings,omitempty"` + + // The properties previously stored in SQL that are not included in this model + Missing common.Unstructured `json:"missing,omitempty"` +} + +// This is like the legacy DTO where access and metadata are all returned in a single call +// +k8s:deepcopy-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DashboardWithAccessInfo struct { + Dashboard `json:",inline"` + + Access DashboardAccess `json:"access"` +} + +// Information about how the requesting user can use a given dashboard +// +k8s:deepcopy-gen=true +type DashboardAccess struct { + // Metadata fields + Slug string `json:"slug,omitempty"` + Url string `json:"url,omitempty"` + + // The permissions part + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanStar bool `json:"canStar"` + CanDelete bool `json:"canDelete"` + AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"` +} + +// +k8s:deepcopy-gen=true +type AnnotationPermission struct { + Dashboard AnnotationActions `json:"dashboard"` + Organization AnnotationActions `json:"organization"` +} + +// +k8s:deepcopy-gen=true +type AnnotationActions struct { + CanAdd bool `json:"canAdd"` + CanEdit bool `json:"canEdit"` + CanDelete bool `json:"canDelete"` +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/validation.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/validation.go new file mode 100644 index 00000000000..9b01c53a3e1 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/validation.go @@ -0,0 +1,84 @@ +package v2alpha2 + +import ( + _ "embed" + json "encoding/json" + fmt "fmt" + "strings" + "sync" + + "k8s.io/apimachinery/pkg/util/validation/field" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/errors" + cuejson "cuelang.org/go/encoding/json" +) + +func ValidateDashboardSpec(obj *Dashboard) field.ErrorList { + data, err := json.Marshal(obj.Spec) + if err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()), + } + } + + if err := cuejson.Validate(data, getCueSchema()); err != nil { + errs := field.ErrorList{} + + for _, e := range errors.Errors(err) { + if + // We don't want to return confusing "empty disjunction" errors, + // because the users don't necessarily understand what to do with them. + // For empty disjunctions, CUE will also return more specific errors, + // so we can safely ignore the generic ones. + strings.Contains(e.Error(), "disjunction") || + // We don't want to return errors about unknown fields either. + strings.Contains(e.Error(), "field not allowed") { + continue + } + + if strings.Contains(e.Error(), "mismatched types null and list") { + // Go populates empty slices as nil, which the cue validator does not like + continue + } + + // We want to manually format the error message, + // because e.Error() contains the full CUE path. + format, args := e.Msg() + + errs = append(errs, field.Invalid( + field.NewPath(formatErrorPath(e.Path())), + field.OmitValueType{}, + fmt.Sprintf(format, args...), + )) + } + + return errs + } + + return nil +} + +func formatErrorPath(path []string) string { + return strings.Join(path, ".") +} + +var ( + compiledSchema cue.Value + getSchemaOnce sync.Once +) + +//go:embed dashboard_spec.cue +var schemaSource string + +func getCueSchema() cue.Value { + getSchemaOnce.Do(func() { + cueCtx := cuecontext.New() + compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + cue.ParsePath("DashboardSpec"), + ) + }) + + return compiledSchema +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go new file mode 100644 index 00000000000..3159ff2e0f7 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.conversion.go @@ -0,0 +1,175 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by conversion-gen. DO NOT EDIT. + +package v2alpha2 + +import ( + url "net/url" + unsafe "unsafe" + + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*AnnotationActions)(nil), (*dashboard.AnnotationActions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(a.(*AnnotationActions), b.(*dashboard.AnnotationActions), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*dashboard.AnnotationActions)(nil), (*AnnotationActions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(a.(*dashboard.AnnotationActions), b.(*AnnotationActions), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*AnnotationPermission)(nil), (*dashboard.AnnotationPermission)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(a.(*AnnotationPermission), b.(*dashboard.AnnotationPermission), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*dashboard.AnnotationPermission)(nil), (*AnnotationPermission)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(a.(*dashboard.AnnotationPermission), b.(*AnnotationPermission), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*DashboardAccess)(nil), (*dashboard.DashboardAccess)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(a.(*DashboardAccess), b.(*dashboard.DashboardAccess), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*dashboard.DashboardAccess)(nil), (*DashboardAccess)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(a.(*dashboard.DashboardAccess), b.(*DashboardAccess), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VersionsQueryOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_url_Values_To_v2alpha2_VersionsQueryOptions(a.(*url.Values), b.(*VersionsQueryOptions), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(in *AnnotationActions, out *dashboard.AnnotationActions, s conversion.Scope) error { + out.CanAdd = in.CanAdd + out.CanEdit = in.CanEdit + out.CanDelete = in.CanDelete + return nil +} + +// Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions is an autogenerated conversion function. +func Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(in *AnnotationActions, out *dashboard.AnnotationActions, s conversion.Scope) error { + return autoConvert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(in, out, s) +} + +func autoConvert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(in *dashboard.AnnotationActions, out *AnnotationActions, s conversion.Scope) error { + out.CanAdd = in.CanAdd + out.CanEdit = in.CanEdit + out.CanDelete = in.CanDelete + return nil +} + +// Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions is an autogenerated conversion function. +func Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(in *dashboard.AnnotationActions, out *AnnotationActions, s conversion.Scope) error { + return autoConvert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(in, out, s) +} + +func autoConvert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(in *AnnotationPermission, out *dashboard.AnnotationPermission, s conversion.Scope) error { + if err := Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(&in.Dashboard, &out.Dashboard, s); err != nil { + return err + } + if err := Convert_v2alpha2_AnnotationActions_To_dashboard_AnnotationActions(&in.Organization, &out.Organization, s); err != nil { + return err + } + return nil +} + +// Convert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission is an autogenerated conversion function. +func Convert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(in *AnnotationPermission, out *dashboard.AnnotationPermission, s conversion.Scope) error { + return autoConvert_v2alpha2_AnnotationPermission_To_dashboard_AnnotationPermission(in, out, s) +} + +func autoConvert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(in *dashboard.AnnotationPermission, out *AnnotationPermission, s conversion.Scope) error { + if err := Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(&in.Dashboard, &out.Dashboard, s); err != nil { + return err + } + if err := Convert_dashboard_AnnotationActions_To_v2alpha2_AnnotationActions(&in.Organization, &out.Organization, s); err != nil { + return err + } + return nil +} + +// Convert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission is an autogenerated conversion function. +func Convert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(in *dashboard.AnnotationPermission, out *AnnotationPermission, s conversion.Scope) error { + return autoConvert_dashboard_AnnotationPermission_To_v2alpha2_AnnotationPermission(in, out, s) +} + +func autoConvert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { + out.Slug = in.Slug + out.Url = in.Url + out.CanSave = in.CanSave + out.CanEdit = in.CanEdit + out.CanAdmin = in.CanAdmin + out.CanStar = in.CanStar + out.CanDelete = in.CanDelete + out.AnnotationsPermissions = (*dashboard.AnnotationPermission)(unsafe.Pointer(in.AnnotationsPermissions)) + return nil +} + +// Convert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess is an autogenerated conversion function. +func Convert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error { + return autoConvert_v2alpha2_DashboardAccess_To_dashboard_DashboardAccess(in, out, s) +} + +func autoConvert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { + out.Slug = in.Slug + out.Url = in.Url + out.CanSave = in.CanSave + out.CanEdit = in.CanEdit + out.CanAdmin = in.CanAdmin + out.CanStar = in.CanStar + out.CanDelete = in.CanDelete + out.AnnotationsPermissions = (*AnnotationPermission)(unsafe.Pointer(in.AnnotationsPermissions)) + return nil +} + +// Convert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess is an autogenerated conversion function. +func Convert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error { + return autoConvert_dashboard_DashboardAccess_To_v2alpha2_DashboardAccess(in, out, s) +} + +func autoConvert_url_Values_To_v2alpha2_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { + // WARNING: Field TypeMeta does not have json tag, skipping. + + if values, ok := map[string][]string(*in)["path"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_string(&values, &out.Path, s); err != nil { + return err + } + } else { + out.Path = "" + } + if values, ok := map[string][]string(*in)["version"]; ok && len(values) > 0 { + if err := runtime.Convert_Slice_string_To_int64(&values, &out.Version, s); err != nil { + return err + } + } else { + out.Version = 0 + } + return nil +} + +// Convert_url_Values_To_v2alpha2_VersionsQueryOptions is an autogenerated conversion function. +func Convert_url_Values_To_v2alpha2_VersionsQueryOptions(in *url.Values, out *VersionsQueryOptions, s conversion.Scope) error { + return autoConvert_url_Values_To_v2alpha2_VersionsQueryOptions(in, out, s) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.deepcopy.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.deepcopy.go new file mode 100644 index 00000000000..9a8a05f7f22 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.deepcopy.go @@ -0,0 +1,283 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v2alpha2 + +import ( + v0alpha1 "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnnotationActions) DeepCopyInto(out *AnnotationActions) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotationActions. +func (in *AnnotationActions) DeepCopy() *AnnotationActions { + if in == nil { + return nil + } + out := new(AnnotationActions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnnotationPermission) DeepCopyInto(out *AnnotationPermission) { + *out = *in + out.Dashboard = in.Dashboard + out.Organization = in.Organization + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotationPermission. +func (in *AnnotationPermission) DeepCopy() *AnnotationPermission { + if in == nil { + return nil + } + out := new(AnnotationPermission) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DashboardAccess) DeepCopyInto(out *DashboardAccess) { + *out = *in + if in.AnnotationsPermissions != nil { + in, out := &in.AnnotationsPermissions, &out.AnnotationsPermissions + *out = new(AnnotationPermission) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardAccess. +func (in *DashboardAccess) DeepCopy() *DashboardAccess { + if in == nil { + return nil + } + out := new(DashboardAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DashboardVersionInfo) DeepCopyInto(out *DashboardVersionInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionInfo. +func (in *DashboardVersionInfo) DeepCopy() *DashboardVersionInfo { + if in == nil { + return nil + } + out := new(DashboardVersionInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DashboardVersionList) DeepCopyInto(out *DashboardVersionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DashboardVersionInfo, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardVersionList. +func (in *DashboardVersionList) DeepCopy() *DashboardVersionList { + if in == nil { + return nil + } + out := new(DashboardVersionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DashboardVersionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DashboardWithAccessInfo) DeepCopyInto(out *DashboardWithAccessInfo) { + *out = *in + in.Dashboard.DeepCopyInto(&out.Dashboard) + in.Access.DeepCopyInto(&out.Access) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardWithAccessInfo. +func (in *DashboardWithAccessInfo) DeepCopy() *DashboardWithAccessInfo { + if in == nil { + return nil + } + out := new(DashboardWithAccessInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DashboardWithAccessInfo) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LibraryPanel) DeepCopyInto(out *LibraryPanel) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(LibraryPanelStatus) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanel. +func (in *LibraryPanel) DeepCopy() *LibraryPanel { + if in == nil { + return nil + } + out := new(LibraryPanel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LibraryPanel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LibraryPanelList) DeepCopyInto(out *LibraryPanelList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LibraryPanel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanelList. +func (in *LibraryPanelList) DeepCopy() *LibraryPanelList { + if in == nil { + return nil + } + out := new(LibraryPanelList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LibraryPanelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LibraryPanelSpec) DeepCopyInto(out *LibraryPanelSpec) { + *out = *in + in.Options.DeepCopyInto(&out.Options) + in.FieldConfig.DeepCopyInto(&out.FieldConfig) + if in.Datasource != nil { + in, out := &in.Datasource, &out.Datasource + *out = new(v0alpha1.DataSourceRef) + **out = **in + } + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]v0alpha1.DataQuery, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanelSpec. +func (in *LibraryPanelSpec) DeepCopy() *LibraryPanelSpec { + if in == nil { + return nil + } + out := new(LibraryPanelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LibraryPanelStatus) DeepCopyInto(out *LibraryPanelStatus) { + *out = *in + if in.Warnings != nil { + in, out := &in.Warnings, &out.Warnings + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Missing.DeepCopyInto(&out.Missing) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LibraryPanelStatus. +func (in *LibraryPanelStatus) DeepCopy() *LibraryPanelStatus { + if in == nil { + return nil + } + out := new(LibraryPanelStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VersionsQueryOptions) DeepCopyInto(out *VersionsQueryOptions) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionsQueryOptions. +func (in *VersionsQueryOptions) DeepCopy() *VersionsQueryOptions { + if in == nil { + return nil + } + out := new(VersionsQueryOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VersionsQueryOptions) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/apis/secret/v0alpha1/zz_generated.defaults.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.defaults.go similarity index 96% rename from pkg/apis/secret/v0alpha1/zz_generated.defaults.go rename to apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.defaults.go index 238fc2f4edc..efead5bdea7 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.defaults.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.defaults.go @@ -5,7 +5,7 @@ // Code generated by defaulter-gen. DO NOT EDIT. -package v0alpha1 +package v2alpha2 import ( runtime "k8s.io/apimachinery/pkg/runtime" diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi.go new file mode 100644 index 00000000000..7ed5e3983d4 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi.go @@ -0,0 +1,5094 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +package v2alpha2 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions": schema_pkg_apis_dashboard_v2alpha2_AnnotationActions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha2_AnnotationPermission(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.Dashboard": schema_pkg_apis_dashboard_v2alpha2_Dashboard(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAccess": schema_pkg_apis_dashboard_v2alpha2_DashboardAccess(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha2_DashboardAdHocFilterWithLabels(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationPanelFilter(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha2_DashboardConversionStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha2_DashboardDashboardLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha2_DashboardDataLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha2_DashboardDataQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha2_DashboardDataSourceRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardDataTransformerConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha2_DashboardDynamicConfigValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha2_DashboardElementReference(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha2_DashboardFieldColor(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha2_DashboardJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKindSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardList": schema_pkg_apis_dashboard_v2alpha2_DashboardList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardMatcherConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha2_DashboardMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha2_DashboardMetricFindValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKindOrLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryOptionsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha2_DashboardRangeMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha2_DashboardRegexMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardRowRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha2_DashboardSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus": schema_pkg_apis_dashboard_v2alpha2_DashboardStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrArrayOfString(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrFloat64(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardTabRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha2_DashboardThreshold(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha2_DashboardThresholdsConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha2_DashboardTimeRangeOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardTimeSettingsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha2_DashboardTransformationKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverrides(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RangeMapOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RangeMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RegexMapOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RegexMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2SpecialValueMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha2_DashboardValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha2_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha2_DashboardValueMappingResult(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha2_DashboardVariableOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha2_DashboardVersionInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha2_DashboardVersionList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha2_DashboardWithAccessInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanel": schema_pkg_apis_dashboard_v2alpha2_LibraryPanel(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha2_LibraryPanelList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha2_LibraryPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha2_LibraryPanelStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha2_VersionsQueryOptions(ref), + } +} + +func schema_pkg_apis_dashboard_v2alpha2_AnnotationActions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "canAdd": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "canEdit": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "canDelete": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"canAdd", "canEdit", "canDelete"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_AnnotationPermission(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "dashboard": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions"), + }, + }, + "organization": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions"), + }, + }, + }, + Required: []string{"dashboard", "organization"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationActions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_Dashboard(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Dashboard", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAccess(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Information about how the requesting user can use a given dashboard", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "slug": { + SchemaProps: spec.SchemaProps{ + Description: "Metadata fields", + Type: []string{"string"}, + Format: "", + }, + }, + "url": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "canSave": { + SchemaProps: spec.SchemaProps{ + Description: "The permissions part", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "canEdit": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "canAdmin": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "canStar": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "canDelete": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "annotationsPermissions": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationPermission"), + }, + }, + }, + Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.AnnotationPermission"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAdHocFilterWithLabels(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Define the AdHocFilterWithLabels type", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "operator": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "values": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "keyLabel": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "valueLabels": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "forceEdit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "origin": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "condition": { + SchemaProps: spec.SchemaProps{ + Description: "@deprecated", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"key", "operator", "value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Adhoc variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAdhocVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Adhoc variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), + }, + }, + "baseFilters": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels"), + }, + }, + }, + }, + }, + "filters": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels"), + }, + }, + }, + }, + }, + "defaultKeys": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetricFindValue"), + }, + }, + }, + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "allowCustomValue": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"name", "baseFilters", "filters", "defaultKeys", "hide", "skipUrlSync", "allowCustomValue"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdHocFilterWithLabels", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMetricFindValue"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationPanelFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "exclude": { + SchemaProps: spec.SchemaProps{ + Description: "Should the specified panels be included or excluded", + Type: []string{"boolean"}, + Format: "", + }, + }, + "ids": { + SchemaProps: spec.SchemaProps{ + Description: "Panel IDs that should be included or excluded", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + }, + Required: []string{"ids"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQuerySpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQuerySpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAnnotationQuerySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind"), + }, + }, + "enable": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "iconColor": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "builtIn": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "filter": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationPanelFilter"), + }, + }, + "legacyOptions": { + SchemaProps: spec.SchemaProps{ + Description: "Catch-all field for datasource-specific properties", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"enable", "hide", "iconColor", "name"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationPanelFilter", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "element": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference"), + }, + }, + "repeat": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridRepeatOptions"), + }, + }, + "conditionalRendering": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind"), + }, + }, + }, + Required: []string{"element"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridRepeatOptions", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "maxColumnCount": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + Format: "double", + }, + }, + "columnWidthMode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "columnWidth": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + Format: "double", + }, + }, + "rowHeightMode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "rowHeight": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + Format: "double", + }, + }, + "fillScreen": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemKind"), + }, + }, + }, + }, + }, + }, + Required: []string{"columnWidthMode", "rowHeightMode", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutItemKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardAutoGridRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"mode", "value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingDataSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "value": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "visibility": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "condition": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"), + }, + }, + }, + }, + }, + }, + Required: []string{"visibility", "condition", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingTimeRangeSizeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "ConditionalRenderingVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKind"), + }, + }, + "ConditionalRenderingDataKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataKind"), + }, + }, + "ConditionalRenderingTimeRangeSizeKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeKind"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingDataKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingTimeRangeSizeKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingVariableKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConditionalRenderingVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "variable": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "operator": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"variable", "operator", "value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Constant variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConstantVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Constant variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "query", "current", "hide", "skipUrlSync"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardConversionStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ConversionStatus is the status of the conversion of the dashboard.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "failed": { + SchemaProps: spec.SchemaProps{ + Description: "Whether from another version has failed. If true, means that the dashboard is not valid, and the caller should instead fetch the stored version.", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "storedVersion": { + SchemaProps: spec.SchemaProps{ + Description: "The version which was stored when the dashboard was created / updated. Fetching this version should always succeed.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "error": { + SchemaProps: spec.SchemaProps{ + Description: "The error message from the conversion. Empty if the conversion has not failed.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"failed", "storedVersion", "error"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Custom variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardCustomVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Custom variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + }, + }, + }, + "multi": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "includeAll": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "allValue": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "allowCustomValue": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDashboardLink(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Links with references to other dashboards or external resources", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Description: "Title to display with the link", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType`", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "icon": { + SchemaProps: spec.SchemaProps{ + Description: "Icon name to be displayed with the link", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "tooltip": { + SchemaProps: spec.SchemaProps{ + Description: "Tooltip to display when the user hovers their mouse over it", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "url": { + SchemaProps: spec.SchemaProps{ + Description: "Link URL. Only required/valid if the type is link", + Type: []string{"string"}, + Format: "", + }, + }, + "tags": { + SchemaProps: spec.SchemaProps{ + Description: "List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "asDropdown": { + SchemaProps: spec.SchemaProps{ + Description: "If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "targetBlank": { + SchemaProps: spec.SchemaProps{ + Description: "If true, the link will be opened in a new tab", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "includeVars": { + SchemaProps: spec.SchemaProps{ + Description: "If true, includes current template variables values in the link as query params", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "keepTime": { + SchemaProps: spec.SchemaProps{ + Description: "If true, includes current time range in the link as query params", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"title", "type", "icon", "tooltip", "tags", "asDropdown", "targetBlank", "includeVars", "keepTime"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDataLink(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "url": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "targetBlank": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"title", "url"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDataQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "The kind of a DataQueryKind is the datasource type", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDataSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "The plugin type-id", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "Specific datasource instance", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDataTransformerConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Transformations allow to manipulate data returned by a query before the system applies a visualization. Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, use the output of one transformation as the input to another transformation, etc.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + SchemaProps: spec.SchemaProps{ + Description: "Unique identifier of transformer", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disabled": { + SchemaProps: spec.SchemaProps{ + Description: "Disabled transformations are skipped", + Type: []string{"boolean"}, + Format: "", + }, + }, + "filter": { + SchemaProps: spec.SchemaProps{ + Description: "Optional frame matcher. When missing it will be applied to all results", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"), + }, + }, + "topic": { + SchemaProps: spec.SchemaProps{ + Description: "Where to pull DataFrames from as input to transformation", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Description: "Options to be passed to the transformer Valid options depend on the transformer id", + Type: []string{"object"}, + Format: "", + }, + }, + }, + Required: []string{"id", "options"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Datasource variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDatasourceVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Datasource variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "pluginId": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "refresh": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "regex": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + }, + }, + }, + "multi": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "includeAll": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "allValue": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "allowCustomValue": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"name", "pluginId", "refresh", "regex", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardDynamicConfigValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + Required: []string{"id"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardElementReference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"kind", "name"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldColor(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Map a field to a color.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Description: "The main color scheme mode.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "fixedColor": { + SchemaProps: spec.SchemaProps{ + Description: "The fixed color value for fixed or shades color modes.", + Type: []string{"string"}, + Format: "", + }, + }, + "seriesBy": { + SchemaProps: spec.SchemaProps{ + Description: "Some visualizations need to know how to assign a series color from by value color schemes.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"mode"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a field. A field can represent a single time series or table column. Field options allow you to change how the data is displayed in your visualizations.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "displayName": { + SchemaProps: spec.SchemaProps{ + Description: "The display value for this field. This supports template variables blank is auto", + Type: []string{"string"}, + Format: "", + }, + }, + "displayNameFromDS": { + SchemaProps: spec.SchemaProps{ + Description: "This can be used by data sources that return and explicit naming structure for values and labels When this property is configured, this value is used rather than the default naming strategy.", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Human readable field metadata", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "An explicit path to the field in the datasource. When the frame meta includes a path, This will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and may be used to update the results", + Type: []string{"string"}, + Format: "", + }, + }, + "writeable": { + SchemaProps: spec.SchemaProps{ + Description: "True if data source can write a value to the path. Auth/authz are supported separately", + Type: []string{"boolean"}, + Format: "", + }, + }, + "filterable": { + SchemaProps: spec.SchemaProps{ + Description: "True if data source field supports ad-hoc filters", + Type: []string{"boolean"}, + Format: "", + }, + }, + "unit": { + SchemaProps: spec.SchemaProps{ + Description: "Unit a field should use. The unit you select is applied to all fields except time. You can use the units ID availables in Grafana or a custom unit. Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts As custom unit, you can use the following formats: `suffix:` for custom unit that should go after value. `prefix:` for custom unit that should go before value. `time:` For custom date time formats type for example `time:YYYY-MM-DD`. `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. `count:` for a custom count unit. `currency:` for custom a currency unit.", + Type: []string{"string"}, + Format: "", + }, + }, + "decimals": { + SchemaProps: spec.SchemaProps{ + Description: "Specify the number of decimals Grafana includes in the rendered value. If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. For example 1.1234 will display as 1.12 and 100.456 will display as 100. To display all decimals, set the unit to `String`.", + Type: []string{"number"}, + Format: "double", + }, + }, + "min": { + SchemaProps: spec.SchemaProps{ + Description: "The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", + Type: []string{"number"}, + Format: "double", + }, + }, + "max": { + SchemaProps: spec.SchemaProps{ + Description: "The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.", + Type: []string{"number"}, + Format: "double", + }, + }, + "mappings": { + SchemaProps: spec.SchemaProps{ + Description: "Convert input values into a display string", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap"), + }, + }, + }, + }, + }, + "thresholds": { + SchemaProps: spec.SchemaProps{ + Description: "Map numeric values to states", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThresholdsConfig"), + }, + }, + "color": { + SchemaProps: spec.SchemaProps{ + Description: "Panel color configuration", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldColor"), + }, + }, + "links": { + SchemaProps: spec.SchemaProps{ + Description: "The behavior when clicking on a result", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "noValue": { + SchemaProps: spec.SchemaProps{ + Description: "Alternative to empty string", + Type: []string{"string"}, + Format: "", + }, + }, + "custom": { + SchemaProps: spec.SchemaProps{ + Description: "custom is specified by the FieldConfig field in panel plugin schemas.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldColor", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThresholdsConfig", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardFieldConfigSource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. Each column within this structure is called a field. A field can represent a single time series or table column. Field options allow you to change how the data is displayed in your visualizations.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "defaults": { + SchemaProps: spec.SchemaProps{ + Description: "Defaults are the options applied to all fields.", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfig"), + }, + }, + "overrides": { + SchemaProps: spec.SchemaProps{ + Description: "Overrides are the options applied to specific fields overriding the defaults.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2FieldConfigSourceOverrides"), + }, + }, + }, + }, + }, + }, + Required: []string{"defaults", "overrides"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfig", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2FieldConfigSourceOverrides"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutItemSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "x": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "y": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "width": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "height": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "element": { + SchemaProps: spec.SchemaProps{ + Description: "reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference"), + }, + }, + "repeat": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRepeatOptions"), + }, + }, + }, + Required: []string{"x", "y", "width", "height", "element"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardElementReference", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRepeatOptions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "GridLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind"), + }, + }, + "AutoGridLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind"), + }, + }, + "TabsLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"), + }, + }, + "RowsLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "GridLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind"), + }, + }, + "RowsLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind"), + }, + }, + "AutoGridLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind"), + }, + }, + "TabsLayoutKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAutoGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGridLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemKind"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutItemKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Group variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardGroupByVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupBy variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), + }, + }, + "defaultValue": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + }, + }, + }, + "multi": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "current", "options", "multi", "hide", "skipUrlSync"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Interval variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardIntervalVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Interval variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + }, + }, + }, + "auto": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "auto_min": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "auto_count": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "refresh": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "query", "current", "options", "auto", "auto_min", "auto_count", "refresh", "hide", "skipUrlSync"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardJSONCodec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DashboardJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding", + Type: []string{"object"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKindSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKindSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelKindSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + SchemaProps: spec.SchemaProps{ + Description: "Panel ID for the library panel in the dashboard", + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Description: "Title for the library panel in the dashboard", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "libraryPanel": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelRef"), + }, + }, + }, + Required: []string{"id", "title", "libraryPanel"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelRef"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardLibraryPanelRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "A library panel is a reusable panel that you can use in any dashboard. When you make a change to a library panel, that change propagates to all instances of where the panel is used. Library panels streamline reuse of panels across multiple dashboards.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Library panel name", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "Library panel uid", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "uid"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.Dashboard"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.Dashboard", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardMatcherConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + SchemaProps: spec.SchemaProps{ + Description: "The matcher id. This is used to find the matcher implementation from registry.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Description: "The matcher options. This is specific to the matcher implementation.", + Type: []string{"object"}, + Format: "", + }, + }, + }, + Required: []string{"id"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "metadata contains embedded CommonMetadata and can be extended with custom string fields without external reference as using the CommonMetadata reference breaks thema codegen.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "updateTimestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "createdBy": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "creationTimestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "deletionTimestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "finalizers": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "generation": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "updatedBy": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "labels": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"updateTimestamp", "createdBy", "uid", "creationTimestamp", "finalizers", "resourceVersion", "generation", "updatedBy", "labels"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardMetricFindValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Define the MetricFindValue type", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "text": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrFloat64"), + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "expandable": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"text"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrFloat64"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelKindOrLibraryPanelKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "PanelKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKind"), + }, + }, + "LibraryPanelKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKind"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQueryKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQuerySpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQuerySpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelQuerySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "query": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind"), + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), + }, + }, + "refId": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "hidden": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"query", "refId", "hidden"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "links": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataLink"), + }, + }, + }, + }, + }, + "data": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupKind"), + }, + }, + "vizConfig": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigKind"), + }, + }, + "transparent": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"id", "title", "description", "links", "data", "vizConfig"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryGroupSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "queries": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQueryKind"), + }, + }, + }, + }, + }, + "transformations": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTransformationKind"), + }, + }, + }, + }, + }, + "queryOptions": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryOptionsSpec"), + }, + }, + }, + Required: []string{"queries", "transformations", "queryOptions"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryOptionsSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTransformationKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryOptionsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "timeFrom": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "maxDataPoints": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", + }, + }, + "timeShift": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "queryCachingTTL": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", + }, + }, + "interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "cacheTimeout": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hideTimeOverride": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Query variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "QueryVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKind"), + }, + }, + "TextVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableKind"), + }, + }, + "ConstantVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableKind"), + }, + }, + "DatasourceVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableKind"), + }, + }, + "IntervalVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableKind"), + }, + }, + "CustomVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableKind"), + }, + }, + "GroupByVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableKind"), + }, + }, + "AdhocVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableKind"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardQueryVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Query variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "refresh": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef"), + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind"), + }, + }, + "regex": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "sort": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "definition": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + }, + }, + }, + "multi": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "includeAll": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "allValue": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "placeholder": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "allowCustomValue": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "staticOptions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + }, + }, + }, + "staticOptionsOrder": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "current", "hide", "refresh", "skipUrlSync", "query", "regex", "sort", "options", "multi", "includeAll", "allowCustomValue"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataSourceRef", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRangeMap(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Description: "Range to match against and the result to apply when the value is within the range", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RangeMapOptions"), + }, + }, + }, + Required: []string{"type", "options"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RangeMapOptions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRegexMap(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Description: "Regular expression to match against and the result to apply when the value matches the regex", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RegexMapOptions"), + }, + }, + }, + Required: []string{"type", "options"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2RegexMapOptions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "direction": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "maxPerRow": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + Required: []string{"mode", "value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRowRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"mode", "value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutRowSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "collapse": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "hideHeader": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "fillScreen": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + "conditionalRendering": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind"), + }, + }, + "repeat": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowRepeatOptions"), + }, + }, + "layout": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind"), + }, + }, + }, + Required: []string{"layout"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowRepeatOptions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardRowsLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "rows": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowKind"), + }, + }, + }, + }, + }, + }, + Required: []string{"rows"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRowsLayoutRowKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "annotations": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQueryKind"), + }, + }, + }, + }, + }, + "cursorSync": { + SchemaProps: spec.SchemaProps{ + Description: "Configuration of dashboard cursor sync behavior. \"Off\" for no shared crosshair or tooltip (default). \"Crosshair\" for shared crosshair. \"Tooltip\" for shared crosshair AND shared tooltip.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Description of dashboard.", + Type: []string{"string"}, + Format: "", + }, + }, + "editable": { + SchemaProps: spec.SchemaProps{ + Description: "Whether a dashboard is editable or not.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "elements": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKindOrLibraryPanelKind"), + }, + }, + }, + }, + }, + "layout": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"), + }, + }, + "links": { + SchemaProps: spec.SchemaProps{ + Description: "Links with references to other dashboards or external websites.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDashboardLink"), + }, + }, + }, + }, + }, + "liveNow": { + SchemaProps: spec.SchemaProps{ + Description: "When set to true, the dashboard will redraw panels at an interval matching the pixel width. This will keep data \"moving left\" regardless of the query refresh rate. This setting helps avoid dashboards presenting stale live data.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "preload": { + SchemaProps: spec.SchemaProps{ + Description: "When set to true, the dashboard will load all panels in the dashboard when it's loaded.", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "revision": { + SchemaProps: spec.SchemaProps{ + Description: "Plugins only. The version of the dashboard installed together with the plugin. This is used to determine if the dashboard should be updated when the plugin is updated.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "tags": { + SchemaProps: spec.SchemaProps{ + Description: "Tags associated with dashboard.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "timeSettings": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeSettingsSpec"), + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Description: "Title of dashboard.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "variables": { + SchemaProps: spec.SchemaProps{ + Description: "Configured template variables.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind"), + }, + }, + }, + }, + }, + }, + Required: []string{"annotations", "cursorSync", "elements", "layout", "links", "preload", "tags", "timeSettings", "title", "variables"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeSettingsSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardSpecialValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2SpecialValueMapOptions"), + }, + }, + }, + Required: []string{"type", "options"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardV2alpha2SpecialValueMapOptions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conversion": { + SchemaProps: spec.SchemaProps{ + Description: "Optional conversion status.", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConversionStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConversionStatus"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrArrayOfString(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "String": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "ArrayOfString": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardStringOrFloat64(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "String": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "Float64": { + SchemaProps: spec.SchemaProps{ + Type: []string{"number"}, + Format: "double", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"mode", "value"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "tabs": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabKind"), + }, + }, + }, + }, + }, + }, + Required: []string{"tabs"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabKind"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabsLayoutTabSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTabsLayoutTabSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "layout": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"), + }, + }, + "conditionalRendering": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind"), + }, + }, + "repeat": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabRepeatOptions"), + }, + }, + }, + Required: []string{"layout"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTabRepeatOptions"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Text variable kind", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTextVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTextVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Text variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"), + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "current", "query", "hide", "skipUrlSync"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVariableOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardThreshold(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "value": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"number"}, + Format: "double", + }, + }, + "color": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"value", "color"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardThresholdsConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "steps": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThreshold"), + }, + }, + }, + }, + }, + }, + Required: []string{"mode", "steps"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardThreshold"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeRangeOption(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "display": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "from": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "to": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"display", "from", "to"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTimeSettingsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Time configuration It defines the default time config for the time picker, the refresh picker for the specific dashboard.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "timezone": { + SchemaProps: spec.SchemaProps{ + Description: "Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".", + Type: []string{"string"}, + Format: "", + }, + }, + "from": { + SchemaProps: spec.SchemaProps{ + Description: "Start time range for dashboard. Accepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "to": { + SchemaProps: spec.SchemaProps{ + Description: "End time range for dashboard. Accepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "autoRefresh": { + SchemaProps: spec.SchemaProps{ + Description: "Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\". v1: refresh", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "autoRefreshIntervals": { + SchemaProps: spec.SchemaProps{ + Description: "Interval options available in the refresh picker dropdown. v1: timepicker.refresh_intervals", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "quickRanges": { + SchemaProps: spec.SchemaProps{ + Description: "Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. v1: timepicker.quick_ranges , not exposed in the UI", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeRangeOption"), + }, + }, + }, + }, + }, + "hideTimepicker": { + SchemaProps: spec.SchemaProps{ + Description: "Whether timepicker is visible or not. v1: timepicker.hidden", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "weekStart": { + SchemaProps: spec.SchemaProps{ + Description: "Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".", + Type: []string{"string"}, + Format: "", + }, + }, + "fiscalYearStartMonth": { + SchemaProps: spec.SchemaProps{ + Description: "The month that the fiscal year starts on. 0 = January, 11 = December", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "nowDelay": { + SchemaProps: spec.SchemaProps{ + Description: "Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. v1: timepicker.nowDelay", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"from", "to", "autoRefresh", "autoRefreshIntervals", "hideTimepicker", "fiscalYearStartMonth"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardTimeRangeOption"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardTransformationKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "The kind of a TransformationKind is the transformation ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataTransformerConfig"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDataTransformerConfig"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2FieldConfigSourceOverrides(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "matcher": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"), + }, + }, + "properties": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDynamicConfigValue"), + }, + }, + }, + }, + }, + }, + Required: []string{"matcher", "properties"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardDynamicConfigValue", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardMatcherConfig"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RangeMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "from": { + SchemaProps: spec.SchemaProps{ + Description: "Min value of the range. It can be null which means -Infinity", + Type: []string{"number"}, + Format: "double", + }, + }, + "to": { + SchemaProps: spec.SchemaProps{ + Description: "Max value of the range. It can be null which means +Infinity", + Type: []string{"number"}, + Format: "double", + }, + }, + "result": { + SchemaProps: spec.SchemaProps{ + Description: "Config to apply when the value is within the range", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + }, + }, + }, + Required: []string{"from", "to", "result"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2RegexMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "pattern": { + SchemaProps: spec.SchemaProps{ + Description: "Regular expression to match against", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "result": { + SchemaProps: spec.SchemaProps{ + Description: "Config to apply when the value matches the regex", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + }, + }, + }, + Required: []string{"pattern", "result"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardV2alpha2SpecialValueMapOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "match": { + SchemaProps: spec.SchemaProps{ + Description: "Special value to match against", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "result": { + SchemaProps: spec.SchemaProps{ + Description: "Config to apply when the value matches the special value", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + }, + }, + }, + Required: []string{"match", "result"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Description: "Map with : ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"), + }, + }, + }, + }, + }, + }, + Required: []string{"type", "options"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMappingResult"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "ValueMap": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMap"), + }, + }, + "RangeMap": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRangeMap"), + }, + }, + "RegexMap": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRegexMap"), + }, + }, + "SpecialValueMap": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpecialValueMap"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRangeMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardRegexMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpecialValueMap", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardValueMap"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardValueMappingResult(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Result used as replacement with text and color when the value matches", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "text": { + SchemaProps: spec.SchemaProps{ + Description: "Text to display when the value matches", + Type: []string{"string"}, + Format: "", + }, + }, + "color": { + SchemaProps: spec.SchemaProps{ + Description: "Text to use when the value matches", + Type: []string{"string"}, + Format: "", + }, + }, + "icon": { + SchemaProps: spec.SchemaProps{ + Description: "Icon to display when the value matches. Only specific visualizations.", + Type: []string{"string"}, + Format: "", + }, + }, + "index": { + SchemaProps: spec.SchemaProps{ + Description: "Position in the mapping array. Only used internally.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardVariableOption(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Variable option specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "selected": { + SchemaProps: spec.SchemaProps{ + Description: "Whether the option is selected or not", + Type: []string{"boolean"}, + Format: "", + }, + }, + "text": { + SchemaProps: spec.SchemaProps{ + Description: "Text to be displayed for the option", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString"), + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "Value of the option", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString"), + }, + }, + }, + Required: []string{"text", "value"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStringOrArrayOfString"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "version": { + SchemaProps: spec.SchemaProps{ + Description: "The internal ID for this version (will be replaced with resourceVersion)", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + "parentVersion": { + SchemaProps: spec.SchemaProps{ + Description: "If the dashboard came from a previous version, it is set here", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "created": { + SchemaProps: spec.SchemaProps{ + Description: "The creation timestamp for this version", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "createdBy": { + SchemaProps: spec.SchemaProps{ + Description: "The user who created this version", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "Message passed while saving the version", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"version", "created"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardVersionList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionInfo"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVersionInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "The kind of a VizConfigKind is the plugin ID", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardVizConfigSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardVizConfigSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "--- Kinds ---", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "pluginVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "fieldConfig": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfigSource"), + }, + }, + }, + Required: []string{"pluginVersion", "options", "fieldConfig"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardFieldConfigSource"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_DashboardWithAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "This is like the legacy DTO where access and metadata are all returned in a single call", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Dashboard", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus"), + }, + }, + "access": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAccess"), + }, + }, + }, + Required: []string{"metadata", "spec", "status", "access"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardAccess", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.DashboardStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_LibraryPanel(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Panel properties", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "Status will show errors", + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelSpec", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanelStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanel"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2.LibraryPanel", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "The panel type", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "pluginVersion": { + SchemaProps: spec.SchemaProps{ + Description: "The panel type", + Type: []string{"string"}, + Format: "", + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Description: "The panel title", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Library panel description", + Type: []string{"string"}, + Format: "", + }, + }, + "options": { + SchemaProps: spec.SchemaProps{ + Description: "The options schema depends on the panel type", + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), + }, + }, + "fieldConfig": { + SchemaProps: spec.SchemaProps{ + Description: "The fieldConfig schema depends on the panel type", + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), + }, + }, + "datasource": { + SchemaProps: spec.SchemaProps{ + Description: "The default datasource type", + Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef"), + }, + }, + "targets": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The datasource queries", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery"), + }, + }, + }, + }, + }, + }, + Required: []string{"type", "options", "fieldConfig"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataQuery", "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.DataSourceRef", "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_LibraryPanelStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "warnings": { + SchemaProps: spec.SchemaProps{ + Description: "Translation warnings (mostly things that were in SQL columns but not found in the saved body)", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "missing": { + SchemaProps: spec.SchemaProps{ + Description: "The properties previously stored in SQL that are not included in this model", + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha2_VersionsQueryOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "Path is the URL path", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + } +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..1db67fbe438 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha2/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,67 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdHocFilterWithLabels,ValueLabels +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdHocFilterWithLabels,Values +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdhocVariableSpec,BaseFilters +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdhocVariableSpec,DefaultKeys +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAdhocVariableSpec,Filters +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAnnotationPanelFilter,Ids +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardAutoGridLayoutSpec,Items +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingGroupSpec,Items +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardCustomVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardDashboardLink,Tags +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardDatasourceVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardFieldConfig,Links +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardFieldConfig,Mappings +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardFieldConfigSource,Overrides +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutSpec,Items +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGroupByVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardIntervalVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardMetadata,Finalizers +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardPanelSpec,Links +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryGroupSpec,Queries +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryGroupSpec,Transformations +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableSpec,Options +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableSpec,StaticOptions +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardRowsLayoutSpec,Rows +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Annotations +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Links +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Tags +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardSpec,Variables +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrArrayOfString,ArrayOfString +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardTabsLayoutSpec,Tabs +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardThresholdsConfig,Steps +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardTimeSettingsSpec,AutoRefreshIntervals +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardTimeSettingsSpec,QuickRanges +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardV2alpha2FieldConfigSourceOverrides,Properties +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,LibraryPanelStatus,Warnings +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingDataKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingTimeRangeSizeKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind,ConditionalRenderingVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,AutoGridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,GridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,RowsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind,TabsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,AutoGridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,GridLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,RowsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind,TabsLayoutKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardIntervalVariableSpec,AutoCount +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardIntervalVariableSpec,AutoMin +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardPanelKindOrLibraryPanelKind,LibraryPanelKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardPanelKindOrLibraryPanelKind,PanelKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,AdhocVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,ConstantVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,CustomVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,DatasourceVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,GroupByVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,IntervalVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,QueryVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,TextVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrArrayOfString,ArrayOfString +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrArrayOfString,String +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrFloat64,Float64 +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardStringOrFloat64,String +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RangeMap +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,RegexMap +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,SpecialValueMap +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap,ValueMap +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2,DashboardList,ListMeta diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 568e229d09d..59f0032ab5f 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -14,6 +14,7 @@ import ( v0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" v1beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" v2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + v2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" ) var appManifestData = app.ManifestData{ @@ -36,6 +37,10 @@ var appManifestData = app.ManifestData{ { Name: "v2alpha1", }, + + { + Name: "v2alpha2", + }, }, }, }, @@ -53,6 +58,7 @@ var kindVersionToGoType = map[string]resource.Kind{ "Dashboard/v0alpha1": v0alpha1.DashboardKind(), "Dashboard/v1beta1": v1beta1.DashboardKind(), "Dashboard/v2alpha1": v2alpha1.DashboardKind(), + "Dashboard/v2alpha2": v2alpha2.DashboardKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index ff9f1c0365c..8fb35a45dbe 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -6,167 +6,77 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/migration" - "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" ) func RegisterConversions(s *runtime.Scheme) error { + // v0 conversions if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_V0_to_V1(a.(*dashv0.Dashboard), b.(*dashv1.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2(a.(*dashv0.Dashboard), b.(*dashv2.Dashboard), scope) + if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V0_to_V2alpha1(a.(*dashv0.Dashboard), b.(*dashv2alpha1.Dashboard), scope) }); err != nil { return err } + if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2alpha2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V0_to_V2alpha2(a.(*dashv0.Dashboard), b.(*dashv2alpha2.Dashboard), scope) + }); err != nil { + return err + } + + // v1 conversions if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_V1_to_V0(a.(*dashv1.Dashboard), b.(*dashv0.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V2(a.(*dashv1.Dashboard), b.(*dashv2.Dashboard), scope) + if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V1_to_V2alpha1(a.(*dashv1.Dashboard), b.(*dashv2alpha1.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*dashv2.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2_to_V0(a.(*dashv2.Dashboard), b.(*dashv0.Dashboard), scope) + if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2alpha2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V1_to_V2alpha2(a.(*dashv1.Dashboard), b.(*dashv2alpha2.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*dashv2.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2_to_V1(a.(*dashv2.Dashboard), b.(*dashv1.Dashboard), scope) + + // v2alpha1 conversions + if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope) }); err != nil { return err } - return nil -} - -func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - out.Spec.Object = in.Spec.Object - - out.Status = dashv1.DashboardStatus{ - Conversion: &dashv1.DashboardConversionStatus{ - StoredVersion: dashv0.VERSION, - }, + if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha1_to_V1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv2alpha2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha1_to_V2alpha2(a.(*dashv2alpha1.Dashboard), b.(*dashv2alpha2.Dashboard), scope) + }); err != nil { + return err } - if err := migration.Migrate(out.Spec.Object, schemaversion.LATEST_VERSION); err != nil { - out.Status.Conversion.Failed = true - out.Status.Conversion.Error = err.Error() - } - - return nil -} - -func Convert_V0_to_V2(in *dashv0.Dashboard, out *dashv2.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - // TODO (@radiohead): implement V0 to V2 conversion - // This is the bare minimum conversion that is needed to make the dashboard servable. - - if v, ok := in.Spec.Object["title"]; ok { - if title, ok := v.(string); ok { - out.Spec.Title = title - } - } - - // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. - out.Spec.Layout = dashv2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ - GridLayoutKind: &dashv2.DashboardGridLayoutKind{ - Kind: "GridLayout", - Spec: dashv2.DashboardGridLayoutSpec{}, - }, - } - - out.Status = dashv2.DashboardStatus{ - Conversion: &dashv2.DashboardConversionStatus{ - StoredVersion: dashv0.VERSION, - Failed: true, - Error: "backend conversion not yet implemented", - }, - } - - return nil -} - -func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - out.Spec.Object = in.Spec.Object - - out.Status = dashv0.DashboardStatus{ - Conversion: &dashv0.DashboardConversionStatus{ - StoredVersion: dashv1.VERSION, - }, - } - - return nil -} - -func Convert_V1_to_V2(in *dashv1.Dashboard, out *dashv2.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - // TODO (@radiohead): implement V1 to V2 conversion - // This is the bare minimum conversion that is needed to make the dashboard servable. - - if v, ok := in.Spec.Object["title"]; ok { - if title, ok := v.(string); ok { - out.Spec.Title = title - } - } - - // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. - out.Spec.Layout = dashv2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ - GridLayoutKind: &dashv2.DashboardGridLayoutKind{ - Kind: "GridLayout", - Spec: dashv2.DashboardGridLayoutSpec{}, - }, - } - - out.Status = dashv2.DashboardStatus{ - Conversion: &dashv2.DashboardConversionStatus{ - StoredVersion: dashv1.VERSION, - Failed: true, - Error: "backend conversion not yet implemented", - }, - } - - return nil -} - -func Convert_V2_to_V0(in *dashv2.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - // TODO: implement V2 to V0 conversion - - out.Status = dashv0.DashboardStatus{ - Conversion: &dashv0.DashboardConversionStatus{ - StoredVersion: dashv2.VERSION, - Failed: true, - Error: "backend conversion not yet implemented", - }, - } - - return nil -} - -func Convert_V2_to_V1(in *dashv2.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { - out.ObjectMeta = in.ObjectMeta - - // TODO: implement V2 to V1 conversion - - out.Status = dashv1.DashboardStatus{ - Conversion: &dashv1.DashboardConversionStatus{ - StoredVersion: dashv2.VERSION, - Failed: true, - Error: "backend conversion not yet implemented", - }, + // v2alpha2 conversions + if err := s.AddConversionFunc((*dashv2alpha2.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha2_to_V0(a.(*dashv2alpha2.Dashboard), b.(*dashv0.Dashboard), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*dashv2alpha2.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha2_to_V1(a.(*dashv2alpha2.Dashboard), b.(*dashv1.Dashboard), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*dashv2alpha2.Dashboard)(nil), (*dashv2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2alpha2_to_V2alpha1(a.(*dashv2alpha2.Dashboard), b.(*dashv2alpha1.Dashboard), scope) + }); err != nil { + return err } return nil diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index 9e51322fee1..0efd0ab6205 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -11,7 +11,8 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" @@ -25,7 +26,8 @@ func TestConversionMatrixExist(t *testing.T) { versions := []v1.Object{ &dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, &dashv1.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV1"}}}, - &dashv2.Dashboard{Spec: dashv2.DashboardSpec{Title: "dashboardV2"}}, + &dashv2alpha1.Dashboard{Spec: dashv2alpha1.DashboardSpec{Title: "dashboardV2alpha1"}}, + &dashv2alpha2.Dashboard{Spec: dashv2alpha2.DashboardSpec{Title: "dashboardV2alpha2"}}, } scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/v0.go b/apps/dashboard/pkg/migration/conversion/v0.go new file mode 100644 index 00000000000..94f3d5b57f4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v0.go @@ -0,0 +1,78 @@ +package conversion + +import ( + "k8s.io/apimachinery/pkg/conversion" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + out.Spec.Object = in.Spec.Object + + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: dashv0.VERSION, + }, + } + + if err := migration.Migrate(out.Spec.Object, schemaversion.LATEST_VERSION); err != nil { + out.Status.Conversion.Failed = true + out.Status.Conversion.Error = err.Error() + } + + return nil +} + +func Convert_V0_to_V2alpha1(in *dashv0.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO (@radiohead): implement V0 to V2 conversion + // This is the bare minimum conversion that is needed to make the dashboard servable. + + if v, ok := in.Spec.Object["title"]; ok { + if title, ok := v.(string); ok { + out.Spec.Title = title + } + } + + // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. + out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + } + + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: dashv0.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} + +func Convert_V0_to_V2alpha2(in *dashv0.Dashboard, out *dashv2alpha2.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V0 to V2alpha2 conversion + + out.Status = dashv2alpha2.DashboardStatus{ + Conversion: &dashv2alpha2.DashboardConversionStatus{ + StoredVersion: dashv0.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} diff --git a/apps/dashboard/pkg/migration/conversion/v1.go b/apps/dashboard/pkg/migration/conversion/v1.go new file mode 100644 index 00000000000..d71d9e40caf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v1.go @@ -0,0 +1,71 @@ +package conversion + +import ( + "k8s.io/apimachinery/pkg/conversion" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" +) + +func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + out.Spec.Object = in.Spec.Object + + out.Status = dashv0.DashboardStatus{ + Conversion: &dashv0.DashboardConversionStatus{ + StoredVersion: dashv1.VERSION, + }, + } + + return nil +} + +func Convert_V1_to_V2alpha1(in *dashv1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO (@radiohead): implement V1 to V2 conversion + // This is the bare minimum conversion that is needed to make the dashboard servable. + + if v, ok := in.Spec.Object["title"]; ok { + if title, ok := v.(string); ok { + out.Spec.Title = title + } + } + + // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. + out.Spec.Layout = dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2alpha1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2alpha1.DashboardGridLayoutSpec{}, + }, + } + + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: dashv1.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} + +func Convert_V1_to_V2alpha2(in *dashv1.Dashboard, out *dashv2alpha2.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V1 to V2alpha2 conversion + + out.Status = dashv2alpha2.DashboardStatus{ + Conversion: &dashv2alpha2.DashboardConversionStatus{ + StoredVersion: dashv1.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go new file mode 100644 index 00000000000..5186f9257e2 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -0,0 +1,105 @@ +package conversion + +import ( + "k8s.io/apimachinery/pkg/conversion" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" +) + +func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V2 to V0 conversion + + out.Status = dashv0.DashboardStatus{ + Conversion: &dashv0.DashboardConversionStatus{ + StoredVersion: dashv2alpha1.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} + +func Convert_V2alpha1_to_V1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V2 to V1 conversion + + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: dashv2alpha1.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} + +func Convert_V2alpha1_to_V2alpha2(in *dashv2alpha1.Dashboard, out *dashv2alpha2.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V2alpha1 to V2alpha2 conversion + + out.Status = dashv2alpha2.DashboardStatus{ + Conversion: &dashv2alpha2.DashboardConversionStatus{ + StoredVersion: dashv2alpha1.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} +func Convert_V2alpha2_to_V0(in *dashv2alpha2.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V2alpha2 to V0 conversion + + out.Status = dashv0.DashboardStatus{ + Conversion: &dashv0.DashboardConversionStatus{ + StoredVersion: dashv2alpha2.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} + +func Convert_V2alpha2_to_V1(in *dashv2alpha2.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V2alpha2 to V1 conversion + + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: dashv2alpha2.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} + +func Convert_V2alpha2_to_V2alpha1(in *dashv2alpha2.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + + // TODO: implement V2alpha2 to V2alpha1 conversion + + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: dashv2alpha2.VERSION, + Failed: true, + Error: "backend conversion not yet implemented", + }, + } + + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/migrations.go b/apps/dashboard/pkg/migration/schemaversion/migrations.go index aeafd4ab892..3ca5eea83bd 100644 --- a/apps/dashboard/pkg/migration/schemaversion/migrations.go +++ b/apps/dashboard/pkg/migration/schemaversion/migrations.go @@ -5,7 +5,7 @@ import ( ) const ( - MIN_VERSION = 32 + MIN_VERSION = 28 LATEST_VERSION = 41 ) @@ -26,6 +26,10 @@ type DataSourceInfoProvider interface { func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc { return map[int]SchemaVersionMigrationFunc{ + 29: V29, + 30: V30, + 31: V31, + 32: V32, 33: V33(dsInfoProvider), 34: V34, 35: V35, diff --git a/apps/dashboard/pkg/migration/schemaversion/v29.go b/apps/dashboard/pkg/migration/schemaversion/v29.go new file mode 100644 index 00000000000..27ffddcb8fc --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v29.go @@ -0,0 +1,59 @@ +package schemaversion + +// V29 migrates query variables to ensure their refresh property is set to 1 (on dashboard load) +// if it is not 1 or 2, and clears their options array if present. +// +// Example before migration: +// +// "templating": { +// "list": [ +// { "type": "query", "refresh": 0, "options": [{ "text": "A", "value": "A" }] }, +// { "type": "query", "refresh": 2, "options": [{ "text": "B", "value": "B" }] }, +// { "type": "query", "options": [{ "text": "C", "value": "C" }] } +// ] +// } +// +// Example after migration: +// +// "templating": { +// "list": [ +// { "type": "query", "refresh": 1, "options": [] }, +// { "type": "query", "refresh": 2, "options": [] }, +// { "type": "query", "refresh": 1, "options": [] } +// ] +// } +func V29(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 29 + + templating, ok := dashboard["templating"].(map[string]interface{}) + if !ok { + return nil + } + list, ok := templating["list"].([]interface{}) + if !ok { + return nil + } + for _, v := range list { + variable, ok := v.(map[string]interface{}) + if !ok { + continue + } + if variable["type"] != "query" { + continue + } + // Set refresh to 1 if not 1 or 2 + refresh, hasRefresh := variable["refresh"] + refreshInt := 0 + if r, ok := refresh.(int); ok { + refreshInt = r + } + if !hasRefresh || (refreshInt != 1 && refreshInt != 2) { + variable["refresh"] = 1 + } + // Clear options if present + if _, hasOptions := variable["options"]; hasOptions { + variable["options"] = []interface{}{} + } + } + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v29_test.go b/apps/dashboard/pkg/migration/schemaversion/v29_test.go new file mode 100644 index 00000000000..62d3b6a9051 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v29_test.go @@ -0,0 +1,127 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV29(t *testing.T) { + tests := []migrationTestCase{ + { + name: "query variables get migrated with refresh and options", + input: map[string]interface{}{ + "title": "V29 Query Variables Migration Test Dashboard", + "schemaVersion": 28, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"type": "query", "name": "never_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 0}, + map[string]interface{}{"type": "query", "name": "never_refresh_without_options", "options": []interface{}{}, "refresh": 0}, + map[string]interface{}{"type": "query", "name": "dashboard_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "dashboard_refresh_without_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "timerange_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "timerange_refresh_without_options", "options": []interface{}{}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "no_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}}, + map[string]interface{}{"type": "query", "name": "no_refresh_without_options", "options": []interface{}{}}, + map[string]interface{}{"type": "query", "name": "unknown_refresh_with_options", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}, "refresh": 2001}, + map[string]interface{}{"type": "query", "name": "unknown_refresh_without_options", "options": []interface{}{}, "refresh": 2001}, + map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}}, + map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}}, + map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}}, + map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}}, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V29 Query Variables Migration Test Dashboard", + "schemaVersion": 29, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"type": "query", "name": "never_refresh_with_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "never_refresh_without_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "dashboard_refresh_with_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "dashboard_refresh_without_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "timerange_refresh_with_options", "options": []interface{}{}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "timerange_refresh_without_options", "options": []interface{}{}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "no_refresh_with_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "no_refresh_without_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "unknown_refresh_with_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "unknown_refresh_without_options", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}}, + map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}}, + map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}}, + map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}}, + }, + }, + }, + }, + { + name: "non-query variables remain unchanged", + input: map[string]interface{}{ + "title": "V29 Non-Query Variables Test Dashboard", + "schemaVersion": 28, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}}, + map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}}, + map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}}, + map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}}, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V29 Non-Query Variables Test Dashboard", + "schemaVersion": 29, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"type": "custom", "name": "custom", "options": []interface{}{map[string]interface{}{"text": "custom", "value": "custom"}}}, + map[string]interface{}{"type": "textbox", "name": "textbox", "options": []interface{}{map[string]interface{}{"text": "Hello", "value": "World"}}}, + map[string]interface{}{"type": "datasource", "name": "datasource", "options": []interface{}{map[string]interface{}{"text": "ds", "value": "ds"}}}, + map[string]interface{}{"type": "interval", "name": "interval", "options": []interface{}{map[string]interface{}{"text": "1m", "value": "1m"}}}, + }, + }, + }, + }, + { + name: "all query variables should have options removed", + input: map[string]interface{}{ + "title": "V29 Query Variables Options Removal Test Dashboard", + "schemaVersion": 28, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"type": "query", "name": "query1", "options": []interface{}{map[string]interface{}{"text": "A", "value": "A"}}}, + map[string]interface{}{"type": "query", "name": "query2", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query3", "options": []interface{}{map[string]interface{}{"text": "B", "value": "B"}, map[string]interface{}{"text": "C", "value": "C"}}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "query4", "options": []interface{}{map[string]interface{}{"text": "D", "value": "D"}}, "refresh": 0}, + map[string]interface{}{"type": "query", "name": "query5", "options": []interface{}{map[string]interface{}{"text": "E", "value": "E"}}, "refresh": 2001}, + map[string]interface{}{"type": "query", "name": "query6", "options": []interface{}{}}, + map[string]interface{}{"type": "query", "name": "query7", "options": []interface{}{map[string]interface{}{"text": "F", "value": "F"}}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query8", "options": []interface{}{map[string]interface{}{"text": "G", "value": "G"}}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "query9", "options": []interface{}{map[string]interface{}{"text": "H", "value": "H"}}}, + map[string]interface{}{"type": "query", "name": "query10", "options": []interface{}{map[string]interface{}{"text": "I", "value": "I"}}, "refresh": 999}, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V29 Query Variables Options Removal Test Dashboard", + "schemaVersion": 29, + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"type": "query", "name": "query1", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query2", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query3", "options": []interface{}{}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "query4", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query5", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query6", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query7", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query8", "options": []interface{}{}, "refresh": 2}, + map[string]interface{}{"type": "query", "name": "query9", "options": []interface{}{}, "refresh": 1}, + map[string]interface{}{"type": "query", "name": "query10", "options": []interface{}{}, "refresh": 1}, + }, + }, + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V29) +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v30.go b/apps/dashboard/pkg/migration/schemaversion/v30.go new file mode 100644 index 00000000000..d892abecb50 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v30.go @@ -0,0 +1,393 @@ +package schemaversion + +import ( + "strconv" +) + +// V30 upgrades value mappings and migrates tooltip options for panels. +// +// This migration addresses two key improvements to panel configurations: +// 1. Value mappings upgrade: Converts legacy value mapping format to the new structured format +// 2. Tooltip options migration: Renames tooltipOptions to tooltip in specific panel types: timeseries, xychart, xychart2 +// +// The migration works by: +// 1. Iterating through all panels in the dashboard, including nested panels in collapsed rows +// 2. For each panel, upgrading value mappings in fieldConfig.defaults and fieldConfig.overrides +// 3. Migrating tooltip options for timeseries, xychart, and xychart2 panels +// 4. Preserving all other panel configurations and options +// +// Value Mappings Migration: +// - Converts legacy mapping types (1 = ValueToText, 2 = RangeToText) to new format +// - Handles special "null" values by converting them to SpecialValue mappings +// - Preserves threshold colors when available +// - Consolidates multiple value mappings into a single ValueToText mapping +// - Maintains range mappings as separate RangeToText mappings +// +// Tooltip Options Migration: +// - Renames panel.options.tooltipOptions to panel.options.tooltip +// - Only applies to timeseries, xychart, and xychart2 panel types +// - Preserves all tooltip configuration options +// +// Example value mappings transformation: +// +// Before migration: +// +// panel: { +// "fieldConfig": { +// "defaults": { +// "mappings": [ +// { "id": 0, "text": "Up", "type": 1, "value": "1" }, +// { "id": 1, "text": "Down", "type": 1, "value": "0" }, +// { "from": "10", "to": "20", "text": "Medium", "type": 2 } +// ] +// } +// } +// } +// +// After migration: +// +// panel: { +// "fieldConfig": { +// "defaults": { +// "mappings": [ +// { +// "type": "value", +// "options": { +// "1": { "text": "Up" }, +// "0": { "text": "Down" } +// } +// }, +// { +// "type": "range", +// "options": { +// "from": 10, +// "to": 20, +// "result": { "text": "Medium" } +// } +// } +// ] +// } +// } +// } +// +// Example tooltip options transformation: +// +// Before migration: +// +// panel: { +// "type": "timeseries", +// "options": { +// "tooltipOptions": { "mode": "multi" } +// } +// } +// +// After migration: +// +// panel: { +// "type": "timeseries", +// "options": { +// "tooltip": { "mode": "multi" } +// } +// } +func V30(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = 30 + + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + return nil + } + + for _, panel := range panels { + panelMap, ok := panel.(map[string]interface{}) + if !ok { + continue + } + + upgradeValueMappingsForPanel(panelMap) + migrateTooltipOptions(panelMap) + + // Handle nested panels in collapsed rows + nestedPanels, hasNested := panelMap["panels"].([]interface{}) + if !hasNested { + continue + } + + for _, nestedPanel := range nestedPanels { + nestedPanelMap, ok := nestedPanel.(map[string]interface{}) + if !ok { + continue + } + upgradeValueMappingsForPanel(nestedPanelMap) + migrateTooltipOptions(nestedPanelMap) + } + } + + return nil +} + +// upgradeValueMappingsForPanel migrates value mappings from old format to new format +func upgradeValueMappingsForPanel(panel map[string]interface{}) { + fieldConfig, ok := panel["fieldConfig"].(map[string]interface{}) + if !ok { + return + } + + // Upgrade defaults mappings + if defaults, ok := fieldConfig["defaults"].(map[string]interface{}); ok { + if mappings, ok := defaults["mappings"].([]interface{}); ok { + var thresholds map[string]interface{} + if t, ok := defaults["thresholds"].(map[string]interface{}); ok { + thresholds = t + } + defaults["mappings"] = upgradeValueMappings(mappings, thresholds) + } + } + + // Upgrade overrides mappings + overrides, hasOverrides := fieldConfig["overrides"].([]interface{}) + if !hasOverrides { + return + } + + for _, override := range overrides { + overrideMap, ok := override.(map[string]interface{}) + if !ok { + continue + } + + properties, hasProperties := overrideMap["properties"].([]interface{}) + if !hasProperties { + continue + } + + for _, property := range properties { + propertyMap, ok := property.(map[string]interface{}) + if !ok { + continue + } + + if propertyMap["id"] != "mappings" { + continue + } + + mappings, ok := propertyMap["value"].([]interface{}) + if !ok { + continue + } + + propertyMap["value"] = upgradeValueMappings(mappings, nil) + } + } +} + +// upgradeValueMappings converts legacy value mappings to new format +func upgradeValueMappings(oldMappings []interface{}, thresholds map[string]interface{}) []interface{} { + if len(oldMappings) == 0 { + return oldMappings + } + + valueMaps := map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{}, + } + + var newMappings []interface{} + hasValueMappings := false + + for _, mapping := range oldMappings { + if mappingMap, ok := mapping.(map[string]interface{}); ok { + // Check if this is already the new format + if mappingType, ok := mappingMap["type"].(string); ok && mappingType != "" { + if mappingType == "value" { + // Consolidate existing value mappings + if options, ok := mappingMap["options"].(map[string]interface{}); ok { + valueMapsOptions := valueMaps["options"].(map[string]interface{}) + for k, v := range options { + valueMapsOptions[k] = v + } + hasValueMappings = true + } + } else { + newMappings = append(newMappings, mappingMap) + } + continue + } + + // Handle legacy format + var color interface{} + if thresholds != nil { + // Try to get color from threshold based on the mapping value + if value, ok := mappingMap["value"]; ok { + if valueStr, ok := value.(string); ok { + if numeric, err := strconv.ParseFloat(valueStr, 64); err == nil { + color = getActiveThresholdColor(numeric, thresholds) + } + } + } + // For range mappings, use the 'from' value to determine color + if fromVal, ok := mappingMap["from"]; ok { + if fromStr, ok := fromVal.(string); ok { + if numeric, err := strconv.ParseFloat(fromStr, 64); err == nil { + color = getActiveThresholdColor(numeric, thresholds) + } + } + } + } + + // Convert legacy type numbers to new format + if mappingType, ok := mappingMap["type"].(float64); ok { + switch int(mappingType) { + case 1: // ValueToText + if value, ok := mappingMap["value"]; ok { + if valueStr, ok := value.(string); ok && valueStr == "null" { + // Handle null values as special value mapping + // For null values, use the base threshold color (lowest step) + if thresholds != nil { + color = getBaseThresholdColor(thresholds) + } + newMappings = append(newMappings, map[string]interface{}{ + "type": "special", + "options": map[string]interface{}{ + "match": "null", + "result": map[string]interface{}{ + "text": mappingMap["text"], + "color": color, + }, + }, + }) + } else { + // Regular value mapping + valueMapsOptions := valueMaps["options"].(map[string]interface{}) + result := map[string]interface{}{ + "text": mappingMap["text"], + } + if color != nil { + result["color"] = color + } + valueMapsOptions[stringifyValue(value)] = result + hasValueMappings = true + } + } + case 2: // RangeToText + result := map[string]interface{}{ + "text": mappingMap["text"], + } + if color != nil { + result["color"] = color + } + + from, _ := strconv.ParseFloat(stringifyValue(mappingMap["from"]), 64) + to, _ := strconv.ParseFloat(stringifyValue(mappingMap["to"]), 64) + + newMappings = append(newMappings, map[string]interface{}{ + "type": "range", + "options": map[string]interface{}{ + "from": from, + "to": to, + "result": result, + }, + }) + } + } + } + } + + // Add consolidated value mappings at the beginning if any exist + if hasValueMappings { + newMappings = append([]interface{}{valueMaps}, newMappings...) + } + + return newMappings +} + +// getActiveThresholdColor returns the color for a value based on thresholds +func getActiveThresholdColor(value float64, thresholds map[string]interface{}) interface{} { + if steps, ok := thresholds["steps"].([]interface{}); ok { + var activeStep map[string]interface{} + + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + if stepValue, ok := stepMap["value"]; ok { + if stepValue == nil { + // Null value represents negative infinity - this is always the base color + activeStep = stepMap + continue + } + + if stepNum, ok := stepValue.(float64); ok { + if value >= stepNum { + activeStep = stepMap + } + } + } + } + } + + if activeStep != nil { + return activeStep["color"] + } + } + + return nil +} + +// getBaseThresholdColor returns the base threshold color (first step with null value) +func getBaseThresholdColor(thresholds map[string]interface{}) interface{} { + if steps, ok := thresholds["steps"].([]interface{}); ok { + for _, step := range steps { + if stepMap, ok := step.(map[string]interface{}); ok { + if stepValue, ok := stepMap["value"]; ok { + if stepValue == nil { + // This is the base color (null value step) + return stepMap["color"] + } + } + } + } + } + + return nil +} + +// migrateTooltipOptions renames tooltipOptions to tooltip for specific panel types +func migrateTooltipOptions(panel map[string]interface{}) { + panelType, ok := panel["type"].(string) + if !ok { + return + } + + // Only migrate for specific panel types + if panelType != "timeseries" && panelType != "xychart" && panelType != "xychart2" { + return + } + + options, ok := panel["options"].(map[string]interface{}) + if !ok { + return + } + + tooltipOptions, ok := options["tooltipOptions"] + if !ok { + return + } + + // Rename tooltipOptions to tooltip + options["tooltip"] = tooltipOptions + delete(options, "tooltipOptions") +} + +// stringifyValue converts a value to string +func stringifyValue(value interface{}) string { + switch v := value.(type) { + case string: + return v + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case int: + return strconv.Itoa(v) + case bool: + return strconv.FormatBool(v) + default: + return "" + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v30_test.go b/apps/dashboard/pkg/migration/schemaversion/v30_test.go new file mode 100644 index 00000000000..bbae0e9a412 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v30_test.go @@ -0,0 +1,499 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV30(t *testing.T) { + tests := []migrationTestCase{ + { + name: "panel with legacy value mappings gets upgraded to new format", + input: map[string]interface{}{ + "title": "V30 Value Mappings Migration Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with legacy value mappings", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "thresholds": map[string]interface{}{ + "mode": "absolute", + "steps": []interface{}{ + map[string]interface{}{ + "color": "green", + "value": nil, + }, + map[string]interface{}{ + "color": "red", + "value": float64(80), + }, + }, + }, + "mappings": []interface{}{ + map[string]interface{}{ + "id": 0, + "text": "Up", + "type": float64(1), + "value": "1", + }, + map[string]interface{}{ + "id": 1, + "text": "Down", + "type": float64(1), + "value": "0", + }, + map[string]interface{}{ + "from": "10", + "to": "20", + "text": "Medium", + "type": float64(2), + }, + map[string]interface{}{ + "type": float64(1), + "value": "null", + "text": "Null Value", + }, + }, + }, + "overrides": []interface{}{ + map[string]interface{}{ + "matcher": map[string]interface{}{ + "id": "byName", + "options": "test-field", + }, + "properties": []interface{}{ + map[string]interface{}{ + "id": "mappings", + "value": []interface{}{ + map[string]interface{}{ + "id": 0, + "text": "Override Up", + "type": float64(1), + "value": "1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Value Mappings Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with legacy value mappings", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "thresholds": map[string]interface{}{ + "mode": "absolute", + "steps": []interface{}{ + map[string]interface{}{ + "color": "green", + "value": nil, + }, + map[string]interface{}{ + "color": "red", + "value": float64(80), + }, + }, + }, + "mappings": []interface{}{ + map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{ + "1": map[string]interface{}{ + "text": "Up", + "color": "green", + }, + "0": map[string]interface{}{ + "text": "Down", + "color": "green", + }, + }, + }, + map[string]interface{}{ + "type": "range", + "options": map[string]interface{}{ + "from": float64(10), + "to": float64(20), + "result": map[string]interface{}{ + "text": "Medium", + "color": "green", + }, + }, + }, + map[string]interface{}{ + "type": "special", + "options": map[string]interface{}{ + "match": "null", + "result": map[string]interface{}{ + "text": "Null Value", + "color": "green", + }, + }, + }, + }, + }, + "overrides": []interface{}{ + map[string]interface{}{ + "matcher": map[string]interface{}{ + "id": "byName", + "options": "test-field", + }, + "properties": []interface{}{ + map[string]interface{}{ + "id": "mappings", + "value": []interface{}{ + map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{ + "1": map[string]interface{}{ + "text": "Override Up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "panel with tooltip options gets migrated to tooltip", + input: map[string]interface{}{ + "title": "V30 Tooltip Options Migration Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with tooltipOptions", + "id": 1, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "multi", + }, + }, + }, + map[string]interface{}{ + "type": "xychart", + "title": "XY Chart with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "single", + }, + }, + }, + map[string]interface{}{ + "type": "xychart2", + "title": "XY Chart2 with tooltipOptions", + "id": 3, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "single", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Tooltip Options Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with tooltipOptions", + "id": 1, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "multi", + }, + }, + }, + map[string]interface{}{ + "type": "xychart", + "title": "XY Chart with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "single", + }, + }, + }, + map[string]interface{}{ + "type": "xychart2", + "title": "XY Chart2 with tooltipOptions", + "id": 3, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "single", + }, + }, + }, + }, + }, + }, + { + name: "panel with nested panels in collapsed row gets migrated", + input: map[string]interface{}{ + "title": "V30 Nested Panels Migration Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "row", + "title": "Collapsed Row", + "id": 1, + "collapsed": true, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltipOptions": map[string]interface{}{ + "mode": "multi", + }, + }, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "mappings": []interface{}{ + map[string]interface{}{ + "id": 0, + "text": "On", + "type": float64(1), + "value": "1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Nested Panels Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "row", + "title": "Collapsed Row", + "id": 1, + "collapsed": true, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel with tooltipOptions", + "id": 2, + "options": map[string]interface{}{ + "tooltip": map[string]interface{}{ + "mode": "multi", + }, + }, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "mappings": []interface{}{ + map[string]interface{}{ + "type": "value", + "options": map[string]interface{}{ + "1": map[string]interface{}{ + "text": "On", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "panel with no mappings or tooltip options remains unchanged", + input: map[string]interface{}{ + "title": "V30 No Changes Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with no relevant configurations", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "unit": "bytes", + }, + }, + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 No Changes Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with no relevant configurations", + "id": 1, + "fieldConfig": map[string]interface{}{ + "defaults": map[string]interface{}{ + "unit": "bytes", + }, + }, + "options": map[string]interface{}{ + "legend": map[string]interface{}{ + "displayMode": "list", + }, + }, + }, + }, + }, + }, + { + name: "panels remain unchanged when no V30 specific migrations apply", + input: map[string]interface{}{ + "title": "V30 Panel Types Unchanged Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph panel", + "id": 1, + }, + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat panel", + "id": 2, + }, + map[string]interface{}{ + "type": "timeseries", + "title": "Already timeseries panel", + "id": 3, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Panel Types Unchanged Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph panel", + "id": 1, + }, + map[string]interface{}{ + "type": "singlestat", + "title": "Singlestat panel", + "id": 2, + }, + map[string]interface{}{ + "type": "timeseries", + "title": "Already timeseries panel", + "id": 3, + }, + }, + }, + }, + { + name: "graph panels with different configurations remain unchanged in V30", + input: map[string]interface{}{ + "title": "V30 Graph Panel Configurations Test Dashboard", + "schemaVersion": 29, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode and legend values", + "id": 1, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + "legend": map[string]interface{}{ + "values": true, + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode", + "id": 2, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with histogram mode", + "id": 3, + "xaxis": map[string]interface{}{ + "mode": "histogram", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with default configuration", + "id": 4, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V30 Graph Panel Configurations Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode and legend values", + "id": 1, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + "legend": map[string]interface{}{ + "values": true, + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with series mode", + "id": 2, + "xaxis": map[string]interface{}{ + "mode": "series", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with histogram mode", + "id": 3, + "xaxis": map[string]interface{}{ + "mode": "histogram", + }, + }, + map[string]interface{}{ + "type": "graph", + "title": "Graph with default configuration", + "id": 4, + }, + }, + }, + }, + } + + runMigrationTests(t, tests, schemaversion.V30) +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v31.go b/apps/dashboard/pkg/migration/schemaversion/v31.go new file mode 100644 index 00000000000..3b4c907ce5d --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v31.go @@ -0,0 +1,132 @@ +package schemaversion + +// V31 adds a merge transformer after any labelsToFields transformer in panel transformations. +// +// This migration addresses data processing workflow optimization by automatically inserting +// a merge transformation after the labelsToFields transformation. When the labelsToFields +// transformer converts time series labels to individual fields, it can result in multiple +// data frames that need to be consolidated for proper visualization and analysis. +// +// The migration works by: +// 1. Iterating through all panels in the dashboard, including nested panels in collapsed rows +// 2. Examining the transformations array within each panel +// 3. Identifying transformations with id 'labelsToFields' +// 4. Inserting a merge transformation with empty options immediately after each labelsToFields transformation +// 5. Preserving the original labelsToFields options (mode, keepLabels, valueLabel, etc.) without modification +// 6. Using empty options for merge transformations to enable optimal default consolidation behavior +// 7. Preserving the original order and configuration of all other transformations +// +// The migration handles complex scenarios including: +// - Multiple labelsToFields transformations within a single panel +// - Panels with mixed transformation types +// - Nested panels within collapsed row panels +// - Panels with no transformations (left unchanged) +// - Panels with transformations but no labelsToFields (left unchanged) +// +// Example transformation: +// +// Before migration: +// +// panel: { +// "transformations": [ +// { "id": "organize", "options": {} }, +// { "id": "labelsToFields", "options": {} }, +// { "id": "calculateField", "options": {} }, +// { "id": "labelsToFields", "options": { "mode": "rows", "keepLabels": ["job", "instance"] } }, +// ] +// } +// +// After migration: +// +// panel: { +// "transformations": [ +// { "id": "organize", "options": {} }, +// { "id": "labelsToFields", "options": {} }, +// { "id": "merge", "options": {} }, +// { "id": "calculateField", "options": {} }, +// { "id": "labelsToFields", "options": { "mode": "rows", "keepLabels": ["job", "instance"] } }, +// { "id": "merge", "options": {} } +// ] +// } +func V31(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = int(31) + + panels, ok := dashboard["panels"].([]interface{}) + if !ok { + return nil + } + + // Process all panels, including nested ones + processPanelsV31(panels) + + return nil +} + +// processPanelsV31 recursively processes panels, including nested panels within rows +func processPanelsV31(panels []interface{}) { + for _, panel := range panels { + p, ok := panel.(map[string]interface{}) + if !ok { + continue + } + + // Process nested panels if this is a row panel + if p["type"] == "row" { + nestedPanels, hasNested := p["panels"].([]interface{}) + if !hasNested { + continue + } + processPanelsV31(nestedPanels) + continue + } + + transformations, ok := p["transformations"].([]interface{}) + if !ok { + continue + } + + // Check if we have any labelsToFields transformations + hasLabelsToFields := false + for _, transformation := range transformations { + t, ok := transformation.(map[string]interface{}) + if !ok { + continue + } + if t["id"] == "labelsToFields" { + hasLabelsToFields = true + break + } + } + + if !hasLabelsToFields { + continue + } + + // Create new transformations array with merge transformations added + newTransformations := []interface{}{} + + for _, transformation := range transformations { + t, ok := transformation.(map[string]interface{}) + if !ok { + newTransformations = append(newTransformations, transformation) + continue + } + + // Add the current transformation (preserving all original options) + newTransformations = append(newTransformations, transformation) + + // If this is a labelsToFields transformation, add a merge transformation after it + // with empty options to enable optimal default consolidation behavior + if t["id"] == "labelsToFields" { + mergeTransformation := map[string]interface{}{ + "id": "merge", + "options": map[string]interface{}{}, + } + newTransformations = append(newTransformations, mergeTransformation) + } + } + + // Update the panel with the new transformations + p["transformations"] = newTransformations + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v31_test.go b/apps/dashboard/pkg/migration/schemaversion/v31_test.go new file mode 100644 index 00000000000..e12040585de --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v31_test.go @@ -0,0 +1,336 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV31(t *testing.T) { + tests := []migrationTestCase{ + { + name: "panel with basic labelsToFields transformation gets merge transformation added", + input: map[string]interface{}{ + "title": "V31 LabelsToFields Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with basic labelsToFields", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V31 LabelsToFields Migration Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with basic labelsToFields", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "merge", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + { + name: "panel with labelsToFields options preserved during migration", + input: map[string]interface{}{ + "title": "V31 LabelsToFields Options Preservation Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with labelsToFields options", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{ + "mode": "rows", + "keepLabels": []interface{}{"job", "instance"}, + "valueLabel": "value", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V31 LabelsToFields Options Preservation Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with labelsToFields options", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{ + "mode": "rows", + "keepLabels": []interface{}{"job", "instance"}, + "valueLabel": "value", + }, + }, + map[string]interface{}{ + "id": "merge", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + { + name: "panel with multiple labelsToFields transformations", + input: map[string]interface{}{ + "title": "V31 Multiple LabelsToFields Migration Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with multiple labelsToFields", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "organize", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "calculateField", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{ + "mode": "rows", + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V31 Multiple LabelsToFields Migration Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with multiple labelsToFields", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "organize", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "merge", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "calculateField", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{ + "mode": "rows", + }, + }, + map[string]interface{}{ + "id": "merge", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + { + name: "panel with no transformations remains unchanged", + input: map[string]interface{}{ + "title": "V31 No Transformations Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with no transformations", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V31 No Transformations Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with no transformations", + "id": 1, + }, + }, + }, + }, + { + name: "panel with transformations but no labelsToFields remains unchanged", + input: map[string]interface{}{ + "title": "V31 Other Transformations Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with other transformations", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "organize", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "reduce", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V31 Other Transformations Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel with other transformations", + "id": 1, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "organize", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "reduce", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + { + name: "nested panels in row with labelsToFields transformation", + input: map[string]interface{}{ + "title": "V31 Nested Panels Test Dashboard", + "schemaVersion": 30, + "panels": []interface{}{ + map[string]interface{}{ + "type": "row", + "title": "Row with nested panels", + "id": 1, + "collapsed": false, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel with labelsToFields", + "id": 2, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{}, + }, + }, + }, + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel without labelsToFields", + "id": 3, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "organize", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V31 Nested Panels Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "row", + "title": "Row with nested panels", + "id": 1, + "collapsed": false, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel with labelsToFields", + "id": 2, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "labelsToFields", + "options": map[string]interface{}{}, + }, + map[string]interface{}{ + "id": "merge", + "options": map[string]interface{}{}, + }, + }, + }, + map[string]interface{}{ + "type": "timeseries", + "title": "Nested panel without labelsToFields", + "id": 3, + "transformations": []interface{}{ + map[string]interface{}{ + "id": "organize", + "options": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "dashboard with no panels", + input: map[string]interface{}{ + "title": "V31 No Panels Test Dashboard", + "schemaVersion": 30, + }, + expected: map[string]interface{}{ + "title": "V31 No Panels Test Dashboard", + "schemaVersion": 31, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V31) +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v32.go b/apps/dashboard/pkg/migration/schemaversion/v32.go new file mode 100644 index 00000000000..6ea48bf0a73 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v32.go @@ -0,0 +1,27 @@ +package schemaversion + +// V32 is a no-op migration that serves as a placeholder for consistency. +// +// The migration performs no modifications to the dashboard structure and simply +// updates the schema version number. +// Example (no changes made): +// +// Before migration: +// +// dashboard: { +// "title": "My Dashboard", +// "schemaVersion": 31, +// "panels": [...] +// } +// +// After migration: +// +// dashboard: { +// "title": "My Dashboard", +// "schemaVersion": 32, +// "panels": [...] // unchanged +// } +func V32(dashboard map[string]interface{}) error { + dashboard["schemaVersion"] = int(32) + return nil +} diff --git a/apps/dashboard/pkg/migration/schemaversion/v32_test.go b/apps/dashboard/pkg/migration/schemaversion/v32_test.go new file mode 100644 index 00000000000..23df4b26d28 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/v32_test.go @@ -0,0 +1,38 @@ +package schemaversion_test + +import ( + "testing" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func TestV32(t *testing.T) { + tests := []migrationTestCase{ + { + name: "v32 no-op migration updates schema version only", + input: map[string]interface{}{ + "title": "V32 No-Op Migration Test Dashboard", + "schemaVersion": 31, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel remains unchanged", + "id": 1, + }, + }, + }, + expected: map[string]interface{}{ + "title": "V32 No-Op Migration Test Dashboard", + "schemaVersion": 32, + "panels": []interface{}{ + map[string]interface{}{ + "type": "timeseries", + "title": "Panel remains unchanged", + "id": 1, + }, + }, + }, + }, + } + runMigrationTests(t, tests, schemaversion.V32) +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v29.query_variables_refresh_and_options.json b/apps/dashboard/pkg/migration/testdata/input/v29.query_variables_refresh_and_options.json new file mode 100644 index 00000000000..00bf06ad0c0 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v29.query_variables_refresh_and_options.json @@ -0,0 +1,137 @@ +{ + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard", + "schemaVersion": 28, + "templating": { + "list": [ + { + "name": "never_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "A", "value": "A"}, + {"text": "B", "value": "B"} + ], + "refresh": 0 + }, + { + "name": "never_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 0 + }, + { + "name": "dashboard_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "C", "value": "C"} + ], + "refresh": 1 + }, + { + "name": "dashboard_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 1 + }, + { + "name": "timerange_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "D", "value": "D"}, + {"text": "E", "value": "E"} + ], + "refresh": 2 + }, + { + "name": "timerange_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 2 + }, + { + "name": "no_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "F", "value": "F"} + ] + }, + { + "name": "no_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [] + }, + { + "name": "unknown_refresh_with_options", + "type": "query", + "datasource": "prometheus", + "options": [ + {"text": "G", "value": "G"} + ], + "refresh": 2001 + }, + { + "name": "unknown_refresh_without_options", + "type": "query", + "datasource": "prometheus", + "options": [], + "refresh": 2001 + }, + { + "name": "custom_variable", + "type": "custom", + "options": [ + {"text": "custom", "value": "custom"} + ] + }, + { + "name": "textbox_variable", + "type": "textbox", + "options": [ + {"text": "Hello", "value": "World"} + ] + }, + { + "name": "datasource_variable", + "type": "datasource", + "options": [ + {"text": "ds", "value": "ds"} + ] + }, + { + "name": "interval_variable", + "type": "interval", + "options": [ + {"text": "1m", "value": "1m"} + ] + } + ] + }, + "panels": [ + { + "id": 1, + "title": "Test Panel", + "type": "timeseries", + "datasource": "prometheus", + "targets": [ + { + "refId": "A", + "expr": "up" + } + ] + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/input/v30.value_mappings_and_tooltip_options.json b/apps/dashboard/pkg/migration/testdata/input/v30.value_mappings_and_tooltip_options.json new file mode 100644 index 00000000000..672a88ed0b6 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v30.value_mappings_and_tooltip_options.json @@ -0,0 +1,214 @@ +{ + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard", + "schemaVersion": 29, + "panels": [ + { + "type": "timeseries", + "title": "Panel with legacy value mappings and tooltip options", + "id": 1, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "mappings": [ + { + "id": 0, + "text": "Up", + "type": 1, + "value": "1" + }, + { + "id": 1, + "text": "Down", + "type": 1, + "value": "0" + }, + { + "from": "10", + "to": "20", + "text": "Medium", + "type": 2 + }, + { + "type": 1, + "value": "null", + "text": "Null Value" + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "id": 0, + "text": "Override Up", + "type": 1, + "value": "1" + } + ] + } + ] + } + ] + }, + "options": { + "tooltipOptions": { + "mode": "multi" + } + } + }, + { + "type": "xychart", + "title": "XY Chart with tooltip options only", + "id": 2, + "options": { + "tooltipOptions": { + "mode": "single" + } + } + }, + { + "type": "xychart2", + "title": "XY Chart2 with tooltip options", + "id": 3, + "options": { + "tooltipOptions": { + "mode": "single", + "sort": "none" + } + } + }, + { + "type": "graph", + "title": "Graph panel gets migrated to timeseries and tooltip", + "id": 4, + "options": { + "tooltip": { + "mode": "single" + } + } + }, + { + "type": "stat", + "title": "Panel with complex value mappings", + "id": 5, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "id": 0, + "text": "Critical", + "type": 1, + "value": "100" + }, + { + "from": "50", + "to": "99", + "text": "Warning", + "type": 2 + }, + { + "from": "0", + "to": "49", + "text": "OK", + "type": 2 + } + ] + } + } + }, + { + "type": "row", + "title": "Collapsed Row with nested panels", + "id": 6, + "collapsed": true, + "panels": [ + { + "type": "timeseries", + "title": "Nested panel with both migrations", + "id": 7, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "id": 0, + "text": "On", + "type": 1, + "value": "1" + }, + { + "id": 1, + "text": "Off", + "type": 1, + "value": "0" + } + ] + } + }, + "options": { + "tooltipOptions": { + "mode": "multi" + } + } + } + ] + }, + { + "type": "timeseries", + "title": "Panel with no relevant configurations", + "id": 8, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "options": { + "legend": { + "displayMode": "list" + } + } + }, + { + "type": "stat", + "title": "Panel with empty mappings array - should return null", + "id": 9, + "fieldConfig": { + "defaults": { + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/input/v31.labels_to_fields_merge.json b/apps/dashboard/pkg/migration/testdata/input/v31.labels_to_fields_merge.json new file mode 100644 index 00000000000..be959f93568 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v31.labels_to_fields_merge.json @@ -0,0 +1,126 @@ +{ + "title": "V31 LabelsToFields Merge Migration Test Dashboard", + "schemaVersion": 30, + "panels": [ + { + "type": "timeseries", + "title": "Panel with basic labelsToFields transformation", + "id": 1, + "transformations": [ + { + "id": "labelsToFields", + "options": {} + } + ] + }, + { + "type": "timeseries", + "title": "Panel with labelsToFields options preserved", + "id": 9, + "transformations": [ + { + "id": "labelsToFields", + "options": { + "mode": "rows", + "keepLabels": ["job", "instance", "region"], + "valueLabel": "value" + } + } + ] + }, + { + "type": "timeseries", + "title": "Panel with multiple labelsToFields transformations", + "id": 2, + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "calculateField", + "options": {} + }, + { + "id": "labelsToFields", + "options": { + "mode": "rows" + } + } + ] + }, + { + "type": "timeseries", + "title": "Panel with no transformations", + "id": 3 + }, + { + "type": "timeseries", + "title": "Panel with other transformations only", + "id": 4, + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "reduce", + "options": {} + } + ] + }, + { + "type": "row", + "title": "Row with nested panels", + "id": 5, + "collapsed": false, + "panels": [ + { + "type": "timeseries", + "title": "Nested panel with labelsToFields", + "id": 6, + "transformations": [ + { + "id": "labelsToFields", + "options": {} + } + ] + }, + { + "type": "timeseries", + "title": "Nested panel without labelsToFields", + "id": 7, + "transformations": [ + { + "id": "organize", + "options": {} + } + ] + } + ] + }, + { + "type": "timeseries", + "title": "Panel with labelsToFields and existing merge", + "id": 8, + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "reduce", + "options": {} + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/input/v32.no_op_migration.json b/apps/dashboard/pkg/migration/testdata/input/v32.no_op_migration.json new file mode 100644 index 00000000000..a190ad11be0 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/input/v32.no_op_migration.json @@ -0,0 +1,80 @@ +{ + "title": "V32 No-Op Migration Test Dashboard", + "schemaVersion": 31, + "panels": [ + { + "type": "timeseries", + "title": "Panel with transformations remains unchanged", + "id": 1, + "transformations": [ + { + "id": "labelsToFields", + "options": { + "mode": "rows", + "keepLabels": ["job", "instance"] + } + }, + { + "id": "merge", + "options": {} + } + ] + }, + { + "type": "graph", + "title": "Graph panel remains unchanged", + "id": 2, + "yAxes": [ + { + "show": true, + "min": null, + "max": null + } + ] + }, + { + "type": "row", + "title": "Row with nested panels", + "id": 3, + "collapsed": false, + "panels": [ + { + "type": "stat", + "title": "Nested stat panel", + "id": 4, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + } + } + ] + } + ], + "templating": { + "list": [ + { + "name": "environment", + "type": "query", + "datasource": "prometheus", + "options": [] + } + ] + }, + "annotations": { + "list": [ + { + "name": "Deployments", + "datasource": "grafana", + "enable": true + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/v29.query_variables_refresh_and_options.json b/apps/dashboard/pkg/migration/testdata/output/v29.query_variables_refresh_and_options.json new file mode 100644 index 00000000000..102a1ad1ebb --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v29.query_variables_refresh_and_options.json @@ -0,0 +1,176 @@ +{ + "panels": [ + { + "datasource": { + "uid": "prometheus" + }, + "id": 1, + "targets": [ + { + "datasource": { + "uid": "prometheus" + }, + "expr": "up", + "refId": "A" + } + ], + "title": "Test Panel", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 41, + "templating": { + "list": [ + { + "datasource": { + "uid": "prometheus" + }, + "name": "never_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "never_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "dashboard_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "dashboard_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "timerange_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "timerange_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "no_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "no_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "unknown_refresh_with_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "datasource": { + "uid": "prometheus" + }, + "name": "unknown_refresh_without_options", + "options": [], + "refresh": 1, + "type": "query" + }, + { + "name": "custom_variable", + "options": [ + { + "text": "custom", + "value": "custom" + } + ], + "type": "custom" + }, + { + "name": "textbox_variable", + "options": [ + { + "text": "Hello", + "value": "World" + } + ], + "type": "textbox" + }, + { + "name": "datasource_variable", + "options": [ + { + "text": "ds", + "value": "ds" + } + ], + "type": "datasource" + }, + { + "name": "interval_variable", + "options": [ + { + "text": "1m", + "value": "1m" + } + ], + "type": "interval" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "title": "V29 Query Variables Refresh and Options Migration Test Dashboard" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/v30.value_mappings_and_tooltip_options.json b/apps/dashboard/pkg/migration/testdata/output/v30.value_mappings_and_tooltip_options.json new file mode 100644 index 00000000000..508e41cb1f6 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v30.value_mappings_and_tooltip_options.json @@ -0,0 +1,373 @@ +{ + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "color": "green", + "text": "Down" + }, + "1": { + "color": "green", + "text": "Up" + } + }, + "type": "value" + }, + { + "options": { + "from": 10, + "result": { + "color": "green", + "text": "Medium" + }, + "to": 20 + }, + "type": "range" + }, + { + "options": { + "match": "null", + "result": { + "color": "green", + "text": "Null Value" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "test-field" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "1": { + "text": "Override Up" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "id": 1, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with legacy value mappings and tooltip options", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "options": { + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "XY Chart with tooltip options only", + "type": "xychart" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "options": { + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "XY Chart2 with tooltip options", + "type": "xychart2" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "options": { + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph panel gets migrated to timeseries and tooltip", + "type": "graph" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "100": { + "text": "Critical" + } + }, + "type": "value" + }, + { + "options": { + "from": 50, + "result": { + "text": "Warning" + }, + "to": 99 + }, + "type": "range" + }, + { + "options": { + "from": 0, + "result": { + "text": "OK" + }, + "to": 49 + }, + "type": "range" + } + ] + } + }, + "id": 5, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with complex value mappings", + "type": "stat" + }, + { + "collapsed": true, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 6, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "Off" + }, + "1": { + "text": "On" + } + }, + "type": "value" + } + ] + } + }, + "id": 7, + "options": { + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Nested panel with both migrations", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Collapsed Row with nested panels", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "id": 8, + "options": { + "legend": { + "displayMode": "list", + "showLegend": true + } + }, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no relevant configurations", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "empty-field" + }, + "properties": [ + { + "id": "mappings", + "value": [] + } + ] + } + ] + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with empty mappings array - should return null", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 41, + "title": "V30 Value Mappings and Tooltip Options Migration Test Dashboard" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/v31.labels_to_fields_merge.json b/apps/dashboard/pkg/migration/testdata/output/v31.labels_to_fields_merge.json new file mode 100644 index 00000000000..d48c4f46c2e --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v31.labels_to_fields_merge.json @@ -0,0 +1,290 @@ +{ + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with basic labelsToFields transformation", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 9, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with labelsToFields options preserved", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance", + "region" + ], + "mode": "rows", + "valueLabel": "value" + } + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with multiple labelsToFields transformations", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "calculateField", + "options": {} + }, + { + "id": "labelsToFields", + "options": { + "mode": "rows" + } + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with no transformations", + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with other transformations only", + "transformations": [ + { + "id": "organize", + "options": {} + }, + { + "id": "reduce", + "options": {} + } + ], + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 5, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 6, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Nested panel with labelsToFields", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 7, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Nested panel without labelsToFields", + "transformations": [ + { + "id": "organize", + "options": {} + } + ], + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Row with nested panels", + "type": "row" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 8, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with labelsToFields and existing merge", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "merge", + "options": {} + }, + { + "id": "reduce", + "options": {} + } + ], + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 41, + "title": "V31 LabelsToFields Merge Migration Test Dashboard" +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/testdata/output/v32.no_op_migration.json b/apps/dashboard/pkg/migration/testdata/output/v32.no_op_migration.json new file mode 100644 index 00000000000..ad9e5158fe4 --- /dev/null +++ b/apps/dashboard/pkg/migration/testdata/output/v32.no_op_migration.json @@ -0,0 +1,159 @@ +{ + "annotations": { + "list": [ + { + "datasource": { + "uid": "grafana" + }, + "enable": true, + "name": "Deployments" + } + ] + }, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 1, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Panel with transformations remains unchanged", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "job", + "instance" + ], + "mode": "rows" + } + }, + { + "id": "merge", + "options": {} + } + ], + "type": "timeseries" + }, + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 2, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Graph panel remains unchanged", + "type": "graph", + "yAxes": [ + { + "max": null, + "min": null, + "show": true + } + ] + }, + { + "collapsed": false, + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "id": 3, + "panels": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "fieldConfig": { + "defaults": { + "unit": "bytes" + } + }, + "id": 4, + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Nested stat panel", + "type": "stat" + } + ], + "targets": [ + { + "datasource": { + "apiVersion": "v1", + "type": "prometheus", + "uid": "default-ds-uid" + }, + "refId": "A" + } + ], + "title": "Row with nested panels", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 41, + "templating": { + "list": [ + { + "datasource": { + "uid": "prometheus" + }, + "name": "environment", + "options": [], + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "title": "V32 No-Op Migration Test Dashboard" +} \ No newline at end of file diff --git a/apps/folder/go.mod b/apps/folder/go.mod index c12fa59b93d..88db83aa995 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,9 +3,9 @@ module github.com/grafana/grafana/apps/folder go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.39.0 + github.com/grafana/grafana-app-sdk v0.39.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e - k8s.io/apimachinery v0.33.1 + k8s.io/apimachinery v0.33.2 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -23,7 +23,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -53,7 +53,7 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index c61276e084f..8f346fcbfcd 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -32,10 +32,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yzbjm6T1T1w= -github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -150,12 +150,12 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 811abf007e5..5c4086d8596 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -3,9 +3,9 @@ module github.com/grafana/grafana/apps/iam go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.39.0 + github.com/grafana/grafana-app-sdk v0.39.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e - k8s.io/apimachinery v0.33.1 + k8s.io/apimachinery v0.33.2 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -23,7 +23,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -53,7 +53,7 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index c61276e084f..8f346fcbfcd 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -32,10 +32,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yzbjm6T1T1w= -github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -150,12 +150,12 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/apps/iam/kinds/manifest.cue b/apps/iam/kinds/manifest.cue index f2f4b519d7b..032261e9030 100644 --- a/apps/iam/kinds/manifest.cue +++ b/apps/iam/kinds/manifest.cue @@ -3,5 +3,16 @@ package kinds manifest: { appName: "iam" groupOverride: "iam.grafana.app" - kinds: [ globalrole, globalrolebinding, corerole, role, rolebinding, resourcepermission ] + kinds: [ + globalrole, + globalrolebinding, + corerole, + role, + rolebinding, + resourcepermission, + user, + team, + teambinding, + serviceaccount, + ] } diff --git a/apps/iam/kinds/serviceaccount.cue b/apps/iam/kinds/serviceaccount.cue new file mode 100644 index 00000000000..22fb1e9e9cf --- /dev/null +++ b/apps/iam/kinds/serviceaccount.cue @@ -0,0 +1,24 @@ +package kinds + +import ( + "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" +) + +serviceaccount: { + kind: "ServiceAccount" + pluralName: "ServiceAccounts" + current: "v0alpha1" + + codegen: { + ts: { enabled: false } + go: { enabled: true } + } + + versions: { + "v0alpha1": { + schema: { + spec: v0alpha1.ServiceAccountSpec + } + } + } +} diff --git a/apps/iam/kinds/team.cue b/apps/iam/kinds/team.cue new file mode 100644 index 00000000000..284d9e601ea --- /dev/null +++ b/apps/iam/kinds/team.cue @@ -0,0 +1,24 @@ +package kinds + +import ( + "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" +) + +team: { + kind: "Team" + pluralName: "Teams" + current: "v0alpha1" + + codegen: { + ts: { enabled: false } + go: { enabled: true } + } + + versions: { + "v0alpha1": { + schema: { + spec: v0alpha1.TeamSpec + } + } + } +} diff --git a/apps/iam/kinds/teambinding.cue b/apps/iam/kinds/teambinding.cue new file mode 100644 index 00000000000..3f0805b0307 --- /dev/null +++ b/apps/iam/kinds/teambinding.cue @@ -0,0 +1,24 @@ +package kinds + +import ( + "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" +) + +teambinding: { + kind: "TeamBinding" + pluralName: "TeamBindings" + current: "v0alpha1" + + codegen: { + ts: { enabled: false } + go: { enabled: true } + } + + versions: { + "v0alpha1": { + schema: { + spec: v0alpha1.TeamBindingSpec + } + } + } +} diff --git a/apps/iam/kinds/user.cue b/apps/iam/kinds/user.cue new file mode 100644 index 00000000000..f5e0f04377b --- /dev/null +++ b/apps/iam/kinds/user.cue @@ -0,0 +1,24 @@ +package kinds + +import ( + "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" +) + +user: { + kind: "User" + pluralName: "Users" + current: "v0alpha1" + + codegen: { + ts: { enabled: false } + go: { enabled: true } + } + + versions: { + "v0alpha1": { + schema: { + spec: v0alpha1.UserSpec + } + } + } +} diff --git a/apps/iam/kinds/v0alpha1/resourcepermission.cue b/apps/iam/kinds/v0alpha1/resourcepermission.cue index 4cb02c3c400..5cb8c4de5d0 100644 --- a/apps/iam/kinds/v0alpha1/resourcepermission.cue +++ b/apps/iam/kinds/v0alpha1/resourcepermission.cue @@ -21,5 +21,3 @@ ResourcePermission: { resource: #Resource permissions: [...#Permission] } - - diff --git a/apps/iam/kinds/v0alpha1/rolespec.cue b/apps/iam/kinds/v0alpha1/rolespec.cue index 46d6ce1f2c4..b7934f0174e 100644 --- a/apps/iam/kinds/v0alpha1/rolespec.cue +++ b/apps/iam/kinds/v0alpha1/rolespec.cue @@ -10,6 +10,7 @@ RoleSpec: { // Display name of the role title: string + description: string version: int group: string @@ -20,4 +21,3 @@ RoleSpec: { // created? // updated? } - diff --git a/apps/iam/kinds/v0alpha1/serviceaccountspec.cue b/apps/iam/kinds/v0alpha1/serviceaccountspec.cue new file mode 100644 index 00000000000..9474fc1a06f --- /dev/null +++ b/apps/iam/kinds/v0alpha1/serviceaccountspec.cue @@ -0,0 +1,6 @@ +package v0alpha1 + +ServiceAccountSpec: { + title: string + disabled: bool +} diff --git a/apps/iam/kinds/v0alpha1/teambindingspec.cue b/apps/iam/kinds/v0alpha1/teambindingspec.cue new file mode 100644 index 00000000000..3bd130c902d --- /dev/null +++ b/apps/iam/kinds/v0alpha1/teambindingspec.cue @@ -0,0 +1,20 @@ +package v0alpha1 + +TeamBindingSpec: { + #Subject: { + // uid of the identity + name: string + // permission of the identity in the team + permission: TeamPermission + } + + subjects: [...#Subject] + teamRef: TeamRef +} + +TeamRef:{ + // Name is the unique identifier for a team. + name: string +} + +TeamPermission: "admin" | "member" diff --git a/apps/iam/kinds/v0alpha1/teamspec.cue b/apps/iam/kinds/v0alpha1/teamspec.cue new file mode 100644 index 00000000000..392c5fedb4a --- /dev/null +++ b/apps/iam/kinds/v0alpha1/teamspec.cue @@ -0,0 +1,6 @@ +package v0alpha1 + +TeamSpec: { + title: string + email: string +} diff --git a/apps/iam/kinds/v0alpha1/userspec.cue b/apps/iam/kinds/v0alpha1/userspec.cue new file mode 100644 index 00000000000..8ddcf57151d --- /dev/null +++ b/apps/iam/kinds/v0alpha1/userspec.cue @@ -0,0 +1,13 @@ +package v0alpha1 + +UserSpec: { + disabled: bool + email: string + emailVerified: bool + grafanaAdmin: bool + login: string + name: string + provisioned: bool + + // What to do with salt, rands and password? +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go index eb2c1d52e40..ac0b41e1972 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go @@ -18,9 +18,10 @@ func NewCoreRolespecPermission() *CoreRolespecPermission { // +k8s:openapi-gen=true type CoreRoleSpec struct { // Display name of the role - Title string `json:"title"` - Version int64 `json:"version"` - Group string `json:"group"` + Title string `json:"title"` + Description string `json:"description"` + Version int64 `json:"version"` + Group string `json:"group"` // TODO: // delegatable?: bool // created? diff --git a/apps/iam/pkg/apis/iam/v0alpha1/extensions.go b/apps/iam/pkg/apis/iam/v0alpha1/extensions.go new file mode 100644 index 00000000000..c0c5a11f7e3 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/extensions.go @@ -0,0 +1,43 @@ +package v0alpha1 + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +func (u User) AuthID() string { + meta, err := utils.MetaAccessor(&u) + if err != nil { + return "" + } + // TODO: Workaround until we move all definitions + // After having all resource definitions here in the app, we can remove this + // and we need to change the List authorization to use the MetaAccessor and the GetDeprecatedInternalID method + //nolint:staticcheck + return fmt.Sprintf("%d", meta.GetDeprecatedInternalID()) +} + +func (s ServiceAccount) AuthID() string { + meta, err := utils.MetaAccessor(&s) + if err != nil { + return "" + } + // TODO: Workaround until we move all definitions + // After having all resource definitions here in the app, we can remove this + // and we need to change the List authorization to use the MetaAccessor and the GetDeprecatedInternalID method + //nolint:staticcheck + return fmt.Sprintf("%d", meta.GetDeprecatedInternalID()) +} + +func (t Team) AuthID() string { + meta, err := utils.MetaAccessor(&t) + if err != nil { + return "" + } + // TODO: Workaround until we move all definitions + // After having all resource definitions here in the app, we can remove this + // and we need to change the List authorization to use the MetaAccessor and the GetDeprecatedInternalID method + //nolint:staticcheck + return fmt.Sprintf("%d", meta.GetDeprecatedInternalID()) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go index 785d2d5724b..1ce8d65646f 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go @@ -18,9 +18,10 @@ func NewGlobalRolespecPermission() *GlobalRolespecPermission { // +k8s:openapi-gen=true type GlobalRoleSpec struct { // Display name of the role - Title string `json:"title"` - Version int64 `json:"version"` - Group string `json:"group"` + Title string `json:"title"` + Description string `json:"description"` + Version int64 `json:"version"` + Group string `json:"group"` // TODO: // delegatable?: bool // created? diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go index f1a165dba4e..5a8185cd25c 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go @@ -18,9 +18,10 @@ func NewRolespecPermission() *RolespecPermission { // +k8s:openapi-gen=true type RoleSpec struct { // Display name of the role - Title string `json:"title"` - Version int64 `json:"version"` - Group string `json:"group"` + Title string `json:"title"` + Description string `json:"description"` + Version int64 `json:"version"` + Group string `json:"group"` // TODO: // delegatable?: bool // created? diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_codec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_codec_gen.go new file mode 100644 index 00000000000..0f5b3e7783a --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// ServiceAccountJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ServiceAccountJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*ServiceAccountJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*ServiceAccountJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &ServiceAccountJSONCodec{} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_metadata_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_metadata_gen.go new file mode 100644 index 00000000000..3a846141f87 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type ServiceAccountMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewServiceAccountMetadata creates a new ServiceAccountMetadata object. +func NewServiceAccountMetadata() *ServiceAccountMetadata { + return &ServiceAccountMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go new file mode 100644 index 00000000000..081e244dd6e --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type ServiceAccount struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the ServiceAccount + Spec ServiceAccountSpec `json:"spec" yaml:"spec"` + + Status ServiceAccountStatus `json:"status" yaml:"status"` +} + +func (o *ServiceAccount) GetSpec() any { + return o.Spec +} + +func (o *ServiceAccount) SetSpec(spec any) error { + cast, ok := spec.(ServiceAccountSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *ServiceAccount) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *ServiceAccount) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *ServiceAccount) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(ServiceAccountStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type ServiceAccountStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *ServiceAccount) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *ServiceAccount) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *ServiceAccount) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *ServiceAccount) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *ServiceAccount) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *ServiceAccount) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *ServiceAccount) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *ServiceAccount) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *ServiceAccount) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *ServiceAccount) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *ServiceAccount) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *ServiceAccount) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ServiceAccount) DeepCopy() *ServiceAccount { + cpy := &ServiceAccount{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ServiceAccount) DeepCopyInto(dst *ServiceAccount) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &ServiceAccount{} + +// +k8s:openapi-gen=true +type ServiceAccountList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []ServiceAccount `json:"items" yaml:"items"` +} + +func (o *ServiceAccountList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ServiceAccountList) Copy() resource.ListObject { + cpy := &ServiceAccountList{ + TypeMeta: o.TypeMeta, + Items: make([]ServiceAccount, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*ServiceAccount); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ServiceAccountList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ServiceAccountList) SetItems(items []resource.Object) { + o.Items = make([]ServiceAccount, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*ServiceAccount) + } +} + +func (o *ServiceAccountList) DeepCopy() *ServiceAccountList { + cpy := &ServiceAccountList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *ServiceAccountList) DeepCopyInto(dst *ServiceAccountList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ServiceAccountList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { + cpy := &ServiceAccountSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *ServiceAccountSpec) DeepCopyInto(dst *ServiceAccountSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of ServiceAccountStatus +func (s *ServiceAccountStatus) DeepCopy() *ServiceAccountStatus { + cpy := &ServiceAccountStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies ServiceAccountStatus into another ServiceAccountStatus object +func (s *ServiceAccountStatus) DeepCopyInto(dst *ServiceAccountStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go new file mode 100644 index 00000000000..ea48c864739 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaServiceAccount = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ServiceAccount{}, &ServiceAccountList{}, resource.WithKind("ServiceAccount"), + resource.WithPlural("serviceaccounts"), resource.WithScope(resource.NamespacedScope)) + kindServiceAccount = resource.Kind{ + Schema: schemaServiceAccount, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ServiceAccountJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ServiceAccountKind() resource.Kind { + return kindServiceAccount +} + +// Schema returns a resource.SimpleSchema representation of ServiceAccount +func ServiceAccountSchema() *resource.SimpleSchema { + return schemaServiceAccount +} + +// Interface compliance checks +var _ resource.Schema = kindServiceAccount diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_spec_gen.go new file mode 100644 index 00000000000..a6bfffd53f9 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_spec_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type ServiceAccountSpec struct { + Title string `json:"title"` + Disabled bool `json:"disabled"` +} + +// NewServiceAccountSpec creates a new ServiceAccountSpec object. +func NewServiceAccountSpec() *ServiceAccountSpec { + return &ServiceAccountSpec{} +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_status_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_status_gen.go new file mode 100644 index 00000000000..8174df97c23 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type ServiceAccountstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State ServiceAccountStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewServiceAccountstatusOperatorState creates a new ServiceAccountstatusOperatorState object. +func NewServiceAccountstatusOperatorState() *ServiceAccountstatusOperatorState { + return &ServiceAccountstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type ServiceAccountStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]ServiceAccountstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewServiceAccountStatus creates a new ServiceAccountStatus object. +func NewServiceAccountStatus() *ServiceAccountStatus { + return &ServiceAccountStatus{} +} + +// +k8s:openapi-gen=true +type ServiceAccountStatusOperatorStateState string + +const ( + ServiceAccountStatusOperatorStateStateSuccess ServiceAccountStatusOperatorStateState = "success" + ServiceAccountStatusOperatorStateStateInProgress ServiceAccountStatusOperatorStateState = "in_progress" + ServiceAccountStatusOperatorStateStateFailed ServiceAccountStatusOperatorStateState = "failed" +) diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_codec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_codec_gen.go new file mode 100644 index 00000000000..fb0ede4ef1e --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// TeamJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type TeamJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*TeamJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*TeamJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &TeamJSONCodec{} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_metadata_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_metadata_gen.go new file mode 100644 index 00000000000..5e833327309 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type TeamMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewTeamMetadata creates a new TeamMetadata object. +func NewTeamMetadata() *TeamMetadata { + return &TeamMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go new file mode 100644 index 00000000000..bfc949acfd1 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Team struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Team + Spec TeamSpec `json:"spec" yaml:"spec"` + + Status TeamStatus `json:"status" yaml:"status"` +} + +func (o *Team) GetSpec() any { + return o.Spec +} + +func (o *Team) SetSpec(spec any) error { + cast, ok := spec.(TeamSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Team) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Team) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Team) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(TeamStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type TeamStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Team) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Team) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Team) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Team) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Team) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Team) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Team) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Team) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Team) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Team) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Team) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Team) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Team) DeepCopy() *Team { + cpy := &Team{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Team) DeepCopyInto(dst *Team) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Team{} + +// +k8s:openapi-gen=true +type TeamList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Team `json:"items" yaml:"items"` +} + +func (o *TeamList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *TeamList) Copy() resource.ListObject { + cpy := &TeamList{ + TypeMeta: o.TypeMeta, + Items: make([]Team, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Team); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *TeamList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *TeamList) SetItems(items []resource.Object) { + o.Items = make([]Team, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Team) + } +} + +func (o *TeamList) DeepCopy() *TeamList { + cpy := &TeamList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *TeamList) DeepCopyInto(dst *TeamList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &TeamList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *TeamSpec) DeepCopy() *TeamSpec { + cpy := &TeamSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *TeamSpec) DeepCopyInto(dst *TeamSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of TeamStatus +func (s *TeamStatus) DeepCopy() *TeamStatus { + cpy := &TeamStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies TeamStatus into another TeamStatus object +func (s *TeamStatus) DeepCopyInto(dst *TeamStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go new file mode 100644 index 00000000000..7a4875cf2d5 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaTeam = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &Team{}, &TeamList{}, resource.WithKind("Team"), + resource.WithPlural("teams"), resource.WithScope(resource.NamespacedScope)) + kindTeam = resource.Kind{ + Schema: schemaTeam, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &TeamJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func TeamKind() resource.Kind { + return kindTeam +} + +// Schema returns a resource.SimpleSchema representation of Team +func TeamSchema() *resource.SimpleSchema { + return schemaTeam +} + +// Interface compliance checks +var _ resource.Schema = kindTeam diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_spec_gen.go new file mode 100644 index 00000000000..994bfdd59c5 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_spec_gen.go @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type TeamSpec struct { + Title string `json:"title"` + Email string `json:"email"` +} + +// NewTeamSpec creates a new TeamSpec object. +func NewTeamSpec() *TeamSpec { + return &TeamSpec{} +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_status_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_status_gen.go new file mode 100644 index 00000000000..0690e938633 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type TeamstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State TeamStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewTeamstatusOperatorState creates a new TeamstatusOperatorState object. +func NewTeamstatusOperatorState() *TeamstatusOperatorState { + return &TeamstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type TeamStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]TeamstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewTeamStatus creates a new TeamStatus object. +func NewTeamStatus() *TeamStatus { + return &TeamStatus{} +} + +// +k8s:openapi-gen=true +type TeamStatusOperatorStateState string + +const ( + TeamStatusOperatorStateStateSuccess TeamStatusOperatorStateState = "success" + TeamStatusOperatorStateStateInProgress TeamStatusOperatorStateState = "in_progress" + TeamStatusOperatorStateStateFailed TeamStatusOperatorStateState = "failed" +) diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_codec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_codec_gen.go new file mode 100644 index 00000000000..358f530ca7b --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// TeamBindingJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type TeamBindingJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*TeamBindingJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*TeamBindingJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &TeamBindingJSONCodec{} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_metadata_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_metadata_gen.go new file mode 100644 index 00000000000..b1e7ab5f8ab --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type TeamBindingMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewTeamBindingMetadata creates a new TeamBindingMetadata object. +func NewTeamBindingMetadata() *TeamBindingMetadata { + return &TeamBindingMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go new file mode 100644 index 00000000000..6c9f6f407e3 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type TeamBinding struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the TeamBinding + Spec TeamBindingSpec `json:"spec" yaml:"spec"` + + Status TeamBindingStatus `json:"status" yaml:"status"` +} + +func (o *TeamBinding) GetSpec() any { + return o.Spec +} + +func (o *TeamBinding) SetSpec(spec any) error { + cast, ok := spec.(TeamBindingSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *TeamBinding) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *TeamBinding) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *TeamBinding) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(TeamBindingStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type TeamBindingStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *TeamBinding) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *TeamBinding) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *TeamBinding) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *TeamBinding) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *TeamBinding) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *TeamBinding) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *TeamBinding) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *TeamBinding) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *TeamBinding) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *TeamBinding) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *TeamBinding) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *TeamBinding) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *TeamBinding) DeepCopy() *TeamBinding { + cpy := &TeamBinding{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *TeamBinding) DeepCopyInto(dst *TeamBinding) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &TeamBinding{} + +// +k8s:openapi-gen=true +type TeamBindingList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []TeamBinding `json:"items" yaml:"items"` +} + +func (o *TeamBindingList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *TeamBindingList) Copy() resource.ListObject { + cpy := &TeamBindingList{ + TypeMeta: o.TypeMeta, + Items: make([]TeamBinding, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*TeamBinding); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *TeamBindingList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *TeamBindingList) SetItems(items []resource.Object) { + o.Items = make([]TeamBinding, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*TeamBinding) + } +} + +func (o *TeamBindingList) DeepCopy() *TeamBindingList { + cpy := &TeamBindingList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *TeamBindingList) DeepCopyInto(dst *TeamBindingList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &TeamBindingList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *TeamBindingSpec) DeepCopy() *TeamBindingSpec { + cpy := &TeamBindingSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *TeamBindingSpec) DeepCopyInto(dst *TeamBindingSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of TeamBindingStatus +func (s *TeamBindingStatus) DeepCopy() *TeamBindingStatus { + cpy := &TeamBindingStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies TeamBindingStatus into another TeamBindingStatus object +func (s *TeamBindingStatus) DeepCopyInto(dst *TeamBindingStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go new file mode 100644 index 00000000000..e3c2c11a8b5 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaTeamBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &TeamBinding{}, &TeamBindingList{}, resource.WithKind("TeamBinding"), + resource.WithPlural("teambindings"), resource.WithScope(resource.NamespacedScope)) + kindTeamBinding = resource.Kind{ + Schema: schemaTeamBinding, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &TeamBindingJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func TeamBindingKind() resource.Kind { + return kindTeamBinding +} + +// Schema returns a resource.SimpleSchema representation of TeamBinding +func TeamBindingSchema() *resource.SimpleSchema { + return schemaTeamBinding +} + +// Interface compliance checks +var _ resource.Schema = kindTeamBinding diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go new file mode 100644 index 00000000000..0e58323533e --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_spec_gen.go @@ -0,0 +1,49 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type TeamBindingspecSubject struct { + // uid of the identity + Name string `json:"name"` + // permission of the identity in the team + Permission TeamBindingTeamPermission `json:"permission"` +} + +// NewTeamBindingspecSubject creates a new TeamBindingspecSubject object. +func NewTeamBindingspecSubject() *TeamBindingspecSubject { + return &TeamBindingspecSubject{} +} + +// +k8s:openapi-gen=true +type TeamBindingTeamPermission string + +const ( + TeamBindingTeamPermissionAdmin TeamBindingTeamPermission = "admin" + TeamBindingTeamPermissionMember TeamBindingTeamPermission = "member" +) + +// +k8s:openapi-gen=true +type TeamBindingTeamRef struct { + // Name is the unique identifier for a team. + Name string `json:"name"` +} + +// NewTeamBindingTeamRef creates a new TeamBindingTeamRef object. +func NewTeamBindingTeamRef() *TeamBindingTeamRef { + return &TeamBindingTeamRef{} +} + +// +k8s:openapi-gen=true +type TeamBindingSpec struct { + Subjects []TeamBindingspecSubject `json:"subjects"` + TeamRef TeamBindingTeamRef `json:"teamRef"` +} + +// NewTeamBindingSpec creates a new TeamBindingSpec object. +func NewTeamBindingSpec() *TeamBindingSpec { + return &TeamBindingSpec{ + Subjects: []TeamBindingspecSubject{}, + TeamRef: *NewTeamBindingTeamRef(), + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_status_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_status_gen.go new file mode 100644 index 00000000000..221eb320ae7 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type TeamBindingstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State TeamBindingStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewTeamBindingstatusOperatorState creates a new TeamBindingstatusOperatorState object. +func NewTeamBindingstatusOperatorState() *TeamBindingstatusOperatorState { + return &TeamBindingstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type TeamBindingStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]TeamBindingstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewTeamBindingStatus creates a new TeamBindingStatus object. +func NewTeamBindingStatus() *TeamBindingStatus { + return &TeamBindingStatus{} +} + +// +k8s:openapi-gen=true +type TeamBindingStatusOperatorStateState string + +const ( + TeamBindingStatusOperatorStateStateSuccess TeamBindingStatusOperatorStateState = "success" + TeamBindingStatusOperatorStateStateInProgress TeamBindingStatusOperatorStateState = "in_progress" + TeamBindingStatusOperatorStateStateFailed TeamBindingStatusOperatorStateState = "failed" +) diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_codec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_codec_gen.go new file mode 100644 index 00000000000..b37a806a30d --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// UserJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type UserJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*UserJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*UserJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &UserJSONCodec{} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_metadata_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_metadata_gen.go new file mode 100644 index 00000000000..8b1e19a94ab --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type UserMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewUserMetadata creates a new UserMetadata object. +func NewUserMetadata() *UserMetadata { + return &UserMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go new file mode 100644 index 00000000000..ce2defa33a8 --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type User struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the User + Spec UserSpec `json:"spec" yaml:"spec"` + + Status UserStatus `json:"status" yaml:"status"` +} + +func (o *User) GetSpec() any { + return o.Spec +} + +func (o *User) SetSpec(spec any) error { + cast, ok := spec.(UserSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *User) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *User) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *User) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(UserStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type UserStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *User) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *User) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *User) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *User) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *User) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *User) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *User) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *User) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *User) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *User) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *User) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *User) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *User) DeepCopy() *User { + cpy := &User{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *User) DeepCopyInto(dst *User) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &User{} + +// +k8s:openapi-gen=true +type UserList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []User `json:"items" yaml:"items"` +} + +func (o *UserList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *UserList) Copy() resource.ListObject { + cpy := &UserList{ + TypeMeta: o.TypeMeta, + Items: make([]User, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*User); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *UserList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *UserList) SetItems(items []resource.Object) { + o.Items = make([]User, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*User) + } +} + +func (o *UserList) DeepCopy() *UserList { + cpy := &UserList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *UserList) DeepCopyInto(dst *UserList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &UserList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *UserSpec) DeepCopy() *UserSpec { + cpy := &UserSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *UserSpec) DeepCopyInto(dst *UserSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of UserStatus +func (s *UserStatus) DeepCopy() *UserStatus { + cpy := &UserStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies UserStatus into another UserStatus object +func (s *UserStatus) DeepCopyInto(dst *UserStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go new file mode 100644 index 00000000000..a44d9dfcf2d --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaUser = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &User{}, &UserList{}, resource.WithKind("User"), + resource.WithPlural("users"), resource.WithScope(resource.NamespacedScope)) + kindUser = resource.Kind{ + Schema: schemaUser, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &UserJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func UserKind() resource.Kind { + return kindUser +} + +// Schema returns a resource.SimpleSchema representation of User +func UserSchema() *resource.SimpleSchema { + return schemaUser +} + +// Interface compliance checks +var _ resource.Schema = kindUser diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go new file mode 100644 index 00000000000..8e83f1d8b5d --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_spec_gen.go @@ -0,0 +1,20 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type UserSpec struct { + Disabled bool `json:"disabled"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + GrafanaAdmin bool `json:"grafanaAdmin"` + Login string `json:"login"` + Name string `json:"name"` + // What to do with salt, rands and password? + Provisioned bool `json:"provisioned"` +} + +// NewUserSpec creates a new UserSpec object. +func NewUserSpec() *UserSpec { + return &UserSpec{} +} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go new file mode 100644 index 00000000000..34a138f59ce --- /dev/null +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type UserstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State UserStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewUserstatusOperatorState creates a new UserstatusOperatorState object. +func NewUserstatusOperatorState() *UserstatusOperatorState { + return &UserstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type UserStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]UserstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewUserStatus creates a new UserStatus object. +func NewUserStatus() *UserStatus { + return &UserStatus{} +} + +// +k8s:openapi-gen=true +type UserStatusOperatorStateState string + +const ( + UserStatusOperatorStateStateSuccess UserStatusOperatorStateState = "success" + UserStatusOperatorStateStateInProgress UserStatusOperatorStateState = "in_progress" + UserStatusOperatorStateStateFailed UserStatusOperatorStateState = "failed" +) diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index df62f68072a..7a0703d53e3 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -51,6 +51,28 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus": schema_pkg_apis_iam_v0alpha1_RoleStatus(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolespecPermission": schema_pkg_apis_iam_v0alpha1_RolespecPermission(ref), "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolestatusOperatorState": schema_pkg_apis_iam_v0alpha1_RolestatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus": schema_pkg_apis_iam_v0alpha1_ServiceAccountStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState": schema_pkg_apis_iam_v0alpha1_ServiceAccountstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus": schema_pkg_apis_iam_v0alpha1_TeamBindingStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef": schema_pkg_apis_iam_v0alpha1_TeamBindingTeamRef(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": schema_pkg_apis_iam_v0alpha1_UserStatus(ref), + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref), } } @@ -164,6 +186,13 @@ func schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref common.ReferenceCallback) com Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Default: 0, @@ -193,7 +222,7 @@ func schema_pkg_apis_iam_v0alpha1_CoreRoleSpec(ref common.ReferenceCallback) com }, }, }, - Required: []string{"title", "version", "group", "permissions"}, + Required: []string{"title", "description", "version", "group", "permissions"}, }, }, Dependencies: []string{ @@ -718,6 +747,13 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref common.ReferenceCallback) c Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Default: 0, @@ -747,7 +783,7 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleSpec(ref common.ReferenceCallback) c }, }, }, - Required: []string{"title", "version", "group", "permissions"}, + Required: []string{"title", "description", "version", "group", "permissions"}, }, }, Dependencies: []string{ @@ -1578,6 +1614,13 @@ func schema_pkg_apis_iam_v0alpha1_RoleSpec(ref common.ReferenceCallback) common. Format: "", }, }, + "description": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "version": { SchemaProps: spec.SchemaProps{ Default: 0, @@ -1607,7 +1650,7 @@ func schema_pkg_apis_iam_v0alpha1_RoleSpec(ref common.ReferenceCallback) common. }, }, }, - Required: []string{"title", "version", "group", "permissions"}, + Required: []string{"title", "description", "version", "group", "permissions"}, }, }, Dependencies: []string{ @@ -1738,3 +1781,968 @@ func schema_pkg_apis_iam_v0alpha1_RolestatusOperatorState(ref common.ReferenceCa }, } } + +func schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the ServiceAccount", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disabled": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"title", "disabled"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_ServiceAccountStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_ServiceAccountstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_Team(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Team", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBinding(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the TeamBinding", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "subjects": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject"), + }, + }, + }, + }, + }, + "teamRef": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef"), + }, + }, + }, + Required: []string{"subjects", "teamRef"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBindingStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBindingTeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the unique identifier for a team.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBindingspecSubject(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "uid of the identity", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "permission": { + SchemaProps: spec.SchemaProps{ + Description: "permission of the identity in the team", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "permission"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamBindingstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"title", "email"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_User(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the User", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_UserList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "disabled": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "emailVerified": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "grafanaAdmin": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "login": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "provisioned": { + SchemaProps: spec.SchemaProps{ + Description: "What to do with salt, rands and password?", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"disabled", "email", "emailVerified", "grafanaAdmin", "login", "name", "provisioned"}, + }, + }, + } +} + +func schema_pkg_apis_iam_v0alpha1_UserStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState"}, + } +} + +func schema_pkg_apis_iam_v0alpha1_UserstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index b80f5e5eee3..667b7a10354 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -83,6 +83,50 @@ var appManifestData = app.ManifestData{ }, }, }, + + { + Kind: "User", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v0alpha1", + }, + }, + }, + + { + Kind: "Team", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v0alpha1", + }, + }, + }, + + { + Kind: "TeamBinding", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v0alpha1", + }, + }, + }, + + { + Kind: "ServiceAccount", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v0alpha1", + }, + }, + }, }, } @@ -101,6 +145,10 @@ var kindVersionToGoType = map[string]resource.Kind{ "Role/v0alpha1": v0alpha1.RoleKind(), "RoleBinding/v0alpha1": v0alpha1.RoleBindingKind(), "ResourcePermission/v0alpha1": v0alpha1.ResourcePermissionKind(), + "User/v0alpha1": v0alpha1.UserKind(), + "Team/v0alpha1": v0alpha1.TeamKind(), + "TeamBinding/v0alpha1": v0alpha1.TeamBindingKind(), + "ServiceAccount/v0alpha1": v0alpha1.ServiceAccountKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 618ea42c9a9..47c762b5a24 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,9 +3,9 @@ module github.com/grafana/grafana/apps/playlist go 1.24.4 require ( - github.com/grafana/grafana-app-sdk v0.39.0 - k8s.io/apimachinery v0.33.1 - k8s.io/client-go v0.33.1 + github.com/grafana/grafana-app-sdk v0.39.2 + k8s.io/apimachinery v0.33.2 + k8s.io/client-go v0.33.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) @@ -31,7 +31,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.38.2 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -73,14 +73,14 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect + k8s.io/api v0.33.2 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 8726cf84afc..9cbf8457905 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -47,10 +47,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/grafana-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yzbjm6T1T1w= -github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -189,10 +189,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -206,14 +206,14 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/apps/secret/Makefile b/apps/secret/Makefile new file mode 100644 index 00000000000..d829f94e596 --- /dev/null +++ b/apps/secret/Makefile @@ -0,0 +1,29 @@ +APP_SDK_VERSION := v0.39.2 +APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) +APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk + +.PHONY: install-app-sdk +install-app-sdk: $(APP_SDK_BIN) ## Install the Grafana App SDK + +$(APP_SDK_BIN): + @echo "Installing Grafana App SDK version $(APP_SDK_VERSION)" + @mkdir -p $(APP_SDK_DIR) + # The only way to install specific versions of binaries using `go install` + # is by setting GOBIN to the directory you want to install the binary to. + GOBIN=$(APP_SDK_DIR) go install github.com/grafana/grafana-app-sdk/cmd/grafana-app-sdk@$(APP_SDK_VERSION) + @touch $@ + +.PHONY: update-app-sdk +update-app-sdk: ## Update the Grafana App SDK dependency in go.mod + go get github.com/grafana/grafana-app-sdk@$(APP_SDK_VERSION) + go mod tidy + +.PHONY: generate +generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none \ + --noschemasinmanifest \ + --postprocess \ No newline at end of file diff --git a/pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml b/apps/secret/decrypt/v1beta1/buf.gen.yaml similarity index 68% rename from pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml rename to apps/secret/decrypt/v1beta1/buf.gen.yaml index f725fb6452d..b2691bae669 100644 --- a/pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml +++ b/apps/secret/decrypt/v1beta1/buf.gen.yaml @@ -2,11 +2,11 @@ version: v1 plugins: - plugin: go - out: pkg/apis/secret/v0alpha1/decrypt + out: apps/secret/decrypt/v1beta1 opt: - paths=source_relative - plugin: go-grpc - out: pkg/apis/secret/v0alpha1/decrypt + out: apps/secret/decrypt/v1beta1 opt: - paths=source_relative - require_unimplemented_servers=false diff --git a/pkg/apis/secret/v0alpha1/decrypt/buf.yaml b/apps/secret/decrypt/v1beta1/buf.yaml similarity index 100% rename from pkg/apis/secret/v0alpha1/decrypt/buf.yaml rename to apps/secret/decrypt/v1beta1/buf.yaml diff --git a/pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go b/apps/secret/decrypt/v1beta1/decrypt.pb.go similarity index 60% rename from pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go rename to apps/secret/decrypt/v1beta1/decrypt.pb.go index cffa2065ef0..4e334ff7ca3 100644 --- a/pkg/apis/secret/v0alpha1/decrypt/decrypt.pb.go +++ b/apps/secret/decrypt/v1beta1/decrypt.pb.go @@ -4,7 +4,7 @@ // protoc (unknown) // source: decrypt.proto -package decrypt +package decryptv1beta1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -208,45 +208,47 @@ var File_decrypt_proto protoreflect.FileDescriptor var file_decrypt_proto_rawDesc = string([]byte{ 0x0a, 0x0d, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x07, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x22, 0x4f, 0x0a, 0x19, 0x53, 0x65, 0x63, 0x75, - 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xea, 0x01, 0x0a, 0x24, 0x53, 0x65, - 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x6d, 0x0a, 0x10, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x64, - 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x0f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x1a, 0x53, 0x0a, 0x14, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x12, 0x16, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, - 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x80, 0x01, 0x0a, 0x14, 0x53, 0x65, - 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x72, 0x12, 0x68, 0x0a, 0x13, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x53, 0x65, 0x63, - 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x64, 0x65, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, - 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, - 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x3d, 0x5a, 0x3b, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, - 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, - 0x70, 0x69, 0x73, 0x2f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x76, 0x30, 0x61, 0x6c, 0x70, - 0x68, 0x61, 0x31, 0x2f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x22, + 0x4f, 0x0a, 0x19, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x22, 0xf8, 0x01, 0x0a, 0x24, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x74, 0x0a, 0x10, 0x64, 0x65, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, + 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, + 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, + 0x5a, 0x0a, 0x14, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x06, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, + 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x8e, + 0x01, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x72, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x29, + 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, + 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x64, 0x65, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x61, 0x70, + 0x70, 0x73, 0x2f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x3b, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -263,16 +265,16 @@ func file_decrypt_proto_rawDescGZIP() []byte { var file_decrypt_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_decrypt_proto_goTypes = []any{ - (*SecureValueDecryptRequest)(nil), // 0: decrypt.SecureValueDecryptRequest - (*SecureValueDecryptResponseCollection)(nil), // 1: decrypt.SecureValueDecryptResponseCollection - (*Result)(nil), // 2: decrypt.Result - nil, // 3: decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry + (*SecureValueDecryptRequest)(nil), // 0: decryptv1beta1.SecureValueDecryptRequest + (*SecureValueDecryptResponseCollection)(nil), // 1: decryptv1beta1.SecureValueDecryptResponseCollection + (*Result)(nil), // 2: decryptv1beta1.Result + nil, // 3: decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry } var file_decrypt_proto_depIdxs = []int32{ - 3, // 0: decrypt.SecureValueDecryptResponseCollection.decrypted_values:type_name -> decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry - 2, // 1: decrypt.SecureValueDecryptResponseCollection.DecryptedValuesEntry.value:type_name -> decrypt.Result - 0, // 2: decrypt.SecureValueDecrypter.DecryptSecureValues:input_type -> decrypt.SecureValueDecryptRequest - 1, // 3: decrypt.SecureValueDecrypter.DecryptSecureValues:output_type -> decrypt.SecureValueDecryptResponseCollection + 3, // 0: decryptv1beta1.SecureValueDecryptResponseCollection.decrypted_values:type_name -> decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry + 2, // 1: decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry.value:type_name -> decryptv1beta1.Result + 0, // 2: decryptv1beta1.SecureValueDecrypter.DecryptSecureValues:input_type -> decryptv1beta1.SecureValueDecryptRequest + 1, // 3: decryptv1beta1.SecureValueDecrypter.DecryptSecureValues:output_type -> decryptv1beta1.SecureValueDecryptResponseCollection 3, // [3:4] is the sub-list for method output_type 2, // [2:3] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name diff --git a/pkg/apis/secret/v0alpha1/decrypt/decrypt.proto b/apps/secret/decrypt/v1beta1/decrypt.proto similarity index 88% rename from pkg/apis/secret/v0alpha1/decrypt/decrypt.proto rename to apps/secret/decrypt/v1beta1/decrypt.proto index bf328ff5217..a8ee64de420 100644 --- a/pkg/apis/secret/v0alpha1/decrypt/decrypt.proto +++ b/apps/secret/decrypt/v1beta1/decrypt.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -package decrypt; +package decryptv1beta1; -option go_package = "github.com/grafana/grafana/pkg/apis/secret/v0alpha1/decrypt"; +option go_package = "github.com/grafana/grafana/apps/secret/decrypt/v1beta1;decryptv1beta1"; message SecureValueDecryptRequest { // The stack_id or org_id. diff --git a/pkg/apis/secret/v0alpha1/decrypt/decrypt_grpc.pb.go b/apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go similarity index 97% rename from pkg/apis/secret/v0alpha1/decrypt/decrypt_grpc.pb.go rename to apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go index 9b14a013678..92092215b58 100644 --- a/pkg/apis/secret/v0alpha1/decrypt/decrypt_grpc.pb.go +++ b/apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go @@ -4,7 +4,7 @@ // - protoc (unknown) // source: decrypt.proto -package decrypt +package decryptv1beta1 import ( context "context" @@ -19,7 +19,7 @@ import ( const _ = grpc.SupportPackageIsVersion8 const ( - SecureValueDecrypter_DecryptSecureValues_FullMethodName = "/decrypt.SecureValueDecrypter/DecryptSecureValues" + SecureValueDecrypter_DecryptSecureValues_FullMethodName = "/decryptv1beta1.SecureValueDecrypter/DecryptSecureValues" ) // SecureValueDecrypterClient is the client API for SecureValueDecrypter service. @@ -97,7 +97,7 @@ func _SecureValueDecrypter_DecryptSecureValues_Handler(srv interface{}, ctx cont // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var SecureValueDecrypter_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "decrypt.SecureValueDecrypter", + ServiceName: "decryptv1beta1.SecureValueDecrypter", HandlerType: (*SecureValueDecrypterServer)(nil), Methods: []grpc.MethodDesc{ { diff --git a/apps/secret/go.mod b/apps/secret/go.mod new file mode 100644 index 00000000000..0c6f4df1e16 --- /dev/null +++ b/apps/secret/go.mod @@ -0,0 +1,66 @@ +module github.com/grafana/grafana/apps/secret + +go 1.24.4 + +require ( + github.com/grafana/grafana-app-sdk v0.39.2 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf + github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.73.0 + google.golang.org/protobuf v1.36.6 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.33.2 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/getkin/kin-openapi v0.132.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/client-go v0.33.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/apps/secret/go.sum b/apps/secret/go.sum new file mode 100644 index 00000000000..fb88ea01837 --- /dev/null +++ b/apps/secret/go.sum @@ -0,0 +1,189 @@ +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +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 v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/apps/secret/kinds/cue.mod/module.cue b/apps/secret/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..bad5ef304e1 --- /dev/null +++ b/apps/secret/kinds/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "github.com/grafana/grafana/apps/secret/kinds" +language: { + version: "v0.9.0" +} diff --git a/apps/secret/kinds/keeper.cue b/apps/secret/kinds/keeper.cue new file mode 100644 index 00000000000..117e3bbd8a3 --- /dev/null +++ b/apps/secret/kinds/keeper.cue @@ -0,0 +1,26 @@ +package secret + +import "github.com/grafana/grafana/apps/secret/kinds/v1beta1" + +keeper: { + kind: "Keeper" + pluralName: "Keepers" + current: "v1beta1" + scope: "Namespaced" + current: "v1beta1" + codegen: { + ts: { + enabled: false + } + go: { + enabled: true + } + } + versions: { + "v1beta1": { + schema: { + spec: v1beta1.KeeperSpec + } + } + } +} diff --git a/apps/secret/kinds/manifest.cue b/apps/secret/kinds/manifest.cue new file mode 100644 index 00000000000..1f288bcd328 --- /dev/null +++ b/apps/secret/kinds/manifest.cue @@ -0,0 +1,10 @@ +package secret + +manifest: { + appName: "secret" + groupOverride: "secret.grafana.app" + kinds: [ + securevalue, + keeper, + ] +} diff --git a/apps/secret/kinds/securevalue.cue b/apps/secret/kinds/securevalue.cue new file mode 100644 index 00000000000..6bed093f46d --- /dev/null +++ b/apps/secret/kinds/securevalue.cue @@ -0,0 +1,27 @@ +package secret + +import "github.com/grafana/grafana/apps/secret/kinds/v1beta1" + +securevalue: { + kind: "SecureValue" + pluralName: "SecureValues" + current: "v1beta1" + scope: "Namespaced" + current: "v1beta1" + codegen: { + ts: { + enabled: false + } + go: { + enabled: true + } + } + versions: { + "v1beta1": { + schema: { + spec: v1beta1.SecureValueSpec + status: v1beta1.SecureValueStatus + } + } + } +} diff --git a/apps/secret/kinds/v1beta1/keeper.cue b/apps/secret/kinds/v1beta1/keeper.cue new file mode 100644 index 00000000000..36e20198ab7 --- /dev/null +++ b/apps/secret/kinds/v1beta1/keeper.cue @@ -0,0 +1,68 @@ +package v1beta1 + +import "strings" + +KeeperSpec: { + // Short description for the Keeper. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + description: string & strings.MinRunes(1) & strings.MaxRunes(253) + + // AWS Keeper Configuration. + // +structType=atomic + // +optional + aws?: #AWSConfig + + // Azure Keeper Configuration. + // +structType=atomic + // +optional + azure?: #AzureConfig + + // GCP Keeper Configuration. + // +structType=atomic + // +optional + gcp?: #GCPConfig + + // HashiCorp Vault Keeper Configuration. + // +structType=atomic + // +optional + hashiCorpVault?: #HashiCorpConfig +} + +#AWSConfig: { + accessKeyID: #CredentialValue + secretAccessKey: #CredentialValue + kmsKeyID?: string +} + +#AzureConfig: { + keyVaultName: string + tenantID: string + clientID: string + clientSecret: #CredentialValue +} + +#GCPConfig: { + projectID: string + credentialsFile: string +} + +#HashiCorpConfig: { + address: string + token: #CredentialValue +} + +#CredentialValue: { + // The name of the secure value that holds the actual value. + // +optional + secureValueName: string + + // The value is taken from the environment variable. + // +optional + valueFromEnv: string + + // The value is taken from the Grafana config file. + // TODO: how do we explain that this is a path to the config file? + // +optional + valueFromConfig: string +} diff --git a/apps/secret/kinds/v1beta1/securevalue.cue b/apps/secret/kinds/v1beta1/securevalue.cue new file mode 100644 index 00000000000..9fef8e63f46 --- /dev/null +++ b/apps/secret/kinds/v1beta1/securevalue.cue @@ -0,0 +1,53 @@ +package v1beta1 + +import ( + "list" + "strings" +) + +// ExposedSecureValue contains the raw decrypted secure value. +#ExposedSecureValue: string + +SecureValueSpec: { + // Short description that explains the purpose of this SecureValue. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=25 + description: string & strings.MinRunes(1) & strings.MaxRunes(25) + + // The raw value is only valid for write. Read/List will always be empty. + // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. + // Minimum and maximum lengths in bytes. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=24576 + // +optional + value?: #ExposedSecureValue + + // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. + // This should not contain sensitive information. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=1024 + // +optional + ref?: string & strings.MinRunes(1) & strings.MaxRunes(1024) + + // Name of the keeper, being the actual storage of the secure value. + // If not specified, the default keeper for the namespace will be used. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + // +optional + keeper?: string & strings.MinRunes(1) & strings.MaxRunes(253) + + // The Decrypters that are allowed to decrypt this secret. + // An empty list means no service can decrypt it. + // +k8s:validation:maxItems=64 + // +k8s:validation:uniqueItems=true + // +listType=atomic + // +optional + decrypters?: [...string] & list.UniqueItems() & list.MaxItems(64) +} + +SecureValueStatus: { + version: int64 & >=0 + + // +optional + externalID: string +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/constants.go b/apps/secret/pkg/apis/secret/v1beta1/constants.go new file mode 100644 index 00000000000..2db86acf35b --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/constants.go @@ -0,0 +1,18 @@ +package v1beta1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "secret.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1beta1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/pkg/apis/secret/v0alpha1/exposed_secure_value.go b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go similarity index 91% rename from pkg/apis/secret/v0alpha1/exposed_secure_value.go rename to apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go index 1f77cca2112..a8738c3093a 100644 --- a/pkg/apis/secret/v0alpha1/exposed_secure_value.go +++ b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go @@ -1,4 +1,8 @@ -package v0alpha1 +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 import ( "encoding/json" @@ -11,7 +15,7 @@ import ( const redacted = "[REDACTED]" // ExposedSecureValue contains the raw decrypted secure value. -type ExposedSecureValue string +type ExposedSecureValue = SecureValueExposedSecureValue var ( _ fmt.Stringer = (*ExposedSecureValue)(nil) diff --git a/pkg/apis/secret/v0alpha1/exposed_secure_value_test.go b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go similarity index 89% rename from pkg/apis/secret/v0alpha1/exposed_secure_value_test.go rename to apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go index 1d21e0505bc..90d53bdf7ae 100644 --- a/pkg/apis/secret/v0alpha1/exposed_secure_value_test.go +++ b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go @@ -1,4 +1,8 @@ -package v0alpha1_test +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 import ( "bytes" @@ -6,7 +10,6 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -15,7 +18,7 @@ func TestExposedSecureValue(t *testing.T) { expected := "[REDACTED]" rawValue := "a-password" - esv := v0alpha1.NewExposedSecureValue(rawValue) + esv := NewExposedSecureValue(rawValue) // String must not return the exposed secure value. require.Equal(t, expected, esv.String()) diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go new file mode 100644 index 00000000000..1a4f6fb2704 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// KeeperJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type KeeperJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*KeeperJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*KeeperJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &KeeperJSONCodec{} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go new file mode 100644 index 00000000000..82bd4258403 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type KeeperMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewKeeperMetadata creates a new KeeperMetadata object. +func NewKeeperMetadata() *KeeperMetadata { + return &KeeperMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go new file mode 100644 index 00000000000..f75d6299345 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Keeper struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Keeper + Spec KeeperSpec `json:"spec" yaml:"spec"` + + Status KeeperStatus `json:"status" yaml:"status"` +} + +func (o *Keeper) GetSpec() any { + return o.Spec +} + +func (o *Keeper) SetSpec(spec any) error { + cast, ok := spec.(KeeperSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Keeper) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Keeper) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Keeper) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(KeeperStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type KeeperStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Keeper) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Keeper) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Keeper) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Keeper) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Keeper) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Keeper) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Keeper) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Keeper) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Keeper) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Keeper) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Keeper) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Keeper) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Keeper) DeepCopy() *Keeper { + cpy := &Keeper{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Keeper) DeepCopyInto(dst *Keeper) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Keeper{} + +// +k8s:openapi-gen=true +type KeeperList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Keeper `json:"items" yaml:"items"` +} + +func (o *KeeperList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *KeeperList) Copy() resource.ListObject { + cpy := &KeeperList{ + TypeMeta: o.TypeMeta, + Items: make([]Keeper, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Keeper); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *KeeperList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *KeeperList) SetItems(items []resource.Object) { + o.Items = make([]Keeper, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Keeper) + } +} + +func (o *KeeperList) DeepCopy() *KeeperList { + cpy := &KeeperList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *KeeperList) DeepCopyInto(dst *KeeperList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &KeeperList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *KeeperSpec) DeepCopy() *KeeperSpec { + cpy := &KeeperSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *KeeperSpec) DeepCopyInto(dst *KeeperSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of KeeperStatus +func (s *KeeperStatus) DeepCopy() *KeeperStatus { + cpy := &KeeperStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies KeeperStatus into another KeeperStatus object +func (s *KeeperStatus) DeepCopyInto(dst *KeeperStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go new file mode 100644 index 00000000000..b2ef185503e --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaKeeper = resource.NewSimpleSchema("secret.grafana.app", "v1beta1", &Keeper{}, &KeeperList{}, resource.WithKind("Keeper"), + resource.WithPlural("keepers"), resource.WithScope(resource.NamespacedScope)) + kindKeeper = resource.Kind{ + Schema: schemaKeeper, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &KeeperJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func KeeperKind() resource.Kind { + return kindKeeper +} + +// Schema returns a resource.SimpleSchema representation of Keeper +func KeeperSchema() *resource.SimpleSchema { + return schemaKeeper +} + +// Interface compliance checks +var _ resource.Schema = kindKeeper diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go new file mode 100644 index 00000000000..76d232465c8 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go @@ -0,0 +1,105 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// +k8s:openapi-gen=true +type KeeperAWSConfig struct { + AccessKeyID KeeperCredentialValue `json:"accessKeyID"` + SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"` + KmsKeyID *string `json:"kmsKeyID,omitempty"` +} + +// NewKeeperAWSConfig creates a new KeeperAWSConfig object. +func NewKeeperAWSConfig() *KeeperAWSConfig { + return &KeeperAWSConfig{ + AccessKeyID: *NewKeeperCredentialValue(), + SecretAccessKey: *NewKeeperCredentialValue(), + } +} + +// +k8s:openapi-gen=true +type KeeperCredentialValue struct { + // The name of the secure value that holds the actual value. + // +optional + SecureValueName string `json:"secureValueName"` + // The value is taken from the environment variable. + // +optional + ValueFromEnv string `json:"valueFromEnv"` + // The value is taken from the Grafana config file. + // TODO: how do we explain that this is a path to the config file? + // +optional + ValueFromConfig string `json:"valueFromConfig"` +} + +// NewKeeperCredentialValue creates a new KeeperCredentialValue object. +func NewKeeperCredentialValue() *KeeperCredentialValue { + return &KeeperCredentialValue{} +} + +// +k8s:openapi-gen=true +type KeeperAzureConfig struct { + KeyVaultName string `json:"keyVaultName"` + TenantID string `json:"tenantID"` + ClientID string `json:"clientID"` + ClientSecret KeeperCredentialValue `json:"clientSecret"` +} + +// NewKeeperAzureConfig creates a new KeeperAzureConfig object. +func NewKeeperAzureConfig() *KeeperAzureConfig { + return &KeeperAzureConfig{ + ClientSecret: *NewKeeperCredentialValue(), + } +} + +// +k8s:openapi-gen=true +type KeeperGCPConfig struct { + ProjectID string `json:"projectID"` + CredentialsFile string `json:"credentialsFile"` +} + +// NewKeeperGCPConfig creates a new KeeperGCPConfig object. +func NewKeeperGCPConfig() *KeeperGCPConfig { + return &KeeperGCPConfig{} +} + +// +k8s:openapi-gen=true +type KeeperHashiCorpConfig struct { + Address string `json:"address"` + Token KeeperCredentialValue `json:"token"` +} + +// NewKeeperHashiCorpConfig creates a new KeeperHashiCorpConfig object. +func NewKeeperHashiCorpConfig() *KeeperHashiCorpConfig { + return &KeeperHashiCorpConfig{ + Token: *NewKeeperCredentialValue(), + } +} + +// +k8s:openapi-gen=true +type KeeperSpec struct { + // Short description for the Keeper. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + Description string `json:"description"` + // AWS Keeper Configuration. + // +structType=atomic + // +optional + Aws *KeeperAWSConfig `json:"aws,omitempty"` + // Azure Keeper Configuration. + // +structType=atomic + // +optional + Azure *KeeperAzureConfig `json:"azure,omitempty"` + // GCP Keeper Configuration. + // +structType=atomic + // +optional + Gcp *KeeperGCPConfig `json:"gcp,omitempty"` + // HashiCorp Vault Keeper Configuration. + // +structType=atomic + // +optional + HashiCorpVault *KeeperHashiCorpConfig `json:"hashiCorpVault,omitempty"` +} + +// NewKeeperSpec creates a new KeeperSpec object. +func NewKeeperSpec() *KeeperSpec { + return &KeeperSpec{} +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go new file mode 100644 index 00000000000..033b71b183b --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// +k8s:openapi-gen=true +type KeeperstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State KeeperStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewKeeperstatusOperatorState creates a new KeeperstatusOperatorState object. +func NewKeeperstatusOperatorState() *KeeperstatusOperatorState { + return &KeeperstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type KeeperStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]KeeperstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewKeeperStatus creates a new KeeperStatus object. +func NewKeeperStatus() *KeeperStatus { + return &KeeperStatus{} +} + +// +k8s:openapi-gen=true +type KeeperStatusOperatorStateState string + +const ( + KeeperStatusOperatorStateStateSuccess KeeperStatusOperatorStateState = "success" + KeeperStatusOperatorStateStateInProgress KeeperStatusOperatorStateState = "in_progress" + KeeperStatusOperatorStateStateFailed KeeperStatusOperatorStateState = "failed" +) diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go new file mode 100644 index 00000000000..0feeac75dd1 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go @@ -0,0 +1,63 @@ +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 + +// KeeperType represents the type of a Keeper. +type KeeperType string + +const ( + AWSKeeperType KeeperType = "aws" + AzureKeeperType KeeperType = "azure" + GCPKeeperType KeeperType = "gcp" + HashiCorpKeeperType KeeperType = "hashicorp" +) + +func (kt KeeperType) String() string { + return string(kt) +} + +// KeeperConfig is an interface that all keeper config types must implement. +type KeeperConfig interface { + Type() KeeperType +} + +func (s *KeeperSpec) GetType() KeeperType { + if s.Aws != nil { + return AWSKeeperType + } + if s.Azure != nil { + return AzureKeeperType + } + if s.Gcp != nil { + return GCPKeeperType + } + if s.HashiCorpVault != nil { + return HashiCorpKeeperType + } + return "" +} + +// System Keeper. +type SystemKeeperConfig struct{} + +func (*SystemKeeperConfig) Type() KeeperType { + return "system" +} + +func (s *KeeperAWSConfig) Type() KeeperType { + return AWSKeeperType +} + +func (s *KeeperAzureConfig) Type() KeeperType { + return AzureKeeperType +} + +func (s *KeeperGCPConfig) Type() KeeperType { + return GCPKeeperType +} + +func (s *KeeperHashiCorpConfig) Type() KeeperType { + return HashiCorpKeeperType +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/register.go b/apps/secret/pkg/apis/secret/v1beta1/register.go new file mode 100644 index 00000000000..32d89abf7d5 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/register.go @@ -0,0 +1,73 @@ +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +var SecureValuesResourceInfo = utils.NewResourceInfo( + APIGroup, APIVersion, + "securevalues", "securevalue", "SecureValue", + func() runtime.Object { return &SecureValue{} }, + func() runtime.Object { return &SecureValueList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"}, + {Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"}, + {Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"}, + }, + Reader: func(obj any) ([]any, error) { + if r, ok := obj.(*SecureValue); ok { + return []any{r.Name, r.Spec.Description, r.Spec.Keeper, r.Spec.Ref}, nil + } + + return nil, fmt.Errorf("expected SecureValue but got %T", obj) + }, + }, +) + +var KeeperResourceInfo = utils.NewResourceInfo( + APIGroup, APIVersion, + "keepers", "keeper", "Keeper", + func() runtime.Object { return &Keeper{} }, + func() runtime.Object { return &KeeperList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Description", Type: "string", Format: "string", Description: "Short description for the Keeper"}, + }, + Reader: func(obj any) ([]any, error) { + if r, ok := obj.(*Keeper); ok { + return []any{r.Name, r.Spec.Description}, nil + } + + return nil, fmt.Errorf("expected Keeper but got %T", obj) + }, + }, +) + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: APIGroup, Version: APIVersion} + +// Adds the list of known types to the given scheme. +func AddKnownTypes(scheme *runtime.Scheme, version string) error { + scheme.AddKnownTypes( + schema.GroupVersion{Group: APIGroup, Version: version}, + &SecureValue{}, + &SecureValueList{}, + &Keeper{}, + &KeeperList{}, + ) + + return nil +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go new file mode 100644 index 00000000000..8e9c88ed873 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// SecureValueJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type SecureValueJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*SecureValueJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*SecureValueJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &SecureValueJSONCodec{} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go new file mode 100644 index 00000000000..c8691021594 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type SecureValueMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewSecureValueMetadata creates a new SecureValueMetadata object. +func NewSecureValueMetadata() *SecureValueMetadata { + return &SecureValueMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go new file mode 100644 index 00000000000..c1832e9bca7 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type SecureValue struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the SecureValue + Spec SecureValueSpec `json:"spec" yaml:"spec"` + + Status SecureValueStatus `json:"status" yaml:"status"` +} + +func (o *SecureValue) GetSpec() any { + return o.Spec +} + +func (o *SecureValue) SetSpec(spec any) error { + cast, ok := spec.(SecureValueSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *SecureValue) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *SecureValue) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *SecureValue) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(SecureValueStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type SecureValueStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *SecureValue) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *SecureValue) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *SecureValue) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *SecureValue) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *SecureValue) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *SecureValue) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *SecureValue) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *SecureValue) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *SecureValue) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *SecureValue) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *SecureValue) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *SecureValue) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *SecureValue) DeepCopy() *SecureValue { + cpy := &SecureValue{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *SecureValue) DeepCopyInto(dst *SecureValue) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &SecureValue{} + +// +k8s:openapi-gen=true +type SecureValueList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []SecureValue `json:"items" yaml:"items"` +} + +func (o *SecureValueList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *SecureValueList) Copy() resource.ListObject { + cpy := &SecureValueList{ + TypeMeta: o.TypeMeta, + Items: make([]SecureValue, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*SecureValue); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *SecureValueList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *SecureValueList) SetItems(items []resource.Object) { + o.Items = make([]SecureValue, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*SecureValue) + } +} + +func (o *SecureValueList) DeepCopy() *SecureValueList { + cpy := &SecureValueList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *SecureValueList) DeepCopyInto(dst *SecureValueList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &SecureValueList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *SecureValueSpec) DeepCopy() *SecureValueSpec { + cpy := &SecureValueSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *SecureValueSpec) DeepCopyInto(dst *SecureValueSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of SecureValueStatus +func (s *SecureValueStatus) DeepCopy() *SecureValueStatus { + cpy := &SecureValueStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies SecureValueStatus into another SecureValueStatus object +func (s *SecureValueStatus) DeepCopyInto(dst *SecureValueStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go new file mode 100644 index 00000000000..4a978fe34b6 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaSecureValue = resource.NewSimpleSchema("secret.grafana.app", "v1beta1", &SecureValue{}, &SecureValueList{}, resource.WithKind("SecureValue"), + resource.WithPlural("securevalues"), resource.WithScope(resource.NamespacedScope)) + kindSecureValue = resource.Kind{ + Schema: schemaSecureValue, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &SecureValueJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func SecureValueKind() resource.Kind { + return kindSecureValue +} + +// Schema returns a resource.SimpleSchema representation of SecureValue +func SecureValueSchema() *resource.SimpleSchema { + return schemaSecureValue +} + +// Interface compliance checks +var _ resource.Schema = kindSecureValue diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go new file mode 100644 index 00000000000..1b5d0956893 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go @@ -0,0 +1,46 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// ExposedSecureValue contains the raw decrypted secure value. +// +k8s:openapi-gen=true +type SecureValueExposedSecureValue string + +// +k8s:openapi-gen=true +type SecureValueSpec struct { + // Short description that explains the purpose of this SecureValue. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=25 + Description string `json:"description"` + // The raw value is only valid for write. Read/List will always be empty. + // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. + // Minimum and maximum lengths in bytes. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=24576 + // +optional + Value *SecureValueExposedSecureValue `json:"value,omitempty"` + // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. + // This should not contain sensitive information. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=1024 + // +optional + Ref *string `json:"ref,omitempty"` + // Name of the keeper, being the actual storage of the secure value. + // If not specified, the default keeper for the namespace will be used. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + // +optional + Keeper *string `json:"keeper,omitempty"` + // The Decrypters that are allowed to decrypt this secret. + // An empty list means no service can decrypt it. + // +k8s:validation:maxItems=64 + // +k8s:validation:uniqueItems=true + // +listType=atomic + // +optional + Decrypters []string `json:"decrypters,omitempty"` +} + +// NewSecureValueSpec creates a new SecureValueSpec object. +func NewSecureValueSpec() *SecureValueSpec { + return &SecureValueSpec{} +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go new file mode 100644 index 00000000000..eac159c6035 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go @@ -0,0 +1,47 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// +k8s:openapi-gen=true +type SecureValuestatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State SecureValueStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewSecureValuestatusOperatorState creates a new SecureValuestatusOperatorState object. +func NewSecureValuestatusOperatorState() *SecureValuestatusOperatorState { + return &SecureValuestatusOperatorState{} +} + +// +k8s:openapi-gen=true +type SecureValueStatus struct { + Version int64 `json:"version"` + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]SecureValuestatusOperatorState `json:"operatorStates,omitempty"` + // +optional + ExternalID string `json:"externalID"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewSecureValueStatus creates a new SecureValueStatus object. +func NewSecureValueStatus() *SecureValueStatus { + return &SecureValueStatus{} +} + +// +k8s:openapi-gen=true +type SecureValueStatusOperatorStateState string + +const ( + SecureValueStatusOperatorStateStateSuccess SecureValueStatusOperatorStateState = "success" + SecureValueStatusOperatorStateStateInProgress SecureValueStatusOperatorStateState = "in_progress" + SecureValueStatusOperatorStateStateFailed SecureValueStatusOperatorStateState = "failed" +) diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go similarity index 55% rename from pkg/apis/secret/v0alpha1/zz_generated.openapi.go rename to apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go index de1934c36cb..87ba7504932 100644 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi.go +++ b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go @@ -1,11 +1,9 @@ //go:build !ignore_autogenerated // +build !ignore_autogenerated -// SPDX-License-Identifier: AGPL-3.0-only +// Code generated by grafana-app-sdk. DO NOT EDIT. -// Code generated by openapi-gen. DO NOT EDIT. - -package v0alpha1 +package v1beta1 import ( common "k8s.io/kube-openapi/pkg/common" @@ -15,95 +13,106 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSCredentials": schema_pkg_apis_secret_v0alpha1_AWSCredentials(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig": schema_pkg_apis_secret_v0alpha1_AWSKeeperConfig(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureCredentials": schema_pkg_apis_secret_v0alpha1_AzureCredentials(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig": schema_pkg_apis_secret_v0alpha1_AzureKeeperConfig(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue": schema_pkg_apis_secret_v0alpha1_CredentialValue(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPCredentials": schema_pkg_apis_secret_v0alpha1_GCPCredentials(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig": schema_pkg_apis_secret_v0alpha1_GCPKeeperConfig(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpCredentials": schema_pkg_apis_secret_v0alpha1_HashiCorpCredentials(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig": schema_pkg_apis_secret_v0alpha1_HashiCorpKeeperConfig(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Keeper": schema_pkg_apis_secret_v0alpha1_Keeper(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperList": schema_pkg_apis_secret_v0alpha1_KeeperList(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperSpec": schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValue": schema_pkg_apis_secret_v0alpha1_SecureValue(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueList": schema_pkg_apis_secret_v0alpha1_SecureValueList(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueSpec": schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueStatus": schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref), - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SystemKeeperConfig": schema_pkg_apis_secret_v0alpha1_SystemKeeperConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper": schema_pkg_apis_secret_v1beta1_Keeper(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig": schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig": schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue": schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig": schema_pkg_apis_secret_v1beta1_KeeperGCPConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig": schema_pkg_apis_secret_v1beta1_KeeperHashiCorpConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperList": schema_pkg_apis_secret_v1beta1_KeeperList(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec": schema_pkg_apis_secret_v1beta1_KeeperSpec(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus": schema_pkg_apis_secret_v1beta1_KeeperStatus(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState": schema_pkg_apis_secret_v1beta1_KeeperstatusOperatorState(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue": schema_pkg_apis_secret_v1beta1_SecureValue(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueList": schema_pkg_apis_secret_v1beta1_SecureValueList(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec": schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus": schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState": schema_pkg_apis_secret_v1beta1_SecureValuestatusOperatorState(ref), } } -func schema_pkg_apis_secret_v0alpha1_AWSCredentials(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_Keeper(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Credentials of remote keepers.", - Type: []string{"object"}, + Type: []string{"object"}, Properties: map[string]spec.Schema{ - "accessKeyId": { + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Keeper", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "accessKeyID": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), }, }, "secretAccessKey": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), }, }, - "kmsKeyId": { + "kmsKeyID": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", }, }, }, - Required: []string{"accessKeyId", "secretAccessKey"}, + Required: []string{"accessKeyID", "secretAccessKey"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, } } -func schema_pkg_apis_secret_v0alpha1_AWSKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Remote Keepers.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "accessKeyId": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), - }, - }, - "secretAccessKey": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), - }, - }, - "kmsKeyId": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"accessKeyId", "secretAccessKey"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"}, - } -} - -func schema_pkg_apis_secret_v0alpha1_AzureCredentials(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -116,14 +125,14 @@ func schema_pkg_apis_secret_v0alpha1_AzureCredentials(ref common.ReferenceCallba Format: "", }, }, - "tenantId": { + "tenantID": { SchemaProps: spec.SchemaProps{ Default: "", Type: []string{"string"}, Format: "", }, }, - "clientId": { + "clientID": { SchemaProps: spec.SchemaProps{ Default: "", Type: []string{"string"}, @@ -133,70 +142,28 @@ func schema_pkg_apis_secret_v0alpha1_AzureCredentials(ref common.ReferenceCallba "clientSecret": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), }, }, }, - Required: []string{"keyVaultName", "tenantId", "clientId", "clientSecret"}, + Required: []string{"keyVaultName", "tenantID", "clientID", "clientSecret"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, } } -func schema_pkg_apis_secret_v0alpha1_AzureKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "keyVaultName": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "tenantId": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "clientId": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "clientSecret": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), - }, - }, - }, - Required: []string{"keyVaultName", "tenantId", "clientId", "clientSecret"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"}, - } -} - -func schema_pkg_apis_secret_v0alpha1_CredentialValue(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Holds the way credentials are obtained.", - Type: []string{"object"}, Properties: map[string]spec.Schema{ "secureValueName": { SchemaProps: spec.SchemaProps{ Description: "The name of the secure value that holds the actual value.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -204,6 +171,7 @@ func schema_pkg_apis_secret_v0alpha1_CredentialValue(ref common.ReferenceCallbac "valueFromEnv": { SchemaProps: spec.SchemaProps{ Description: "The value is taken from the environment variable.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -211,36 +179,24 @@ func schema_pkg_apis_secret_v0alpha1_CredentialValue(ref common.ReferenceCallbac "valueFromConfig": { SchemaProps: spec.SchemaProps{ Description: "The value is taken from the Grafana config file.", + Default: "", Type: []string{"string"}, Format: "", }, }, }, }, - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-unions": []interface{}{ - map[string]interface{}{ - "fields-to-discriminateBy": map[string]interface{}{ - "secureValueName": "SecureValueName", - "valueFromConfig": "ValueFromConfig", - "valueFromEnv": "ValueFromEnv", - }, - }, - }, - }, - }, }, } } -func schema_pkg_apis_secret_v0alpha1_GCPCredentials(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperGCPConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "projectId": { + "projectID": { SchemaProps: spec.SchemaProps{ Default: "", Type: []string{"string"}, @@ -255,40 +211,13 @@ func schema_pkg_apis_secret_v0alpha1_GCPCredentials(ref common.ReferenceCallback }, }, }, - Required: []string{"projectId", "credentialsFile"}, + Required: []string{"projectID", "credentialsFile"}, }, }, } } -func schema_pkg_apis_secret_v0alpha1_GCPKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "projectId": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "credentialsFile": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"projectId", "credentialsFile"}, - }, - }, - } -} - -func schema_pkg_apis_secret_v0alpha1_HashiCorpCredentials(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperHashiCorpConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -304,7 +233,7 @@ func schema_pkg_apis_secret_v0alpha1_HashiCorpCredentials(ref common.ReferenceCa "token": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), }, }, }, @@ -312,39 +241,11 @@ func schema_pkg_apis_secret_v0alpha1_HashiCorpCredentials(ref common.ReferenceCa }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, } } -func schema_pkg_apis_secret_v0alpha1_HashiCorpKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "address": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "token": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"), - }, - }, - }, - Required: []string{"address", "token"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.CredentialValue"}, - } -} - -func schema_pkg_apis_secret_v0alpha1_Keeper(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -366,84 +267,33 @@ func schema_pkg_apis_secret_v0alpha1_Keeper(ref common.ReferenceCallback) common }, "metadata": { SchemaProps: spec.SchemaProps{ - Description: "Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-patch-merge-key": "name", - "x-kubernetes-patch-strategy": "replace", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "This is the actual keeper schema.", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperSpec"), - }, - }, - }, - Required: []string{"spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_secret_v0alpha1_KeeperList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Description: "Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), }, }, "items": { SchemaProps: spec.SchemaProps{ - Description: "Slice containing all keepers.", - Type: []string{"array"}, + Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Keeper"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper"), }, }, }, }, }, }, - Required: []string{"items"}, + Required: []string{"metadata", "items"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Keeper", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -467,10 +317,10 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co }, SchemaProps: spec.SchemaProps{ Description: "AWS Keeper Configuration.", - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig"), }, }, - "azurekeyvault": { + "azure": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ "x-kubernetes-map-type": "atomic", @@ -478,7 +328,7 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co }, SchemaProps: spec.SchemaProps{ Description: "Azure Keeper Configuration.", - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig"), }, }, "gcp": { @@ -489,10 +339,10 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co }, SchemaProps: spec.SchemaProps{ Description: "GCP Keeper Configuration.", - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig"), }, }, - "hashivault": { + "hashiCorpVault": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ "x-kubernetes-map-type": "atomic", @@ -500,7 +350,7 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co }, SchemaProps: spec.SchemaProps{ Description: "HashiCorp Vault Keeper Configuration.", - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig"), }, }, }, @@ -508,111 +358,202 @@ func schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AWSKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.AzureKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig"}, } } -func schema_pkg_apis_secret_v0alpha1_SecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "kind": { + "operatorStates": { SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Description: "Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "This is the actual secure value schema.", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueSpec"), - }, - }, - "status": { - SchemaProps: spec.SchemaProps{ - Description: "Read-only observed status of the `SecureValue`. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueStatus"), - }, - }, - }, - Required: []string{"spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueSpec", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValueStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_secret_v0alpha1_SecureValueList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Description: "Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Description: "Slice containing all secure values. This will NOT output decrypted values.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValue"), + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", }, }, }, }, }, }, - Required: []string{"items"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.SecureValue", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState"}, } } -func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_KeeperstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the SecureValue", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValueList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -623,7 +564,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac Description: "Short description that explains the purpose of this SecureValue.", Default: "", MinLength: ptr.To[int64](1), - MaxLength: ptr.To[int64](253), + MaxLength: ptr.To[int64](25), Type: []string{"string"}, Format: "", }, @@ -684,47 +625,111 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac } } -func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "phase": { + "version": { SchemaProps: spec.SchemaProps{ - Description: "High-level summary of where the `SecureValue` is in its lifecycle. One of: `Pending`, `Succeeded` or `Failed`.\n\nPossible enum values:\n - `\"Failed\"` If the creation of the secret fails, it will move to the `Failed` phase. Check the additional `status` fields for more information on what caused the failure. This state is unrecoverable.\n - `\"Pending\"` When the `SecureValue` is created, it will start in `Pending` phase to create the underlying secret asynchronously.\n - `\"Succeeded\"` If the creation of the secret is successful, it will move to the `Succeeded` phase.", - Default: "", - Type: []string{"string"}, - Format: "", - Enum: []interface{}{"Failed", "Pending", "Succeeded"}, + Default: 0, + Type: []string{"integer"}, + Format: "int64", }, }, - "message": { + "operatorStates": { SchemaProps: spec.SchemaProps{ - Description: "A human readable message indicating details about why the `SecureValue` is in this phase. Only applicable if the `phase=Failed`.", - Type: []string{"string"}, - Format: "", + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState"), + }, + }, + }, }, }, - "externalId": { + "externalID": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, }, }, }, - Required: []string{"phase"}, + Required: []string{"version"}, }, }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState"}, } } -func schema_pkg_apis_secret_v0alpha1_SystemKeeperConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_secret_v1beta1_SecureValuestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "System Keeper.", - Type: []string{"object"}, + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, }, }, } diff --git a/apps/secret/pkg/apis/secret_manifest.go b/apps/secret/pkg/apis/secret_manifest.go new file mode 100644 index 00000000000..12138bbf57b --- /dev/null +++ b/apps/secret/pkg/apis/secret_manifest.go @@ -0,0 +1,65 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "fmt" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + + v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" +) + +var () + +var appManifestData = app.ManifestData{ + AppName: "secret", + Group: "secret.grafana.app", + Kinds: []app.ManifestKind{ + { + Kind: "SecureValue", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v1beta1", + }, + }, + }, + + { + Kind: "Keeper", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v1beta1", + }, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("secret") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "SecureValue/v1beta1": v1beta1.SecureValueKind(), + "Keeper/v1beta1": v1beta1.KeeperKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} diff --git a/conf/defaults.ini b/conf/defaults.ini index bc43af809ac..ad0c3276a4a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1121,7 +1121,7 @@ content_types = text/html # Use space to separate multiple modes, e.g. "console file" mode = console file -# Either "debug", "info", "warn", "error", "critical", default is "info" +# Either "debug", "info", "warn", "error". Default is "info" level = info # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug @@ -2018,8 +2018,7 @@ provider = static [feature_toggles.openfeature.context] # This is EXPERIMENTAL. Please, do not use this section -# instance = "grafana" -# version = 11.0.0 +# foo = bar [date_formats] # For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/ @@ -2172,12 +2171,12 @@ alert_rules_state = "paused" ###################################### Secrets Manager ###################################### [secrets_manager] -# Used for signing +# Current key provider used for envelope encryption +encryption_provider = secret_key.v1 + +[secrets_manager.encryption.secret_key.v1] +# Used to encrypt data keys secret_key = SW2YcwTIb9zpOOhoPsMm -# Current key provider used for envelope encryption, default to static value specified by secret_key -encryption_provider = secretKey.v1 -# List of configured key providers, space separated (Enterprise only): e.g., awskms.v1 azurekv.v1 -available_encryption_providers = ################################## Frontend development configuration ################################### # Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code diff --git a/conf/sample.ini b/conf/sample.ini index 42642479375..238162d2590 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1105,7 +1105,7 @@ # Use space to separate multiple modes, e.g. "console file" ;mode = console file -# Either "debug", "info", "warn", "error", "critical", default is "info" +# Either "debug", "info", "warn", "error". Default is "info" ;level = info # optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug diff --git a/contribute/ISSUE_TRIAGE.md b/contribute/ISSUE_TRIAGE.md index ed9d6c9418e..b856798fb5c 100644 --- a/contribute/ISSUE_TRIAGE.md +++ b/contribute/ISSUE_TRIAGE.md @@ -2,61 +2,43 @@ The main goal of issue triage is to categorize all incoming Grafana issues and make sure each issue has all the essential information needed for anyone to understand and be able to start working on it. -> **Note:** This information is for Grafana project Maintainers, Owners, and Admins. If you are a Contributor, then you won't be able to perform most of the tasks in this topic. +> [!NOTE] +> This information is for Grafana project Maintainers, Owners, and Admins. If you are a Contributor, then you won't be able to perform most of the tasks in this topic. The core maintainers of the Grafana project are responsible for categorizing all incoming issues and delegating any critical or important issue to other maintainers. Currently, one maintainer each week is responsible. Besides that part, triage provides an important way to contribute to an open source project. ## Simplified flowchart diagram of the issue triage process - - -``` - +-------------------+ - | | - | new issue/ | + - | more info added | - | | - +---------|---------+ - | - +-------------|-----------+ - | | - NO +------- all info needed to ------+ YES - | | categorize the issue? | | - | | | | - | +-------------------------+ | - +------|-----------+ +------------|----------+ - | | | | - | label: | | label: type/* | - | needs more info | | label: area/* | - | | | label: datasource/* | - +------------------+ | | | - +------------|----------+ - | - +--------|-------+ - | | - NO +---------- is duplicate? --------------------+ YES - | | | | - | +----------------+ | - | +----------------|---------------+ - +------|-----+ | | - | | | add comment: | - NO +------------ can repro? ------------+ YES | | - | | | | | /duplicate of # | - | +------------+ | | | - | | +--------------------------------+ -+-------------|-------------+ | -| | +--------|---------+ -| label: | | | -| triage/needs-confirmation | NO +-------- needs priority? ------+ YES -| | | | | | -+-------------|-------------+ | +------------------+ | - | | | -+-------------|------+ +-------|--------+ +----------|---------+ -| | | | | | -| Assign to project ------ Done -------------- label: priority/* | -| | | | | | -+--------------------+ +----------------+ +--------------------+ +```mermaid +flowchart TD + A([New issue /
more info added]) + B{All info needed to categorize the issue?} + C1([Label: needs more info]) + C2([Label: type/#42;
Label: area/#42;
Label: datasource/#42;]) + D{Is duplicate?} + E1([Add comment:
/duplicate of #<issue number>]) + E2{Can repro?} + F1([Label: triage/needs-confirmation]) + F2{Needs priority?} + G1([Assign to project]) + G2([Label: priority/*]) + H([Done]) + %% Flow + A --> B + B -- No --> C1 + B -- Yes --> C2 + C2 --> D + D -- Yes --> E1 + D -- No --> E2 + E2 -- No --> F1 + E2 -- Yes --> F2 + F1 --> G1 + F2 -- No --> H + F2 -- Yes --> G2 + G2 --> H + E1 --> H + G1 --> H ``` ## Key functions of issue triage @@ -107,7 +89,7 @@ Bug reports should explain what happened, what was expected, and how to reproduc ##### Enhancement requests -> **Note:** Prior to August, 2023, community-submitted feature requests were submitted as [Github discussions](https://github.com/grafana/grafana/discussions). These are now submitted using the [feature request issue template](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request&projects=&template=1-feature_requests.md). +> **Note:** Prior to August, 2023, community-submitted feature requests were submitted as [GitHub discussions](https://github.com/grafana/grafana/discussions). These are now submitted using the [feature request issue template](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request&projects=&template=1-feature_requests.md). When submitting an enhancement request we ask that users focus on the problem they'd like to solve and why it’s a problem rather than focusing on the solution itself. To facilitate these objectives, the feature requests template includes the following: diff --git a/contribute/style-guides/e2e-plugins.md b/contribute/style-guides/e2e-plugins.md index e708281d675..8d6ffaca61a 100644 --- a/contribute/style-guides/e2e-plugins.md +++ b/contribute/style-guides/e2e-plugins.md @@ -6,9 +6,9 @@ When end-to-end testing Grafana plugins, a best practice is to use the [`@grafan ## Add end-to-end tests for a core plugin -You can add Playwright end-to-end tests for plugins to the [`e2e/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e) directory. +You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory. -1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e). This is the directory where your plugin tests will be kept. +1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept. 1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration. In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests. diff --git a/devenv/dev-dashboards/panel-table/table_pagination.json b/devenv/dev-dashboards/panel-table/table_pagination.json index 0bb4758490b..48f5df03f1f 100644 --- a/devenv/dev-dashboards/panel-table/table_pagination.json +++ b/devenv/dev-dashboards/panel-table/table_pagination.json @@ -2662,9 +2662,10 @@ } ], "schemaVersion": 32, - "tags": [ - "gdev", - "panel-tests" + "tags": [ + "gdev", + "panel-tests", + "table" ], "templating": { "list": [] @@ -2675,8 +2676,8 @@ }, "timepicker": {}, "timezone": "", - "title": "Table panel - Pagination", + "title": "Panel Tests - Table - Pagination", "uid": "1YGJxPUnz", - "version": 2, + "version": 3, "weekStart": "" -} \ No newline at end of file +} diff --git a/devenv/dev-dashboards/panel-table/table_sparkline_cell.json b/devenv/dev-dashboards/panel-table/table_sparkline_cell.json index d18488ca89d..24ec5a57326 100644 --- a/devenv/dev-dashboards/panel-table/table_sparkline_cell.json +++ b/devenv/dev-dashboards/panel-table/table_sparkline_cell.json @@ -18,22 +18,27 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, + "id": 1734, "links": [], - "liveNow": false, "panels": [ { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, "fieldConfig": { "defaults": { "color": { - "mode": "thresholds" + "fixedColor": "blue", + "mode": "shades" }, "custom": { "align": "auto", "cellOptions": { - "type": "auto" + "type": "sparkline" }, - "inspect": false + "inspect": false, + "wrapHeaderText": false }, "mappings": [], "thresholds": { @@ -41,7 +46,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -53,8 +58,8 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 12, + "h": 16, + "w": 8, "x": 0, "y": 0 }, @@ -71,14 +76,507 @@ }, "showHeader": true }, - "pluginVersion": "10.1.0-pre", + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "ca3da78e-6bcc-4a2a-a080-d338a14528ca" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 31 + } + ], + "title": "Sparkline (lines)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "timeField": "time" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "shades" + }, + "custom": { + "align": "auto", + "cellOptions": { + "drawStyle": "bars", + "type": "sparkline" + }, + "inspect": false, + "wrapHeaderText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 2, + "maxDataPoints": 60, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "ca3da78e-6bcc-4a2a-a080-d338a14528ca" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 31 + } + ], + "title": "Sparklines (bars)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "timeField": "time" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "shades" + }, + "custom": { + "align": "auto", + "cellOptions": { + "drawStyle": "points", + "type": "sparkline" + }, + "inspect": false, + "wrapHeaderText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 3, + "maxDataPoints": 100, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "ca3da78e-6bcc-4a2a-a080-d338a14528ca" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 31 + } + ], + "title": "Sparklines (points)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "timeField": "time" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "shades" + }, + "custom": { + "align": "auto", + "cellOptions": { + "drawStyle": "line", + "type": "sparkline" + }, + "inspect": false, + "wrapHeaderText": false + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "areaF2" + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 4, + "maxDataPoints": 100, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "ca3da78e-6bcc-4a2a-a080-d338a14528ca" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 31 + } + ], + "title": "Sparklines (min=0)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "timeField": "time" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "yellow", + "mode": "shades" + }, + "custom": { + "align": "auto", + "cellOptions": { + "drawStyle": "line", + "type": "sparkline" + }, + "inspect": false, + "wrapHeaderText": false + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "areaF2" + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 5, + "maxDataPoints": 100, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "ca3da78e-6bcc-4a2a-a080-d338a14528ca" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 31 + } + ], + "title": "Sparklines (max=100)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "timeField": "time" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "purple", + "mode": "shades" + }, + "custom": { + "align": "auto", + "cellOptions": { + "drawStyle": "line", + "lineInterpolation": "linear", + "type": "sparkline" + }, + "inspect": false, + "wrapHeaderText": false + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "areaF2" + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 6, + "maxDataPoints": 100, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "ca3da78e-6bcc-4a2a-a080-d338a14528ca" + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 31 + } + ], + "title": "Sparklines (min=0, max=100)", + "transformations": [ + { + "id": "timeSeriesTable", + "options": { + "A": { + "timeField": "time" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "wrapHeaderText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 32 + }, + "id": 7, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.1.0-pre", "targets": [ { "csvContent": "time,x\n2023-06-12T05:49:00Z,10\n2023-06-12T07:01:00Z,20\n2023-06-12T08:13:00Z,NaN\n2023-06-12T09:25:00Z,NaN\n2023-06-12T10:37:00Z,NaN\n2023-06-12T11:49:00Z,30\n2023-06-12T13:01:00Z,20\n2023-06-12T14:13:00Z,10\n2023-06-12T15:25:00Z,Inf\n2023-06-12T16:37:00Z,null\n2023-06-12T17:49:00Z,", - "datasource": { - "type": "testdata", - "uid": "PD8C576611E62080A" - }, "refId": "A", "scenarioId": "csv_content" } @@ -87,26 +585,31 @@ "transformations": [ { "id": "timeSeriesTable", - "options": {} + "options": { + "A": { + "stat": "last", + "timeField": "" + } + } } ], "type": "table" } ], + "preload": false, "refresh": "", - "schemaVersion": 38, + "schemaVersion": 41, "tags": [], "templating": { "list": [] }, "time": { - "from": "2023-06-12T00:00:00.000Z", - "to": "2023-06-12T18:00:00.000Z" + "from": "now-6h", + "to": "now" }, "timepicker": {}, - "timezone": "utc", - "title": "Table with SparklineCell", + "timezone": "", + "title": "Panel Tests - Table - Sparklines", "uid": "d6373b49-1957-4f00-9218-ee2120d3ecd9", - "version": 4, - "weekStart": "" -} \ No newline at end of file + "version": 5 +} diff --git a/devenv/dev-dashboards/panel-table/table_tests.json b/devenv/dev-dashboards/panel-table/table_tests.json index b8ca436e110..a8d0a0d5253 100644 --- a/devenv/dev-dashboards/panel-table/table_tests.json +++ b/devenv/dev-dashboards/panel-table/table_tests.json @@ -431,7 +431,11 @@ ], "refresh": false, "schemaVersion": 16, - "tags": ["gdev", "panel-tests"], + "tags": [ + "gdev", + "panel-tests", + "table" + ], "templating": { "list": [] }, @@ -445,5 +449,5 @@ "timezone": "browser", "title": "Panel Tests - Table", "uid": "pttable", - "version": 2 + "version": 3 } diff --git a/devenv/dev-dashboards/panel-table/table_tests_new.json b/devenv/dev-dashboards/panel-table/table_tests_new.json index 69b50b9b23e..e928e9fc578 100644 --- a/devenv/dev-dashboards/panel-table/table_tests_new.json +++ b/devenv/dev-dashboards/panel-table/table_tests_new.json @@ -995,7 +995,8 @@ "schemaVersion": 41, "tags": [ "gdev", - "panel-tests" + "panel-tests", + "table" ], "templating": { "list": [] diff --git a/docs/sources/administration/grafana-advisor/_index.md b/docs/sources/administration/grafana-advisor/_index.md index cd9b812d59e..28caf9b1953 100644 --- a/docs/sources/administration/grafana-advisor/_index.md +++ b/docs/sources/administration/grafana-advisor/_index.md @@ -90,6 +90,12 @@ This collapsible section provides more details about which checks have been perf Click the cogwheel in this section to access Grafana Advisor settings, where you can enable or disable checks according to your preferences. {{< /admonition >}} +### Enable LLM suggestions + +If the [Grafana LLM app](https://grafana.com/grafana/plugins/grafana-llm-app/) is installed, the Advisor can use it to generate suggestions for issues. Enable the LLM app and click the magic (✨) button to generate a suggestion for an issue. + +![](/media/docs/grafana-advisor/llm-suggestions.png) + ## Address issues To resolve issues flagged by Grafana Advisor and maintain system reliability, follow the best practices below. Regularly check the Advisor to keep your Grafana instance secure and up to date. diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md index 484210ab244..1c5a5fa7272 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-for-app-plugins/index.md @@ -72,6 +72,7 @@ The following list contains app plugins that have fine-grained RBAC support. | [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) | | [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a | | [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) | +| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) | ### Revoke fine-grained access from app plugins diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md index 4d4ec4b3200..a01783fb921 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-jira.md @@ -62,13 +62,13 @@ For more details on contact points, including how to test them and enable notifi | Key | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Title | The issue title. Supports templating. Max length is 255 characters. | +| Summary | The issue title. Supports templating. Max length is 255 characters. | | Description | The description of the issue. Depending on version of the API, it can be a text, Markdown, or JSON (v3 API only). Maximum size of the field is 32kb.
**Note:** JSON is not limited by the client, and if it exceeds the size, the API is likely to reject the request. | | Labels | Custom labels can be added to organize and filter issues created in Jira. Supports templating, allowing dynamic label generation based on alert data. | | Priority | The priority level of the issue (e.g., `Low`, `Medium`, `High`, `Critical`).
Ensure that the priority value matches the available options for your Jira instance. You can customize priority levels in Jira [here](https://support.atlassian.com/jira-cloud-administration/docs/configure-priorities/). | | Resolve Transition | The transition name to move the issue to a resolved state when an alert is resolved. Ensure that the value matches a valid transition available in your Jira workflow for the specified issue type. If this field is empty, the issue will not be transitioned to Done. | | Reopen Transition | The transition name to move the issue back to an open state when an alert reoccurs. Ensure that the value matches a valid transition available in your Jira workflow for the specified issue type. If this field is empty, the issue will not be reopened. | | Reopen Duration | The time duration (in minutes) to control whether to reopen an issue that was closed within this duration or create a new one. If not specified, the most recent issue that matches the deduplication key will be updated and reopened (if reopen transition is specified). | -| Ignored Resolution | Specify a resolution status that should be ignored when searching for existing issues. For example, issues with this resolution will not be reopened or updated by subsequent alerts. | +| "Won't fix" Transition | Specify a resolution status that should be ignored when searching for existing issues. For example, issues with this resolution will not be reopened or updated by subsequent alerts. | | Deduplication Key Field | Custom field to store the deduplication key. Must be a text field.
If not specified, the deduplication key is added to labels in the format of `ALERT(hash sum)`. See [Jira documentation](https://support.atlassian.com/jira-cloud-administration/docs/create-a-custom-field/) for how to create custom fields. | | Fields | Allows to configure custom fields of Jira issue. The field name should be of the format like `customfield_10001`. | diff --git a/docs/sources/dashboards/build-dashboards/_index.md b/docs/sources/dashboards/build-dashboards/_index.md index 4c228c3458c..4978be9dd35 100644 --- a/docs/sources/dashboards/build-dashboards/_index.md +++ b/docs/sources/dashboards/build-dashboards/_index.md @@ -28,7 +28,3 @@ refs: This section includes the following topics: {{< section >}} - -## Dynamic dashboards - -You can create more interactive and dynamic dashboards by adding and using [variables](ref:variables). Instead of hard-coding things like server, application, and sensor names in your metric queries, you can use variables in their place. Read more about variables [here](ref:variables). diff --git a/docs/sources/dashboards/variables/variable-syntax/index.md b/docs/sources/dashboards/variables/variable-syntax/index.md index fd876b86847..7d0fbc81993 100644 --- a/docs/sources/dashboards/variables/variable-syntax/index.md +++ b/docs/sources/dashboards/variables/variable-syntax/index.md @@ -94,6 +94,16 @@ String to interpolate: '${servers:glob}' Interpolation result: '{test1,test2}' ``` +### Join + +Formats multi-valued variables with a custom delimiter. If no delimiter argument is supplied, they will be combined with `,`. + +```bash +servers = ["test1", "test2"] +String to interpolate: '${servers:join:&}' +Interpolation result: "test1&test2" +``` + ### JSON Formats variables with multiple values as a comma-separated string. @@ -134,6 +144,24 @@ String to interpolate: '${servers:pipe}' Interpolation result: 'test1.|test2' ``` +### Query parameters + +Formats single- and multi-valued variables into their query parameter representation. Example: `var-foo=value1&var-foo=value2` + +```bash +servers = ["test1", "test2"] +String to interpolate: '${servers:queryparam}' +Interpolation result: "var-servers=test1&var-servers=test2" +``` + +Use the `customqueryparam` formatter to customize how the query parameters are formatted. It accepts two optional arguments to specify the parameter name, and a value prefix. + +```bash +servers = ["test1", "test2"] +String to interpolate: '${servers:customqueryparam:v-servers:x-}' +Interpolation result: "v-servers=x-test1&v-servers=x-test2" +``` + ### Raw Doesn't apply any data source-specific formatting to the variable. @@ -194,13 +222,3 @@ servers = ["test1", "test2"] String to interpolate: '${servers:text}' Interpolation result: "test1 + test2" ``` - -### Query parameters - -Formats single- and multi-valued variables into their query parameter representation. Example: `var-foo=value1&var-foo=value2` - -```bash -servers = ["test1", "test2"] -String to interpolate: '${servers:queryparam}' -Interpolation result: "var-servers=test1&var-servers=test2" -``` diff --git a/docs/sources/datasources/aws-cloudwatch/aws-authentication/index.md b/docs/sources/datasources/aws-cloudwatch/aws-authentication/index.md index 5df10ff1eae..0d289af1380 100644 --- a/docs/sources/datasources/aws-cloudwatch/aws-authentication/index.md +++ b/docs/sources/datasources/aws-cloudwatch/aws-authentication/index.md @@ -160,26 +160,22 @@ securityContext: ## Use Grafana Assume Role {{< admonition type="note" >}} -Grafana Assume Role is currently in [private preview](https://grafana.com/docs/release-life-cycle/) for Grafana Cloud. +Grafana Assume Role is only available in Grafana Cloud. -It's currently only available for Amazon CloudWatch. - -To gain early access to this feature, contact Customer Support and ask for the `awsDatasourcesTempCredentials` feature toggle to be enabled on your account. +It's currently only available for Amazon CloudWatch and Athena. {{< /admonition >}} The Grafana Assume Role authentication provider lets you authenticate with AWS without having to create and maintain long term AWS users or rotate their access and secret keys. Instead, you can create an IAM role that has permissions to access CloudWatch and a trust relationship with Grafana's AWS account. Grafana's AWS account then makes an STS request to AWS to create temporary credentials to access your AWS data. It makes this STS request by passing along an `externalID` that's unique per Cloud account, to ensure that Grafana Cloud users can only access their own AWS data. For more information, refer to the [AWS documentation on external ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). To use the Grafana Assume Role: -1. Grafana Cloud customers need to open a support ticket to enable the feature `awsDatasourcesTempCredentials`. - This feature is enabled by default in open source Grafana and Grafana Enterprise. -2. Once the feature is enabled, create a new CloudWatch data source (or update an existing one) and select **Grafana Assume Role** as an authentication provider. -3. In the AWS Console, create a new IAM role, and under **Trusted entity type**, select **Another AWS account** as the trusted Entity. -4. Enter Grafana's account id (displayed in the instructions box on the **Settings** tab of the CloudWatch data source configuration) and check the **Require external ID** box. -5. Enter the external ID specified in the instructions box on the **Settings** tab of the CloudWatch data source configuration in Grafana. This external ID will be unique to your Grafana instance. -6. Attach any required permissions you would like Grafana to be able to access on your behalf (for example, CloudWatch Logs and CloudWatch Metrics policies). -7. Give the role a name and description, and click **Create role**. -8. Copy the ARN of the role you just created and paste it into the **Assume Role ARN** field on the **Settings** tab of CloudWatch data source configuration in Grafana. +1. Create a new CloudWatch data source (or update an existing one) and select **Grafana Assume Role** as an authentication provider. +2. In the AWS Console, create a new IAM role, and under **Trusted entity type**, select **Another AWS account** as the trusted Entity. +3. Enter the Grafana account id (displayed in the instructions box on the **Settings** tab of the CloudWatch data source configuration) and check the **Require external ID** box. +4. Enter the external ID specified in the instructions box on the **Settings** tab of the CloudWatch data source configuration in Grafana. This external ID will be unique to your Grafana instance. +5. Attach any required permissions you would like Grafana to be able to access on your behalf (for example, CloudWatch Logs and CloudWatch Metrics policies). +6. Give the role a name and description, and click **Create role**. +7. Copy the ARN of the role you just created and paste it into the **Assume Role ARN** field on the **Settings** tab of CloudWatch data source configuration in Grafana. Sample Trust Relationship for an IAM role: diff --git a/docs/sources/datasources/loki/_index.md b/docs/sources/datasources/loki/_index.md index 523be2c9fbc..4176a2771e7 100644 --- a/docs/sources/datasources/loki/_index.md +++ b/docs/sources/datasources/loki/_index.md @@ -62,7 +62,7 @@ The following guides will help you get started with Loki: This data source supports these versions of Loki: -- v2.8+ +- v2.9+ ## Adding a data source diff --git a/docs/sources/observability-as-code/schema-v2/_index.md b/docs/sources/observability-as-code/schema-v2/_index.md index 61de6337b15..9dafaf936bf 100644 --- a/docs/sources/observability-as-code/schema-v2/_index.md +++ b/docs/sources/observability-as-code/schema-v2/_index.md @@ -20,7 +20,7 @@ weight: 200 {{< admonition type="caution" >}} -Dashboard JSON schema v2 is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `dashboardNewLayouts` feature toggle in Grafana to use this feature. +Dashboard JSON schema v2 is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog). **Do not enable this feature in production environments as it may result in the irreversible loss of data.** @@ -32,7 +32,8 @@ Observability as Code works with all versions of the JSON model, and it's fully ## Before you begin -Schema v2 is automatically enabled with the Dynamic Dashboards feature toggle, `dashboardNewLayouts`. +Schema v2 is automatically enabled with the Dynamic Dashboards feature toggle. +To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog). It also requires the new dashboards API feature toggle, `kubernetesDashboards`, to be enabled as well. For more information on how dashboards behave depending on your feature flag configuration, refer to [Notes and limitations](#notes-and-limitations). @@ -217,18 +218,18 @@ Upon saving, they’ll be updated to the new schema where you can take advantage ### Dashboard behavior with disabled feature flags -If you disable the `dashboardNewLayouts` or `kubernetesDashboards` feature flags, you should be aware of how dashboards will behave. +If you disable the Dynamic dashboards or `kubernetesDashboards` feature flags, you should be aware of how dashboards will behave. -#### Disable `dashboardNewLayouts` +#### Disable Dynamic dashboards -If `dashboardNewLayouts` feature toggle is disabled, depending on how the dashboard was built, it will behave differently: +If the Dynamic dashboards feature toggle is disabled, depending on how the dashboard was built, it will behave differently: - Dashboards built on the new schema through the UI - View only - Dashboards built on Schema v1 - View and edit - Dashboards built on the new schema by way of Terraform or the CLI - View and edit - Provisioned dashboards built on the new schema - View and edit, but the edit experience will be the old experience -#### Disable `dashboardNewLayouts` and `kubernetesDashboards` +#### Disable Dynamic dashboards and `kubernetesDashboards` You’ll be unable to view or edit dashboards created or updated in the new schema. diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 529c517f2f9..7782acad459 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -635,7 +635,7 @@ This transformation goes in two steps. First you specify one or multiple fields | 2020-07-07 10:31:22 | **_server 3_** | 55 | OK | | 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting | -All rows with the same value of Server ID are grouped together. +All rows with the same value of Server ID are grouped together. Optionally, you can add a count of how may values fall in the selected group. After choosing which field you want to group your data by, you can add various calculations on the other fields, and apply the calculation to each group of rows. For instance, we could want to calculate the average CPU temperature for each of those servers. So we can add the _mean_ calculation applied on the CPU Temperature field to get the following: @@ -645,6 +645,14 @@ After choosing which field you want to group your data by, you can add various c | server 2 | 88.6 | | server 3 | 59.6 | +If you had added the count stat to the group by, there would be an extra column showing that the count of each server from above was 3. + +| Server ID | CPU Temperature (mean) | Server ID (count) | +| --------- | ---------------------- | ----------------- | +| server 1 | 82 | 3 | +| server 2 | 88.6 | 3 | +| server 3 | 59.6 | 3 | + And we can add more than one calculation. For instance: - For field Time, we can calculate the _Last_ value, to know when the last data point was received for each server diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 50b0e485bdd..1ce20139b48 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1547,7 +1547,7 @@ Use spaces to separate multiple modes, for example, `console file`. #### `level` -Options are `debug`, `info`, `warn`, `error`, and `critical`. Default is `info`. +Options are `debug`, `info`, `warn`, `error`. `critical` is an alias for `error`. Default is `info`. #### `filters` @@ -1576,7 +1576,7 @@ Only applicable when `console` is used in `[log]` mode. #### `level` -Options are `debug`, `info`, `warn`, `error`, and `critical`. Default is inherited from `[log]` level. +See [`[log] level`](#level) for values. Default is inherited from `[log]` level. #### `format` @@ -1590,7 +1590,7 @@ Only applicable when `file` used in `[log]` mode. #### `level` -Options are `debug`, `info`, `warn`, `error`, and `critical`. Default is inherited from `[log]` level. +See [`[log] level`](#level) for values. Default is inherited from `[log]` level. #### `format` @@ -1625,7 +1625,7 @@ Only applicable when `syslog` used in `[log]` mode. #### `level` -Options are `debug`, `info`, `warn`, `error`, and `critical`. Default is inherited from `[log]` level. +See [`[log] level`](#level) for values. Default is inherited from `[log]` level. #### `format` @@ -2838,22 +2838,14 @@ For example: ```ini [time_picker] -quick_ranges = [ - { - "display": "Last 5 minutes", - "from": "now-5m", - "to": "now", - }, - { - "display": "Yesterday", - "from": "now-1d/d", - }, - { - "display": "Today so far", - "from": "now/d", - "to": "now", - } -] +quick_ranges = """[ +{"from":"now-6s","to":"now","display":"Last 6 seconds"}, +{"from":"now-10m","to":"now","display":"Last 10 minutes"}, +{"from":"now-25h","to":"now","display":"Last 24 hours"}, +{"from":"now/w","to":"now/w","display":"This week"}, +{"from":"now-1w/w","to":"now-1w/w","display":"Last week"}, +{"from":"now-10d","to":"now","display":"Last 10 days"} +]""" ``` ### `[expressions]` @@ -2866,7 +2858,7 @@ Set this to `false` to disable expressions and hide them in the Grafana UI. Defa Set the maximum number of cells that can be passed to a SQL expression. Default is `100000`. -#### `sql_expression_cell_output_limit` +#### `sql_expression_output_cell_limit` Set the maximum number of cells that can be returned from a SQL expression. Default is `100000`. 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 c20e71dab3d..303cd8e4839 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -38,6 +38,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | Yes | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | +| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes | | `transformationsRedesign` | Enables the transformations redesign | Yes | | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | | `dashgpt` | Enable AI powered features in dashboards | Yes | @@ -72,7 +73,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `pluginsSriChecks` | Enables SRI checks for plugin assets | | | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | | `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | -| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | Yes | | `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes | | `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | | `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes | @@ -82,7 +82,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `alertingMigrationUI` | Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules | Yes | | `alertingImportYAMLUI` | Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules | Yes | | `unifiedNavbars` | Enables unified navbars | | -| `tabularNumbers` | Use fixed-width numbers globally in the UI | Yes | +| `tabularNumbers` | Use fixed-width numbers globally in the UI | | ## Public preview feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md index d2d8180482f..f381c212abb 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md @@ -236,9 +236,11 @@ With our Grafana and Apache containers running, you can now connect to http://lo If the user is deleted from Grafana, the user will be not be able to login and resync until after the `sync_ttl` has expired. {{< /admonition >}} -### Team Sync (Enterprise only) +### Team Sync -> Only available in Grafana Enterprise v6.3+ +{{< admonition type="note" >}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +{{< /admonition >}} With Team Sync, it's possible to set up synchronization between teams in your authentication provider and Grafana. You can send Grafana values as part of an HTTP header and have Grafana map them to your team structure. This allows you to put users into specific teams automatically. 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 0b6cbec6a95..5d5b5489061 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 @@ -239,10 +239,6 @@ Ensure that you have followed the steps in [Create the Microsoft Entra ID applic ## Configure Azure AD authentication client using the Grafana UI -{{< admonition type="note" >}} -Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle. -{{< /admonition >}} - As a Grafana Admin, you can configure your Azure AD/Entra ID OAuth client from within Grafana using the Grafana UI. To do this, navigate to the **Administration > Authentication > Azure AD** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations. @@ -255,10 +251,6 @@ If you run Grafana in high availability mode, configuration changes may not get ## Configure Azure AD authentication client using the Terraform provider -{{< admonition type="note" >}} -Available in Public Preview in Grafana 10.4 behind the `ssoSettingsApi` feature toggle. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "azuread_sso_settings" { provider_name = "azuread" @@ -426,7 +418,11 @@ This setting is ignored if multiple auth providers are configured to use auto lo auto_login = true ``` -### Team Sync (Enterprise only) +### Team Sync + +{{< admonition type="note" >}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +{{< /admonition >}} With Team Sync you can map your Entra ID groups to teams in Grafana so that your users will automatically be added to the correct teams. @@ -556,11 +552,13 @@ The following table outlines the various Azure AD/Entra ID configuration options | `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure Azure AD/Entra ID to include the `groups` claim following [Configure group membership claims on the Azure Portal](#configure-group-membership-claims-on-the-azure-portal). | | | `allowed_organizations` | No | Yes | List of comma- or space-separated Azure tenant identifiers. The user should be a member of at least one tenant to log in. | | | `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | -| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | -| `tls_client_cert` | No | No | The path to the certificate. | | -| `tls_client_key` | No | No | The path to the key. | | -| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | -| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | -| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` | -| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` | -| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | +| `domain_hint` | No | Yes | The realm of the user in a federated directory. This skips the email-based discovery process that the user goes through on the Azure AD/Entra ID sign-in page, for a slightly more streamlined user experience. More info [here](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#send-the-sign-in-request). | | + +| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | No | The path to the certificate. | | +| `tls_client_key` | No | No | The path to the key. | | +| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | +| `use_refresh_token` | No | Yes | Enables the use of refresh tokens and checks for access token expiration. When enabled, Grafana automatically adds the `offline_access` scope to the list of scopes. | `true` | +| `force_use_graph_api` | No | Yes | Set to `true` to always fetch groups from the Microsoft Graph API instead of the `id_token`. If a user belongs to more than 200 groups, the Microsoft Graph API will be used to retrieve the groups regardless of this setting. | `false` | +| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 17305e59a3f..7b8d312b4af 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -49,10 +49,6 @@ If Users use the same email address in Azure AD that they use with other authent ## Configure generic OAuth authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure Generic OAuth client from within Grafana using the Generic OAuth UI. To do this, navigate to **Administration > Authentication > Generic OAuth** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save** to save the configuration. If the save was successful, Grafana will apply the new configurations. @@ -67,10 +63,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure generic OAuth authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "generic_sso_settings" { provider_name = "generic_oauth" @@ -134,7 +126,7 @@ To integrate your OAuth2 provider with Grafana using our Generic OAuth authentic ### Configure login -Grafana can resolve a user's login from the OAuth2 ID token or user information retrieved from the OAuth2 UserInfo endpoint. +Grafana can resolve a user's login from the OAuth2 ID token, user information retrieved from the OAuth2 UserInfo endpoint, or the OAuth2 access token. Grafana looks at these sources in the order listed until it finds a login. If no login is found, then the user's login is set to user's email address. @@ -146,10 +138,12 @@ Refer to the following table for information on what to configure based on how y | Another field of the OAuth2 ID token. | Set `login_attribute_path` configuration option. | | `login` or `username` field of the user information from the UserInfo endpoint. | N/A | | Another field of the user information from the UserInfo endpoint. | Set `login_attribute_path` configuration option. | +| `login` or `username` field of the OAuth2 access token. | N/A | +| Another field of the OAuth2 access token. | Set `login_attribute_path` configuration option. | ### Configure display name -Grafana can resolve a user's display name from the OAuth2 ID token or user information retrieved from the OAuth2 UserInfo endpoint. +Grafana can resolve a user's display name from the OAuth2 ID token, user information retrieved from the OAuth2 UserInfo endpoint, or the OAuth2 access token. Grafana looks at these sources in the order listed until it finds a display name. If no display name is found, then user's login is displayed instead. @@ -161,10 +155,12 @@ Refer to the following table for information on what you need to configure depen | Another field of the OAuth2 ID token. | Set `name_attribute_path` configuration option. | | `name` or `display_name` field of the user information from the UserInfo endpoint. | N/A | | Another field of the user information from the UserInfo endpoint. | Set `name_attribute_path` configuration option. | +| `name` or `display_name` field of the OAuth2 access token. | N/A | +| Another field of the OAuth2 access token. | Set `name_attribute_path` configuration option. | ### Configure email address -Grafana can resolve the user's email address from the OAuth2 ID token, the user information retrieved from the OAuth2 UserInfo endpoint, or the OAuth2 `/emails` endpoint. +Grafana can resolve the user's email address from the OAuth2 ID token, the user information retrieved from the OAuth2 UserInfo endpoint, the OAuth2 access token, or the OAuth2 `/emails` endpoint. Grafana looks at these sources in the order listed until an email address is found. If no email is found, then the email address of the user is set to an empty string. @@ -177,6 +173,10 @@ Refer to the following table for information on what to configure based on how t | `upn` field of the OAuth2 ID token. | N/A | | `email` field of the user information from the UserInfo endpoint. | N/A | | Another field of the user information from the UserInfo endpoint. | Set `email_attribute_path` configuration option. | +| `email` field of the OAuth2 access token. | N/A | +| `attributes` map of the OAuth2 access token. | Set `email_attribute_name` configuration option. By default, Grafana searches for email under `email:primary` key. | +| `upn` field of the OAuth2 access token. | N/A | +| Another field of the OAuth2 access token. | Set `email_attribute_path` configuration option. | | Email address marked as primary from the `/emails` endpoint of
the OAuth2 provider (obtained by appending `/emails` to the URL
configured with `api_url`) | N/A | ### Configure a refresh token @@ -199,6 +199,7 @@ The `accessTokenExpirationCheck` feature toggle has been removed in Grafana v10. Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from the auth provider upon user login. The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option. +Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no role is found, the expression will be evaluated using the OAuth2 access token. To map the server administrator role, use the `allow_assign_grafana_admin` configuration option. Refer to [configuration options](#configuration-options) for more information. @@ -319,13 +320,14 @@ org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can link your OAuth2 groups to teams within Grafana. This will automatically assign users to the appropriate teams. Teams for each user are synchronized when the user logs in. Generic OAuth groups can be referenced by group ID, such as `8bab1c86-8fba-33e5-2089-1d1c80ec267d` or `myteam`. +Group information can be extracted from the OAuth2 ID token, user information from the UserInfo endpoint, or the OAuth2 access token. For information on configuring OAuth2 groups with Grafana using the `groups_attribute_path` configuration option, refer to [configuration options](#configuration-options). To learn more about Team Sync, refer to [Configure team sync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/). @@ -359,46 +361,46 @@ The following table outlines the various Generic OAuth configuration options. Yo If the configuration option requires a JMESPath expression that includes a colon, enclose the entire expression in quotes to prevent parsing errors. For example `role_attribute_path: "role:view"` {{< /admonition >}} -| Setting | Required | Supported on Cloud | Description | Default | -| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -| `enabled` | No | Yes | Enables Generic OAuth authentication. | `false` | -| `name` | No | Yes | Name that refers to the Generic OAuth authentication from the Grafana user interface. | `OAuth` | -| `icon` | No | Yes | Icon used for the Generic OAuth authentication in the Grafana user interface. | `signin` | -| `client_id` | Yes | Yes | Client ID provided by your OAuth2 app. | | -| `client_secret` | Yes | Yes | Client secret provided by your OAuth2 app. | | -| `auth_url` | Yes | Yes | Authorization endpoint of your OAuth2 provider. | | -| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | | -| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | | -| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | -| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `user:email` | -| `empty_scopes` | No | Yes | Set to `true` to use an empty scope during authentication. | `false` | -| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Generic OAuth login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` | -| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | -| `id_token_attribute_name` | No | Yes | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` | -| `login_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login](#configure-login). | | -| `name_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user name lookup from the user ID token. This name will be used as the user's display name. For more information on how user display name is retrieved, refer to [Configure display name](#configure-display-name). | | -| `email_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | | -| `email_attribute_name` | No | Yes | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | `email:primary` | -| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | | -| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` | -| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | -| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | -| `org_mapping` | No | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | -| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` | -| `groups_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | | -| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | | -| `allowed_organizations` | No | Yes | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | | -| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | -| `team_ids` | No | Yes | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | | -| `team_ids_attribute_path` | No | Yes | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | | -| `teams_url` | No | Yes | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | | -| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | -| `tls_client_cert` | No | No | The path to the certificate. | | -| `tls_client_key` | No | No | The path to the key. | | -| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | -| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` | -| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. | `false` | -| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | +| Setting | Required | Supported on Cloud | Description | Default | +| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | +| `enabled` | No | Yes | Enables Generic OAuth authentication. | `false` | +| `name` | No | Yes | Name that refers to the Generic OAuth authentication from the Grafana user interface. | `OAuth` | +| `icon` | No | Yes | Icon used for the Generic OAuth authentication in the Grafana user interface. | `signin` | +| `client_id` | Yes | Yes | Client ID provided by your OAuth2 app. | | +| `client_secret` | Yes | Yes | Client secret provided by your OAuth2 app. | | +| `auth_url` | Yes | Yes | Authorization endpoint of your OAuth2 provider. | | +| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | | +| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | | +| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` | +| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `user:email` | +| `empty_scopes` | No | Yes | Set to `true` to use an empty scope during authentication. | `false` | +| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Generic OAuth login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` | +| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | +| `id_token_attribute_name` | No | Yes | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` | +| `login_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login](#configure-login). | | +| `name_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user name lookup from the user ID token. This name will be used as the user's display name. For more information on how user display name is retrieved, refer to [Configure display name](#configure-display-name). | | +| `email_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | | +| `email_attribute_name` | No | Yes | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | `email:primary` | +| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no role is found, the expression will be evaluated using the OAuth2 access token. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | | +| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` | +| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` | +| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no value is returned, the expression will be evaluated using the OAuth2 access token. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `org_mapping` | No | No | List of comma- or space-separated `::` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | | +| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` | +| `groups_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no groups are found, the expression will be evaluated using the OAuth2 access token. The result of the evaluation should be a string array of groups. | | +| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | | +| `allowed_organizations` | No | Yes | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | | +| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | | +| `team_ids` | No | Yes | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | | +| `team_ids_attribute_path` | No | Yes | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | | +| `teams_url` | No | Yes | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | | +| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | No | The path to the certificate. | | +| `tls_client_key` | No | No | The path to the key. | | +| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` | +| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. | `false` | +| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | | ## Examples of setting up Generic OAuth diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 387f23425fb..9546be11af3 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -43,10 +43,6 @@ Ensure you know how to create a GitHub OAuth app. Consult GitHub's documentation ## Configure GitHub authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure GitHub OAuth client from within Grafana using the GitHub UI. To do this, navigate to **Administration > Authentication > GitHub** page and fill in the form. If you have a current configuration in the Grafana configuration file, the form will be pre-populated with those values. Otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -61,10 +57,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure GitHub authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "github_sso_settings" { provider_name = "github" @@ -219,7 +211,7 @@ role_attribute_path = [login=='octocat'][0] && 'GrafanaAdmin' || 'Viewer' ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and Grafana Cloud. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can map teams from your GitHub organization to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 04a39492d50..16c3ba72032 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -57,10 +57,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure GitLab authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "gitlab_sso_settings" { provider_name = "gitlab" @@ -241,7 +237,7 @@ use_refresh_token = true ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index 0d77243b6ae..8085e222252 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -45,10 +45,6 @@ First, you need to create a Google OAuth Client: ## Configure Google authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure Google OAuth client from within Grafana using the Google UI. To do this, navigate to **Administration > Authentication > Google** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -61,10 +57,6 @@ If you run Grafana in high availability mode, configuration changes may not get ## Configure Google authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "google_sso_settings" { provider_name = "google" @@ -167,6 +159,10 @@ auto_login = true ### Configure team synchronization +{{< admonition type="note" >}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +{{< /admonition >}} + With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example. To set up team sync for Google OAuth: diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md index 5798ee15540..88adc9d4a2a 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md @@ -106,10 +106,10 @@ editor viewer ``` -## Teamsync +## Team sync {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} [Teamsync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/) is a feature that allows you to map groups from your identity provider to Grafana teams. This is useful if you want to give your users access to specific dashboards or folders based on their group membership. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index ff316b4d2f6..3a90f19b371 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -101,10 +101,6 @@ If you want to configure the role for all users in the Okta directory, you can a ## Configure Okta authentication client using the Grafana UI -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. -{{< /admonition >}} - As a Grafana Admin, you can configure Okta OAuth2 client from within Grafana using the Okta UI. To do this, navigate to **Administration > Authentication > Okta** page and fill in the form. If you have a current configuration in the Grafana configuration file then the form will be pre-populated with those values otherwise the form will contain default values. After you have filled in the form, click **Save**. If the save was successful, Grafana will apply the new configurations. @@ -119,10 +115,6 @@ Refer to [configuration options](#configuration-options) for more information. ## Configure Okta authentication client using the Terraform provider -{{< admonition type="note" >}} -Available behind the `ssoSettingsAPI` feature toggle, which is enabled by default. Supported in the Terraform provider since v2.12.0. -{{< /admonition >}} - ```terraform resource "grafana_sso_settings" "okta_sso_settings" { provider_name = "okta" @@ -243,10 +235,10 @@ org_attribute_path = groups org_mapping = ["Group 1:org_foo:Viewer", "Group 2:org_bar:Editor", "*:3:Editor"] ``` -### Configure team synchronization (Enterprise only) +### Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](../../../../introduction/grafana-cloud). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can link your Okta groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md index 06a38944b83..4a846a622e2 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/_index.md @@ -40,7 +40,7 @@ If you are using Okta or Azure AD as Identity Provider, see the following docume - [Configure SAML with Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml/configure-saml-with-okta/) {{< admonition type="note" >}} -The API and Terraform support are available in Public Preview in Grafana v11.1 behind the `ssoSettingsSAML` feature toggle. You must also enable the `ssoSettingsApi` flag. +The API and Terraform support are available in Public Preview in Grafana v11.1 behind the `ssoSettingsSAML` feature toggle. {{< /admonition >}} All methods offer the same configuration options. However, if you want to keep all of Grafana authentication settings in one place, use the Grafana configuration file or the Terraform provider. If you are a Grafana Cloud user, you do not have access to Grafana configuration file. Instead, configure SAML through the other methods. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md index a7c6bc3fd88..edfedcefb9a 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md @@ -11,6 +11,10 @@ weight: 540 # Configure team sync for SAML +{{< admonition type="note" >}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +{{< /admonition >}} + To use SAML Team sync, set [`assertion_attribute_groups`](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_groups) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. {{< admonition type="warning" >}} diff --git a/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md b/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md index 4d9a5dab53c..75729ee8b7d 100644 --- a/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md @@ -20,7 +20,8 @@ weight: 300 System for Cross-domain Identity Management (SCIM) is an open standard that allows automated user provisioning and management. With SCIM, you can automate the provisioning of users and groups from your identity provider to Grafana. {{< admonition type="note" >}} -Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Pro and Advanced](/docs/grafana-cloud/). +Available in [Grafana Enterprise](/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Pro and Advanced](/docs/grafana-cloud/) in [public preview](https://grafana.com/docs/release-life-cycle/). +Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. {{< /admonition >}} {{< admonition type="note" >}} diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index c7c16c0f924..f99de6a6189 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -16,7 +16,9 @@ weight: 1000 Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana. -> **Note:** Available in [Grafana Enterprise](../../../introduction/grafana-enterprise/) and [Grafana Cloud Pro and Advanced](/docs/grafana-cloud/). +{{< admonition type="note" >}} +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +{{< /admonition >}} Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot. This mechanism allows Grafana to remove an existing synchronized user from a team when its group membership changes. This mechanism also enables you to manually add a user as member of a team, and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. @@ -27,14 +29,14 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea ## Supported providers -- [Auth Proxy](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/auth-proxy/#team-sync-enterprise-only) -- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#team-sync-enterprise-only) +- [Auth Proxy](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/auth-proxy/#team-sync) +- [Azure AD](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/azuread/#team-sync) - [Generic OAuth integration](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/generic-oauth/#configure-team-synchronization) - [GitHub OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/github/#configure-team-synchronization) - [GitLab OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/gitlab/#configure-team-synchronization) - [Google OAuth](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/google/#configure-team-synchronization) - [LDAP](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/enhanced-ldap/) -- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta/#configure-team-synchronization-enterprise-only) +- [Okta](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/okta/#configure-team-synchronization) - [SAML](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/saml/) ## Synchronize a Grafana team with an external group diff --git a/docs/sources/shared/visualizations/datalink-options-2.md b/docs/sources/shared/visualizations/datalink-options-2.md index c34cb13a228..37cc660b23d 100644 --- a/docs/sources/shared/visualizations/datalink-options-2.md +++ b/docs/sources/shared/visualizations/datalink-options-2.md @@ -26,6 +26,7 @@ For each action, define the following API call settings: | Confirmation message | A descriptive prompt to confirm or cancel the action. | | Method | Select from **POST**, **PUT**, or **GET**. | | URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | +| Variables | **Key** and **Name** pairs with a type selection. Click the **+** icon to add as many variables as you need. To add a variable to the request, prefix the key with `$`. You can set the values for the variables when performing an action. | | Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | | Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | | Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | diff --git a/docs/sources/shared/visualizations/datalink-options-3.md b/docs/sources/shared/visualizations/datalink-options-3.md index bc68c9c45fb..f99d6c9a065 100644 --- a/docs/sources/shared/visualizations/datalink-options-3.md +++ b/docs/sources/shared/visualizations/datalink-options-3.md @@ -28,6 +28,7 @@ For each action, define the following API call settings: | Confirmation message | A descriptive prompt to confirm or cancel the action. | | Method | Select from **POST**, **PUT**, or **GET**. | | URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | +| Variables | **Key** and **Name** pairs with a type selection. Click the **+** icon to add as many variables as you need. To add a variable to the request, prefix the key with `$`. You can set the values for the variables when performing an action. | | Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | | Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | | Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | diff --git a/docs/sources/tutorials/alerting-get-started-pt2/index.md b/docs/sources/tutorials/alerting-get-started-pt2/index.md index 8559c75b8c7..b64564ec74e 100644 --- a/docs/sources/tutorials/alerting-get-started-pt2/index.md +++ b/docs/sources/tutorials/alerting-get-started-pt2/index.md @@ -182,6 +182,10 @@ Create a notification policy if you want to handle metrics returned by alert rul {{< admonition type="note" >}} If you don’t have any contact points, add a [Contact point](https://grafana.com/tutorials/alerting-get-started/#create-a-contact-point). {{}} +1. Enable Continue matching: + + Turn on **Continue matching subsequent sibling nodes** so the evaluation continues even after one or more labels (i.e. `device` label) match. + 1. Click **Save Policy**. This new child policy routes alerts that match the label `device=desktop` to the Webhook contact point. diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 23f3894a84e..542190188f1 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -24,6 +24,8 @@ killercoda: In this tutorial, we walk you through the process of setting up your first alert in just a few minutes. You'll witness your alert in action with real-time data, as well as sending alert notifications. +{{< youtube id="YVhKNbQNNss" >}} + In this tutorial you will: - Create a contact point. diff --git a/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts b/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts new file mode 100644 index 00000000000..29cc753df75 --- /dev/null +++ b/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts @@ -0,0 +1,544 @@ +import { readFileSync } from 'fs'; +import { load } from 'js-yaml'; +import { Page } from 'playwright-core'; +import { v4 as uuidv4 } from 'uuid'; + +import { + test, + expect, + CreateDataSourcePageArgs, + DataSourceConfigPage, + E2ESelectorGroups, + DashboardPage, +} from '@grafana/plugin-e2e'; + +import { AzureQueryType } from '../../public/app/plugins/datasource/azuremonitor/dataquery.gen'; +import { selectors as azMonSelectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors'; +import { + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceSecureJsonData, +} from '../../public/app/plugins/datasource/azuremonitor/types/types'; + +const provisioningPath = 'provisioning/datasources/azmonitor-ds.yaml'; + +type AzureMonitorConfig = { + secureJsonData: AzureMonitorDataSourceSecureJsonData; + jsonData: AzureMonitorDataSourceJsonData; +}; + +type AzureMonitorProvision = { datasources: AzureMonitorConfig[] }; + +const dataSourceName = `Azure Monitor E2E Tests - ${uuidv4()}`; +const storageAcctName = 'azmonteststorage'; +const logAnalyticsName = 'az-mon-test-logs'; +const applicationInsightsName = 'az-mon-test-ai-a'; +const rootSubscription = 'grafanalabs-datasources-dev'; + +async function provisionAzureMonitorDatasources( + createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise, + datasourceConfig: AzureMonitorConfig, + page: Page, + selectors: E2ESelectorGroups +) { + const configPage = await createDataSourceConfigPage({ + type: 'grafana-azure-monitor-datasource', + name: dataSourceName, + }); + + const azureCloudInput = page.getByTestId(azMonSelectors.components.configEditor.azureCloud.input).locator('input'); + await azureCloudInput.fill('Azure'); + await azureCloudInput.press('Enter'); + const tenantIdInput = page.getByTestId(azMonSelectors.components.configEditor.tenantID.input).locator('input'); + await tenantIdInput.fill(datasourceConfig.jsonData.tenantId!); + const clientIdInput = page.getByTestId(azMonSelectors.components.configEditor.clientID.input).locator('input'); + await clientIdInput.fill(datasourceConfig.jsonData.clientId!); + const clientSecretInput = page + .getByTestId(azMonSelectors.components.configEditor.clientSecret.input) + .locator('input'); + await clientSecretInput.fill(datasourceConfig.secureJsonData.clientSecret!); + const loadSubscriptionsButton = page.getByTestId(azMonSelectors.components.configEditor.loadSubscriptions.button); + await loadSubscriptionsButton.click(); + const defaultSubscriptionInput = page + .getByTestId(azMonSelectors.components.configEditor.defaultSubscription.input) + .locator('input'); + await defaultSubscriptionInput.fill('datasources'); + await configPage + .getByGrafanaSelector(selectors.components.Select.option) + .filter({ + hasText: 'datasources', + }) + .click(); + await configPage.saveAndTest(); +} + +// TODO unskip when we've figured out how to populate the credentials in CI +test.describe.skip( + 'Azure Monitor datasource', + { + tag: ['@cloud-plugins'], + }, + () => { + let datasourceConfig: AzureMonitorConfig; + + test.beforeAll(async () => { + // Check if we're running in CI + const CI = process.env.CI; + if (CI) { + const outputs = JSON.parse(readFileSync('outputs.json', 'utf8')); + datasourceConfig = { + jsonData: { + cloudName: 'Azure', + tenantId: outputs.tenantId, + clientId: outputs.clientId, + }, + secureJsonData: { clientSecret: outputs.clientSecret }, + }; + } else { + const yamlContent = readFileSync(provisioningPath, 'utf8'); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const yaml = load(yamlContent) as AzureMonitorProvision; + datasourceConfig = yaml.datasources[0]; + } + }); + + test('create dashboard, add panel for metrics, log analytics, ARG, and traces queries', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + selectors, + page, + }) => { + // this test can absolutely take longer than the default 30s timeout + test.setTimeout(120000); + await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({ + timeRange: { + from: 'now-6h', + to: 'now', + zone: 'Coordinated Universal Time', + }, + }); + + // Add metrics panel + const metricsPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + const datasourcePicker = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill(dataSourceName); + const datasourceList = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText(dataSourceName).click(); + + // Configure metrics query + const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button); + await resourcePickerButton.click(); + await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 }); + const resourceSearchInput = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.search.input); + await resourceSearchInput.fill(storageAcctName); + await expect(page.getByText(storageAcctName)).toBeVisible({ timeout: 30000 }); + await page.getByText(storageAcctName).click(); + const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button); + await applyButton.click(); + await expect(page.getByText('microsoft.storage/storageaccounts')).toBeVisible(); + const metricNameInput = page + .getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input) + .locator('input'); + await metricNameInput.fill('Used capacity'); + await metricNameInput.press('Enter'); + + // Save and go back to dashboard + metricsPanel.backToDashboard(); + + // Add logs panel + const logsPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + await datasourcePicker.fill(dataSourceName); + await datasourceList.getByText(dataSourceName).click(); + + // Switch to Logs query type + const queryTypeSelect = page.getByTestId(azMonSelectors.components.queryEditor.header.select).locator('input'); + await queryTypeSelect.fill('Logs'); + await queryTypeSelect.press('Enter'); + + // Configure logs query + await resourcePickerButton.click(); + await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 }); + await resourceSearchInput.fill(logAnalyticsName); + await expect(page.getByText(logAnalyticsName)).toBeVisible({ timeout: 30000 }); + await page.getByText(logAnalyticsName).click(); + await applyButton.click(); + let codeEditor = logsPanel.getByGrafanaSelector(selectors.components.CodeEditor.container).locator('textarea'); + await codeEditor.fill('AzureDiagnostics', { force: true }); + const formatSelection = page + .getByTestId(azMonSelectors.components.queryEditor.logsQueryEditor.formatSelection.input) + .locator('input'); + await formatSelection.fill('Time series'); + await formatSelection.press('Enter'); + + // Save and go back to dashboard + logsPanel.backToDashboard(); + + // Add Azure Resource Graph panel + const resourceGraphPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + await datasourcePicker.fill(dataSourceName); + await datasourceList.getByText(dataSourceName).click(); + + // Switch to Azure Resource Graph query type + await queryTypeSelect.fill('Azure Resource Graph'); + await queryTypeSelect.press('Enter'); + + // Configure Azure Resource Graph query + const subscriptionsInput = page + .getByTestId(azMonSelectors.components.queryEditor.argsQueryEditor.subscriptions.input) + .locator('input'); + await subscriptionsInput.fill('datasources'); + await subscriptionsInput.press('Enter'); + codeEditor = resourceGraphPanel + .getByGrafanaSelector(selectors.components.CodeEditor.container) + .locator('textarea'); + await codeEditor.fill( + "Resources | where resourceGroup == 'cloud-plugins-e2e-test-azmon' | project name, resourceGroup" + ); + + // Save and go back to dashboard + resourceGraphPanel.backToDashboard(); + + // Add traces panel + const tracesPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + await datasourcePicker.fill(dataSourceName); + await datasourceList.getByText(dataSourceName).click(); + + // Switch to Traces query type + await queryTypeSelect.fill('Traces'); + await queryTypeSelect.press('Enter'); + + // Configure traces query + await resourcePickerButton.click(); + await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 }); + await resourceSearchInput.fill(applicationInsightsName); + await expect(page.getByText(applicationInsightsName)).toBeVisible({ timeout: 30000 }); + await page.getByText(applicationInsightsName).click(); + await applyButton.click(); + await formatSelection.fill('Trace'); + await formatSelection.press('Enter'); + }); + + test('create dashboard with template variables', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + page, + selectors, + }) => { + await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({ + timeRange: { + from: 'now-6h', + to: 'now', + }, + }); + + // Add subscription variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'subscription', + AzureQueryType.SubscriptionsQuery, + true + ); + + // Add resource groups variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'resourceGroups', + AzureQueryType.ResourceGroupsQuery, + false, + { + subscription: '$subscription', + } + ); + + // Add namespaces variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'namespaces', + AzureQueryType.NamespacesQuery, + false, + { + subscription: '$subscription', + resourceGroup: '$resourceGroups', + } + ); + + // Add region variable + await addAzureMonitorVariable(page, dashboardPage, selectors, 'region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); + + // Add resource variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'resource', + AzureQueryType.ResourceNamesQuery, + false, + { + subscription: '$subscription', + resourceGroup: '$resourceGroups', + namespace: '$namespaces', + region: '$region', + } + ); + + // Set variable values + const subscriptionVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('subscription')) + .locator('..') + .locator('input'); + await subscriptionVariable.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .filter({ hasText: 'grafanalabs-datasources-dev' }) + .click(); + + const resourceGroupsVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resourceGroups')) + .locator('..') + .locator('input'); + await resourceGroupsVariable.fill('cloud-plugins-e2e-test-azmon'); + await resourceGroupsVariable.press('ArrowDown'); + await resourceGroupsVariable.press('Enter'); + + const namespacesVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('namespaces')) + .locator('..') + .locator('input'); + await namespacesVariable.fill('microsoft.storage/storageaccounts'); + await namespacesVariable.press('ArrowDown'); + await namespacesVariable.press('Enter'); + + const regionVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('region')) + .locator('..') + .locator('input'); + await regionVariable.fill('uk south'); + await regionVariable.press('ArrowDown'); + await regionVariable.press('Enter'); + + const resourceVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resource')) + .locator('..') + .locator('input'); + await resourceVariable.fill(storageAcctName); + await resourceVariable.press('ArrowDown'); + await resourceVariable.press('Enter'); + + // Add panel with template variables + const newPanel = await dashboardPage.addPanel(); + + const datasourcePicker = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill(dataSourceName); + const datasourceList = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText(dataSourceName).click(); + + const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button); + await resourcePickerButton.click(); + const advancedCollapse = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.collapse); + await advancedCollapse.click(); + const advancedSubscription = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.subscription.input) + .locator('input'); + await advancedSubscription.fill('$subscription'); + const advancedResourceGroup = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resourceGroup.input) + .locator('input'); + await advancedResourceGroup.fill('$resourceGroups'); + const advancedNamespace = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.namespace.input) + .locator('input'); + await advancedNamespace.fill('$namespaces'); + const advancedRegion = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.region.input) + .locator('input'); + await advancedRegion.fill('$region'); + const advancedResource = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resource.input) + .locator('input'); + await advancedResource.fill('$resource'); + + const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button); + await applyButton.click(); + + const metricNameInput = page + .getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input) + .locator('input'); + await metricNameInput.fill('Transactions'); + await metricNameInput.press('Enter'); + }); + + // Helper function to add template variables + async function addAzureMonitorVariable( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + name: string, + type: AzureQueryType, + isFirst: boolean, + options?: { + subscription?: string; + resourceGroup?: string; + namespace?: string; + resource?: string; + region?: string; + } + ) { + // Navigate to variables settings + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.settingsButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Variables')).click(); + + if (isFirst) { + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2) + .click(); + } else { + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + } + + // Configure variable + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.clear(); + await nameInput.fill(name); + + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill(dataSourceName); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText(dataSourceName).click(); + + const queryTypeInput = page + .getByTestId(azMonSelectors.components.variableEditor.queryType.input) + .locator('input'); + await queryTypeInput.fill(type.replace('Azure', '').trim()); + await queryTypeInput.press('Enter'); + + // Configure type-specific options + switch (type) { + case AzureQueryType.ResourceGroupsQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + break; + case AzureQueryType.LocationsQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + break; + case AzureQueryType.NamespacesQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + if (options?.resourceGroup) { + const resourceGroupInput = page + .getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input) + .locator('input'); + await resourceGroupInput.fill(options.resourceGroup); + await resourceGroupInput.press('Enter'); + } + break; + case AzureQueryType.ResourceNamesQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + if (options?.resourceGroup) { + const resourceGroupInput = page + .getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input) + .locator('input'); + await resourceGroupInput.fill(options.resourceGroup); + await resourceGroupInput.press('Enter'); + } + if (options?.namespace) { + const namespaceInput = page + .getByTestId(azMonSelectors.components.variableEditor.namespace.input) + .locator('input'); + await namespaceInput.fill(options.namespace); + await namespaceInput.press('Enter'); + } + if (options?.region) { + const regionInput = page + .getByTestId(azMonSelectors.components.variableEditor.region.input) + .locator('input'); + await regionInput.fill(options.region); + await regionInput.press('Enter'); + } + break; + case AzureQueryType.MetricNamesQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + if (options?.resourceGroup) { + const resourceGroupInput = page + .getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input) + .locator('input'); + await resourceGroupInput.fill(options.resourceGroup); + await resourceGroupInput.press('Enter'); + } + if (options?.namespace) { + const namespaceInput = page + .getByTestId(azMonSelectors.components.variableEditor.namespace.input) + .locator('input'); + await namespaceInput.fill(options.namespace); + await namespaceInput.press('Enter'); + } + if (options?.resource) { + const resourceInput = page + .getByTestId(azMonSelectors.components.variableEditor.resource.input) + .locator('input'); + await resourceInput.fill(options.resource); + await resourceInput.press('Enter'); + } + break; + } + + // Save variable + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton) + .click(); + + // Go back to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + } + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts new file mode 100644 index 00000000000..f42082587d8 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; + +import { flows } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.describe( + 'Dashboard panels', + { + tag: ['@dashboards'], + }, + () => { + test('can duplicate a panel', async ({ dashboardPage, selectors, page }) => { + await page.goto(selectors.pages.ImportDashboard.url); + await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); + await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); + await page.getByTestId(selectors.components.ImportDashboardForm.name).fill('Paste tab'); + await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + const oldPanelTitle = 'New panel'; + const panelTitle = 'Unique'; + await flows.changePanelTitle(dashboardPage, selectors, oldPanelTitle, panelTitle); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle)) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu(panelTitle)) + .click({ force: true }); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('More...')).hover(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Duplicate')).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount( + 2 + ); + + // Save, reload, and ensure duplicate has persisted + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click(); + await page.reload(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount( + 2 + ); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts new file mode 100644 index 00000000000..80f94f855b2 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -0,0 +1,804 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +// these tests require a larger viewport +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +test.describe( + 'Grouping panels', + { + tag: ['@dashboards'], + }, + () => { + // Helper functions + async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) { + await page.goto(selectors.pages.ImportDashboard.url); + await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); + await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); + await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title); + await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); + } + + async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click(); + } + + async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into row').click(); + } + + async function groupIntoTab(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into tab').click(); + } + + async function ungroupPanels(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroup).click(); + } + + async function addPanel(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click(); + } + + /* + * Rows + */ + + test('can group and ungroup new panels into row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into row + await groupIntoRow(page, dashboardPage, selectors); + + // Verify row and panel titles + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify row and panel titles after reload + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can add and remove several rows', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Add and remove rows'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click(); + await addPanel(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(5); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(5); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + .locator('..') + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + .locator('..') + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can paste a copied row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Paste row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Copy by selecting row and using copy button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Copy' }).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteRow).click(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // scroll last `New panel` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + .last() + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + }); + + test('can duplicate a row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Duplicate row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Duplicate by selecting row and using duplicate button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // scroll last `New panel` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + .last() + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + }); + + test('can collapse rows', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Collapse rows'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Duplicate row + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + + // Collapse rows by clicking on their titles + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(0); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(0); + }); + + test('can convert rows into tabs when changing layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Rows to tabs'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Duplicate row + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + + // Go back to dashboard options + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + + // Expand layouts section + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category')) + .click(); + + // Select tabs layout + await page.getByLabel('Tabs').click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1')).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row')).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can group and ungroup new panels into row with tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into tab with row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into row with tab + await groupIntoRow(page, dashboardPage, selectors); + await groupIntoTab(page, dashboardPage, selectors); + + // Verify tab and panel titles + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify tab, row and panel titles after reload + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); // ungroup tabs + await ungroupPanels(dashboardPage, selectors); // ungroup rows + + // Verify tab and row titles is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + /* + * Tabs + */ + + test('can group and ungroup new panels into tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into tab'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into tab + await groupIntoTab(page, dashboardPage, selectors); + + // Verify tab and panel titles + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify row and panel titles after reload + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can add and remove several tabs', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Add and remove tabs'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click(); + await addPanel(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click(); + await addPanel(dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute( + 'aria-selected', + 'true' + ); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(1); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute( + 'aria-selected', + 'true' + ); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(1); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can paste a copied tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Paste tab'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + // Copy by selecting tab and using copy button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Copy' }).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteTab).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can duplicate a tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Duplicate tab'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + // Duplicate by selecting tab and using duplicate button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can convert tabs into rows when changing layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Tabs to rows'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + // Duplicate tab twice + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); + + // Go back to dashboard options + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + + // Expand layouts section + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category')) + .click(); + + // Select rows layout + await page.getByLabel('Rows').click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab')) + ).toBeVisible(); + // Wait for panels to load + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first() + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + ).toBeVisible(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(9); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab')) + ).toBeVisible(); + // Wait for panels to load + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first() + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + ).toBeVisible(); + + // scroll last `New panel` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + .last() + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(9); + }); + + test('can group and ungroup new panels into tab with row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into tab with row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into tab + await groupIntoTab(page, dashboardPage, selectors); + await groupIntoRow(page, dashboardPage, selectors); + + // Verify tab and panel titles + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify tab, row and panel titles after reload + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); // ungroup rows + await ungroupPanels(dashboardPage, selectors); // ungroup tabs + + // Verify tab and row titles is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts new file mode 100644 index 00000000000..c2214121d85 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +test.describe( + 'Dashboard Outline', + { + tag: ['@dashboards'], + }, + () => { + test('can use dashboard outline', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); + + // Should be able to click Variables item in outline to see add variable button + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) + ).toBeVisible(); + + // Clicking a panel should scroll that panel in view + await expect(page.getByText('Dashboard panel 48')).toBeHidden(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click(); + await expect(page.getByText('Dashboard panel 48')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts new file mode 100644 index 00000000000..d0b4cb5fcaf --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard panels', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new panel', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click(); + + // Check that new panel has been added + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + + // Check that pressing the configure button shows the panel editor + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .filter({ hasText: 'Configure' }) + .click(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts new file mode 100644 index 00000000000..01a3e65db22 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Ad hoc variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new adhoc variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'adhoc', + name: 'VariableUnderTest', + value: 'label1', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // Select datasource for the ad hoc variable + const dataSource = 'gdev-loki'; + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect) + .click(); + await page.getByText(dataSource).click(); + + // mock the API call to get the labels + const labels = ['label1', 'label2']; + await page.route('**/resources/labels*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labels, + }), + }); + }); + + // Select the variable in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // mock the API call to get the label values + const labelValues = ['label2Value1']; + await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labelValues, + }), + }); + }); + + // choose the label and value + await page.getByText(labels[1]).click(); + await page.getByText('=', { exact: true }).click(); + await page.getByText(labelValues[0]).click(); + await page.keyboard.press('Escape'); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}="${labelValues[0]}"`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts new file mode 100644 index 00000000000..14af22ab2c3 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - datasource variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new datasource variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const dsType = 'cloudwatch'; + const variable: Variable = { + type: 'datasource', + name: 'VariableUnderTest', + label: 'VariableUnderTest', + value: `gdev-${dsType}`, + }; + + // Common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect) + .click(); + await page.getByText(dsType).click(); + + const regexFilter = 'cloud'; + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.nameFilter) + .fill(regexFilter); + + // Assert the variable dropdown is visible with correct label + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // Assert the variable values are correctly displayed in the panel + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`${variable.name}: ${variable.value}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts new file mode 100644 index 00000000000..e964414dcad --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Group By variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new group by variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'groupby', + name: 'VariableUnderTest', + value: 'label1', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // select datasource for the group by variable + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect) + .click(); + const dataSource = 'gdev-loki'; + await page.getByText(dataSource).click(); + + // mock the API call to get the labels + const labels = ['label1', 'label2']; + await page.route('**/resources/labels*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labels, + }), + }); + }); + + // open the variable dropdown in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).locator('> div').click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // mock the API call to get the label values + const labelValues = ['label2Value1']; + await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labelValues, + }), + }); + }); + + // choose the label and value + await page.getByText(labels[1]).click(); + await page.keyboard.press('Escape'); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts new file mode 100644 index 00000000000..30373362528 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph'; + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can edit panel title and description', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + const oldTitle = 'No Data Points Warning'; + const firstPanelTitle = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .first() + .locator('h2') + .first(); + await expect(firstPanelTitle).toHaveText(oldTitle); + + const newDescription = 'A description of this panel'; + await flows.changePanelDescription(dashboardPage, selectors, oldTitle, newDescription); + + const newTitle = 'New Panel Title'; + await flows.changePanelTitle(dashboardPage, selectors, oldTitle, newTitle); + + // Check that new title is reflected in panel header + const updatedPanelTitle = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .first() + .locator('h2') + .first(); + await expect(updatedPanelTitle).toHaveText(newTitle); + + // Reveal description tooltip and check that its value is as expected + const descriptionIcon = page.locator('[data-testid="title-items-container"] > span').first(); + await descriptionIcon.click({ force: true }); + + // Get the tooltip ID from the aria-describedby attribute + const tooltipId = await descriptionIcon.getAttribute('aria-describedby'); + const tooltip = page.locator(`[id="${tooltipId}"]`); + await expect(tooltip).toHaveText(`${newDescription}\n`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts new file mode 100644 index 00000000000..d483b7b8005 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph'; + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can toggle transparent background switch', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^No Data Points Warning$/ }) + .first() + .click(); + + const panelTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('No Data Points Warning') + ); + + const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background); + expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/); + + await page.locator('#transparent-background').click({ force: true }); + + const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background); + expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts new file mode 100644 index 00000000000..6c55988a309 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Query variable', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new query variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const queryVariableOptions = ['default']; + + const variable: Variable = { + type: 'query', + name: 'VariableUnderTest', + value: queryVariableOptions[0], + label: 'VariableUnderTest', // constant doesn't really need a label + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // open the modal query variable editor + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsOpenButton) + .click(); + // select a core data source that just runs a query during preview + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + + const dataSource = 'gdev-cloudwatch'; + // this will trigger an API call to get the query options + await page.getByText(dataSource).click(); + + // show the preview of the query results + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.previewButton) + .click(); + // assert the query results are shown + const firstPreviewOption = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption) + .first(); + await expect(firstPreviewOption).toBeVisible(); + const previewOptionText = await firstPreviewOption.textContent(); + const previewOption = previewOptionText?.trim() || ''; + + // close the modal + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.closeButton) + .click(); + + // assert the query variable values are in the variable value select + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!) + ); + const nextElement = variableLabel.locator('+ *'); + await expect(nextElement).toHaveText(previewOption); + + // Assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${previewOption}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts new file mode 100644 index 00000000000..411d2122954 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'custom', + name: 'foo', + label: 'Foo', + value: 'one,two,three', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // set the custom variable value + const customValueInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput + ); + await customValueInput.fill(variable.value); + await customValueInput.blur(); + + // assert the dropdown for the variable is visible and has the correct values + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + const values = variable.value.split(','); + const firstValueLink = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(values[0]) + ); + await expect(firstValueLink).toBeVisible(); + + // check that variable deletion works + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await expect(variableLabel).toBeHidden(); + }); + + test('can add a new constant variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'constant', + name: 'VariableUnderTest', + value: 'foo', + label: 'VariableUnderTest', // constant doesn't really need a label + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // set the constant variable value + const type = 'variable-type Value'; + const fieldLabel = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(type) + ); + await expect(fieldLabel).toBeVisible(); + const inputField = fieldLabel.locator('input'); + await expect(inputField).toBeVisible(); + await inputField.fill(variable.value); + await inputField.blur(); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`); + }); + + test('can add a new textbox variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'textbox', + name: 'VariableUnderTest', + value: 'foo', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // set the textbox variable value + const type = 'variable-type Value'; + const fieldLabel = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(type) + ); + await expect(fieldLabel).toBeVisible(); + const inputField = fieldLabel.locator('input'); + await expect(inputField).toBeVisible(); + await inputField.fill(variable.value); + await inputField.blur(); + + // select the variable in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`); + }); + + test('can add a new interval variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'interval', + name: 'VariableUnderTest', + value: '1m', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // enable the auto option + await page.getByText('Auto option').click(); + + // select the variable in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`); + + // select the variable in the dashboard and set the Auto option + const variableDropdown = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!) + ); + const nextElement = variableDropdown.locator('+ *'); + await expect(nextElement).toHaveText('1m'); + await nextElement.click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Auto' }).click(); + + // assert the panel is visible and has the correct "Auto" value + await expect(panelContent).toBeVisible(); + await expect(markdownContent).toContainText('VariableUnderTest: 10m'); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts new file mode 100644 index 00000000000..b376c6f6af2 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts @@ -0,0 +1,120 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'ed155665/annotation-filtering'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, + // these tests require a larger viewport + viewport: { width: 1920, height: 1080 }, +}); + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can drag and drop panels', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Move panel three to panel one position + await movePanel(dashboardPage, selectors, /^Panel three$/, /^Panel one$/); + + // Verify panel three is now above panel one + const panel3Position = await getPanelPosition(dashboardPage, selectors, /^Panel three$/); + const panel1Position = await getPanelPosition(dashboardPage, selectors, /^Panel one$/); + + expect(panel3Position?.y).toBeLessThan(panel1Position?.y || 0); + + // Move panel two to panel three position + await movePanel(dashboardPage, selectors, /^Panel two$/, /^Panel three$/); + + // Verify panel two is now above panel three + const panel2Position = await getPanelPosition(dashboardPage, selectors, /^Panel two$/); + const panel3PositionAfter = await getPanelPosition(dashboardPage, selectors, /^Panel three$/); + + expect(panel2Position?.y).toBeLessThan(panel3PositionAfter?.y || 0); + }); + + // Note, moving a panel from a nested row to a parent row currently just deletes the panel + // This test will need to be updated once the correct behavior is implemented. + test('can move panel from nested row to parent row', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + await groupIntoRow(page, dashboardPage, selectors); + + const firstRowElement = dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + .first(); + const rowBoundingBox = await firstRowElement.boundingBox(); + + if (!rowBoundingBox) { + throw new Error('Row element not found'); + } + + const panel1Element = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel one$/ }); + + await panel1Element.hover(); + await page.mouse.down(); + await page.mouse.move(rowBoundingBox.x, rowBoundingBox.y); + await page.mouse.up(); + + await expect( + dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel one$/ }) + ).toBeHidden(); + }); + } +); + +// Helper functions +async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into row').click(); +} + +async function getPanelPosition( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + panelTitle: string | RegExp +) { + const panel = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: panelTitle }); + const boundingBox = await panel.boundingBox(); + return boundingBox; +} + +async function movePanel( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + sourcePanel: string | RegExp, + targetPanel: string | RegExp +) { + // Get target panel position + const targetPanelElement = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: targetPanel }); + + // Get source panel element + const sourcePanelElement = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: sourcePanel }); + + // Perform drag and drop + await sourcePanelElement.dragTo(targetPanelElement); +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts new file mode 100644 index 00000000000..2cdb847a03f --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -0,0 +1,409 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.describe( + 'Dashboard Panel Layouts', + { + tag: ['@dashboards'], + }, + () => { + test('can switch to auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Switch to auto grid'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await checkAutoGridLayoutInputs(dashboardPage, selectors); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await checkAutoGridLayoutInputs(dashboardPage, selectors); + }); + + test('can change min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set min column width'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + // Get initial positions - standard width should have panels on different rows + const firstPanelTop = await getPanelTop(dashboardPage, selectors); + const lastPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).last(); + const lastPanelBox = await lastPanel.boundingBox(); + const lastPanelTop = lastPanelBox?.y || 0; + + // Verify standard layout has panels on different rows + expect(lastPanelTop).toBeGreaterThan(firstPanelTop); + + // Change to narrow min column width + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + .click(); + await page.getByRole('option', { name: 'Narrow' }).click(); + + // Verify narrow layout has all panels on same row + const firstPanelTopNarrow = await getPanelTop(dashboardPage, selectors); + const lastPanelBoxNarrow = await lastPanel.boundingBox(); + const lastPanelTopNarrow = lastPanelBoxNarrow?.y || 0; + + expect(lastPanelTopNarrow).toBe(firstPanelTopNarrow); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth + ) + ).toHaveValue('Narrow'); + + const firstPanelTopReload = await getPanelTop(dashboardPage, selectors); + const lastPanelBoxReload = await lastPanel.boundingBox(); + const lastPanelTopReload = lastPanelBoxReload?.y || 0; + + expect(lastPanelTopReload).toBe(firstPanelTopReload); + }); + + test('can change to custom min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set custom min column width'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + .click(); + await page.getByRole('option', { name: 'Custom' }).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) + .fill('900'); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) + .blur(); + + // Changing to 900 custom width should have each panel span the whole row (stacked vertically) + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth + ) + ).toHaveValue('900'); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth) + .click(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth + ) + ).toHaveValue('Standard'); + }); + + test('can change max columns in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set max columns'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + + // Changing to 1 max column should have each panel span the whole row (stacked vertically) + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) + ).toHaveValue('1'); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + }); + + test('can change row height in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set row height'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + const regularRowHeight = await getPanelHeight(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + .click(); + await page.getByRole('option', { name: 'Short' }).click(); + + const shortHeight = await getPanelHeight(dashboardPage, selectors); + expect(shortHeight).toBeLessThan(regularRowHeight); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + .click(); + await page.getByRole('option', { name: 'Tall' }).click(); + + const tallHeight = await getPanelHeight(dashboardPage, selectors); + expect(tallHeight).toBeGreaterThan(regularRowHeight); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + const tallHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(tallHeightAfterReload).toBeGreaterThan(regularRowHeight); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + ).toHaveValue('Tall'); + + const tallHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); + expect(tallHeightAfterEdit).toBeGreaterThan(regularRowHeight); + }); + + test('can change to custom row height in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set custom row height'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + const regularRowHeight = await getPanelHeight(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + .click(); + await page.getByRole('option', { name: 'Custom' }).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight) + .fill('800'); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight) + .blur(); + + const customHeight = await getPanelHeight(dashboardPage, selectors); + expect(customHeight).toBeCloseTo(800, 5); // Allow some tolerance for rendering differences + expect(customHeight).toBeGreaterThan(regularRowHeight); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + const customHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(customHeightAfterReload).toBeCloseTo(800, 5); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight + ) + ).toHaveValue('800'); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + ).toHaveValue('Standard'); + }); + + test('can change fill screen in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set fill screen'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + // Set narrow column width first to ensure panels fit horizontally + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + .click(); + await page.getByRole('option', { name: 'Narrow' }).click(); + + const initialHeight = await getPanelHeight(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) + .click({ force: true }); + + const fillScreenHeight = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeight).toBeGreaterThan(initialHeight); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + const fillScreenHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeightAfterReload).toBeGreaterThan(initialHeight); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) + ).toBeChecked(); + + const fillScreenHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeightAfterEdit).toBeGreaterThan(initialHeight); + }); + } +); + +// Helper functions +async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) { + await page.goto(selectors.pages.ImportDashboard.url); + await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); + await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); + await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title); + await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); +} + +async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click(); +} + +async function checkAutoGridLayoutInputs(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) + ).toBeVisible(); +} + +async function verifyPanelsStackedVertically(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + const panels = await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).all(); + let previousTop = 0; + + for (const panel of panels) { + const boundingBox = await panel.boundingBox(); + if (boundingBox) { + if (previousTop === 0) { + previousTop = boundingBox.y; + } else { + expect(boundingBox.y).toBeGreaterThan(previousTop); + previousTop = boundingBox.y; + } + } + } +} + +async function getPanelHeight(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first(); + const boundingBox = await panel.boundingBox(); + return boundingBox?.height || 0; +} + +async function getPanelTop(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first(); + const boundingBox = await panel.boundingBox(); + return boundingBox?.y || 0; +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts new file mode 100644 index 00000000000..5b5202f1455 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts @@ -0,0 +1,66 @@ +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +test.describe( + 'Dashboard panels', + { + tag: ['@dashboards'], + }, + () => { + test('can remove a panel', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Remove Panel #1 + await removePanelsByTitle(dashboardPage, selectors, ['Panel #1']); + + // Check that panel has been deleted + await expect( + dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel #1$/ }) + ).toBeHidden(); + }); + + test('can remove several panels at once', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Remove multiple panels + await removePanelsByTitle(dashboardPage, selectors, ['Panel #1', 'Panel #2', 'Panel #3']); + + // Check that panels have been deleted + await expect( + dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel #[123]$/ }) + ).toBeHidden(); + }); + } +); + +// Helper function to remove a panel by its title +async function removePanelsByTitle(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, panelTitles: string[]) { + for (const panelTitle of panelTitles) { + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: panelTitle }) + .click({ + modifiers: ['Shift'], + }); + } + + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts new file mode 100644 index 00000000000..87b8fc6ff3b --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'ed155665/annotation-filtering'; + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can change dashboard description and title', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Check that current dashboard title is visible in breadcrumb + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering')) + ).toBeVisible(); + + const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input'); + await expect(titleInput).toHaveValue('Annotation filtering'); + await titleInput.fill('New dashboard title'); + await expect(titleInput).toHaveValue('New dashboard title'); + + // Check that new dashboard title is reflected in breadcrumb + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('New dashboard title')) + ).toBeVisible(); + + // Check that we can successfully change the dashboard description + const descriptionTextArea = page.locator( + '[aria-label="dashboard-options Description field property editor"] textarea' + ); + await descriptionTextArea.fill('Dashboard description'); + await expect(descriptionTextArea).toHaveValue('Dashboard description'); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts new file mode 100644 index 00000000000..1ef85b85261 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -0,0 +1,84 @@ +import { DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const deselectPanels = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls).click({ + position: { x: 0, y: 0 }, + }); +}; + +export const flows = { + deselectPanels, + async changePanelTitle( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + oldPanelTitle: string, + newPanelTitle: string + ) { + await deselectPanels(dashboardPage, selectors); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: oldPanelTitle }) + .first() + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldInput('Title')) + .fill(newPanelTitle); + }, + async changePanelDescription( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + panelTitle: string, + newDescription: string + ) { + await deselectPanels(dashboardPage, selectors); + const panelTitleRegex = new RegExp(`^${panelTitle}$`); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: panelTitleRegex }) + .first() + .click(); + const descriptionTextArea = dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('panel-options Description')) + .locator('textarea'); + await descriptionTextArea.fill(newDescription); + }, + async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) + .click(); + }, + async newEditPanelCommonVariableInputs( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + variable: Variable + ) { + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.variableType(variable.type)) + .click(); + const variableNameInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.variableNameInput + ); + await variableNameInput.click(); + await variableNameInput.fill(variable.name); + await variableNameInput.blur(); + if (variable.label) { + const variableLabelInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.variableLabelInput + ); + await variableLabelInput.click(); + await variableLabelInput.fill(variable.label); + await variableLabelInput.blur(); + } + }, +}; + +export type Variable = { + type: string; + name: string; + label?: string; + description?: string; + value: string; +}; diff --git a/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts b/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts new file mode 100644 index 00000000000..742aa106336 --- /dev/null +++ b/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Dashboard search', + { + tag: ['@dashboards-search'], + }, + () => { + test.use({ viewport: { width: 1280, height: 1080 } }); + + test.beforeEach(async ({ page, selectors }) => { + await page.goto('/dashboards'); + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible(); + }); + + test('Search - Dashboards list', async ({ page, selectors }) => { + await toggleSearchView(page, selectors); + await assertResultsCount(page, 24); + }); + + test('Search - Filter by search input', async ({ page, selectors }) => { + await toggleSearchView(page, selectors); + await assertResultsCount(page, 24); + + const searchInput = await page.getByTestId('input-wrapper'); + await searchInput.click(); + await page.keyboard.type('Datasource tests - MySQL'); + + await assertResultsCount(page, 2); + + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type('Datasource tests - MySQL (unittest)'); + + await assertResultsCount(page, 1); + + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type('- MySQL'); + + await assertResultsCount(page, 2); + }); + } +); + +async function assertResultsCount(page, length) { + const rowGroup = await page.getByRole('rowgroup'); + await expect(rowGroup).toHaveCount(1); + + const rows = await rowGroup.first().getByRole('row'); + await expect(rows).toHaveCount(length); +} + +async function toggleSearchView(page, selectors) { + const toggleButtons = await page.getByTestId(selectors.pages.Dashboards.toggleView); + await expect(toggleButtons).toHaveCount(2); + + const listRadioButton = await toggleButtons.nth(1).locator('input'); + await expect(listRadioButton).toBeChecked({ checked: false }); + + await listRadioButton.check(); + await expect(listRadioButton).toBeChecked({ checked: true }); +} diff --git a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts new file mode 100644 index 00000000000..7f88e462deb --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { makeNewDashboardRequestBody } from './utils/makeDashboard'; + +const NUM_ROOT_FOLDERS = 60; +const NUM_ROOT_DASHBOARDS = 60; +const NUM_NESTED_FOLDERS = 60; +const NUM_NESTED_DASHBOARDS = 60; + +// TODO change this test so it doesn't conflict with the existing dashboard browse test +// probably needs a separate user +test.describe.skip( + 'Dashboard browse (nested)', + { + tag: ['@dashboards'], + }, + () => { + const dashboardUIDsToCleanUp: string[] = []; + const folderUIDsToCleanUp: string[] = []; + + test.beforeAll(async ({ request }) => { + // Add root folders + for (let i = 0; i < NUM_ROOT_FOLDERS; i++) { + const response = await request.post('/api/folders', { + data: { + title: `Root folder ${i.toString().padStart(2, '0')}`, + }, + }); + const responseBody = await response.json(); + folderUIDsToCleanUp.push(responseBody.uid); + } + + // Add root dashboards + for (let i = 0; i < NUM_ROOT_DASHBOARDS; i++) { + const response = await request.post('/api/dashboards/db', { + data: makeNewDashboardRequestBody(`Root dashboard ${i.toString().padStart(2, '0')}`), + }); + const responseBody = await response.json(); + dashboardUIDsToCleanUp.push(responseBody.uid); + } + + // Add folder with children + const folderResponse = await request.post('/api/folders', { + data: { + title: 'A root folder with children', + }, + }); + const folderResponseBody = await folderResponse.json(); + const folderUid = folderResponseBody.uid; + folderUIDsToCleanUp.push(folderUid); + + // Add nested folders + for (let i = 0; i < NUM_NESTED_FOLDERS; i++) { + await request.post('/api/folders', { + data: { + title: `Nested folder ${i.toString().padStart(2, '0')}`, + parentUid: folderUid, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + // Add nested dashboards + for (let i = 0; i < NUM_NESTED_DASHBOARDS; i++) { + await request.post('/api/dashboards/db', { + data: makeNewDashboardRequestBody(`Nested dashboard ${i.toString().padStart(2, '0')}`, folderUid), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + }); + + test.afterAll(async ({ request }) => { + // Clean up root dashboards + for (const dashboardUID of dashboardUIDsToCleanUp) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + + // Clean up root folders (cascading delete will remove any nested folders and dashboards) + for (const folderUID of folderUIDsToCleanUp) { + await request.delete(`/api/folders/${folderUID}`, { + params: { + forceDeleteRules: false, + }, + }); + } + }); + + test('pagination works correctly for folders and root', async ({ page, selectors }) => { + // Navigate to dashboards page + await page.goto('/dashboards'); + + // Wait for and verify the root folder with children is visible + await expect(page.getByText('A root folder with children')).toBeVisible(); + + // Expand A root folder with children + await page.getByLabel('Expand folder A root folder with children').click(); + await expect(page.getByText('Nested folder 00')).toBeVisible(); + + // Get the table body container for scrolling + const tableBody = page.getByTestId(selectors.pages.BrowseDashboards.table.body).locator('> div'); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 2100)); + await expect(page.getByText('Nested folder 59')).toBeVisible(); + await expect(page.getByText('Nested dashboard 00')).toBeVisible(); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 4200)); + await expect(page.getByText('Nested dashboard 59')).toBeVisible(); + await expect(page.getByText('Root folder 00')).toBeVisible(); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 6300)); + await expect(page.getByText('Root folder 59')).toBeVisible(); + await expect(page.getByText('Root dashboard 00')).toBeVisible(); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 8400)); + await expect(page.getByText('Root dashboard 59')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts new file mode 100644 index 00000000000..98106773c2d --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/TestDashboard.json'; + +test.describe( + 'Dashboard browse', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test.beforeAll(async ({ request }) => { + // Import the test dashboard + const response = await request.post('/api/dashboards/import', { + data: { + dashboard: testDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); + const responseBody = await response.json(); + dashboardUID = responseBody.uid; + }); + + test.afterAll(async ({ request }) => { + // Clean up the imported dashboard + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + }); + + test('Manage Dashboards tests', async ({ page, selectors }) => { + // Navigate to dashboards page + await page.goto('/dashboards'); + + // Folders and dashboards should be visible + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible(); + await expect( + page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard')) + ).toBeVisible(); + + // gdev dashboards folder is collapsed - its content should not be visible + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeHidden(); + + // should click a folder and see its children + await page + .getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards')) + .getByLabel(/Expand folder/) + .click(); + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeVisible(); + + // Open the new folder drawer + await page.getByText('New').click(); + await page.getByRole('menuitem', { name: 'New folder' }).click(); + + // And create a new folder + await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.nameInput).fill('My new folder'); + await page + .getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form) + .getByRole('button', { name: 'Create' }) + .click(); + // await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form).getByRole('button', { name: 'Create' }).click({ force: true }); + + // Verify success alert and close it + const alert = page.getByTestId(selectors.components.Alert.alertV2('success')); + await expect(alert).toBeVisible(); + await alert.getByLabel('Close alert').click(); + await expect(page.getByRole('heading', { name: 'My new folder' })).toBeVisible(); + + // Delete the folder and expect to go back to the root + await page.getByRole('button', { name: 'Folder actions' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete'); + await page.getByTestId(selectors.pages.ConfirmModal.delete).click(); + await expect(page.getByRole('heading', { name: 'Dashboards' })).toBeVisible(); + + // Can collapse the gdev folder and delete the dashboard we imported + await page + .getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards')) + .getByLabel(/Collapse folder/) + .click(); + + // Select the imported dashboard using checkbox + await page + .getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard')) + .getByRole('checkbox') + .click({ force: true }); + + // Delete the selected dashboard + await page.getByRole('button', { name: 'Delete' }).click(); + + // Confirm deletion in modal + await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete'); + await page.getByTestId(selectors.pages.ConfirmModal.delete).click(); + await expect( + page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard')) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts new file mode 100644 index 00000000000..d5a3f4ecac3 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Export as JSON', + { + tag: ['@dashboards'], + }, + () => { + test('Export for internal and external use', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'ZqZnVvFZz', + }); + + // Open the export drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsJson) + .click(); + + await expect(page).toHaveURL(/.*shareView=export.*/); + + // Export as JSON + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.container) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle) + ).toBeChecked({ + checked: false, + }); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.CodeEditor.container)).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.saveToFileButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton) + .click(); + // TODO failing in CI - fix it + // let clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); + // expect(clipboardContent).not.toContain('__inputs'); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle) + .click({ force: true }); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton) + .click(); + // TODO failing in CI - fix it + // clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); + // expect(clipboardContent).toContain('__inputs'); + + await dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton).click(); + + await expect(page).not.toHaveURL(/.*shareView=export.*/); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts new file mode 100644 index 00000000000..893afe13f09 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Dashboard keybindings', + { + tag: ['@dashboards'], + }, + () => { + // the "should collapse and expand all rows" test requires a larger viewport + // otherwise the final panel is not visible when everything is expanded + test.use({ + viewport: { width: 1280, height: 1080 }, + }); + + test('should collapse and expand all rows', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'Repeating-rows-uid/repeating-rows' }); + + const panelContents = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content); + await expect(panelContents).toHaveCount(5); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = A, pod = Bob')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = B, pod = Bob')) + ).toBeVisible(); + + // Collapse all rows using keyboard shortcut: d + Shift+C + await page.keyboard.press('d'); + await page.keyboard.press('Shift+C'); + + await expect(panelContents).toHaveCount(0); + await expect(page.getByText('server = A, pod = Bob')).toBeHidden(); + await expect(page.getByText('server = B, pod = Bob')).toBeHidden(); + + // Expand all rows using keyboard shortcut: d + Shift+E + await page.keyboard.press('d'); + await page.keyboard.press('Shift+E'); + + await expect(panelContents).toHaveCount(6); + await expect(page.getByText('server = A, pod = Bob')).toBeVisible(); + await expect(page.getByText('server = B, pod = Bob')).toBeVisible(); + }); + + test('should open panel inspect', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'edediimbjhdz4b/a-tall-dashboard' }); + + // Find Panel #1 and press 'i' to open inspector + const panel1 = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1')); + await expect(panel1).toBeVisible(); + await panel1.press('i'); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible(); + + // Press Escape to close inspector + await page.keyboard.press('Escape'); + + await expect(page.getByTestId(selectors.components.PanelInspector.Json.content)).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts new file mode 100644 index 00000000000..89a03671f5a --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json'; + +test.describe( + 'Dashboard with data links that have no slug', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test.beforeAll(async ({ request }) => { + // Import the test dashboard + const response = await request.post('/api/dashboards/import', { + data: { + dashboard: testDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); + const responseBody = await response.json(); + dashboardUID = responseBody.uid; + }); + + test.afterAll(async ({ request }) => { + // Clean up the imported dashboard + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + }); + + test('Should not reload if linking to same dashboard', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: dashboardUID }); + + const panel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Data links without slug') + ); + await expect(panel).toBeVisible(); + + const urlShouldContain = '/d/data-link-no-slug/data-link-without-slug-test'; + await dashboardPage + .getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink) + .getByText(/9yy21uzzxypg/) + .click(); + await expect(page.getByText(/Loading/)).toBeHidden(); + await expect(page).toHaveURL(new RegExp(urlShouldContain)); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink) + .getByText(/dr199bpvpcru/) + .click(); + await expect(page.getByText(/Loading/)).toBeHidden(); + await expect(page).toHaveURL(new RegExp(urlShouldContain)); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink) + .getByText(/dre33fzyxcrz/) + .click(); + await expect(page.getByText(/Loading/)).toBeHidden(); + await expect(page).toHaveURL(new RegExp(urlShouldContain)); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts new file mode 100644 index 00000000000..79ae94c5e96 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/DashboardLiveTest.json'; + +test.describe( + 'Dashboard Live streaming support', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test.beforeAll(async ({ request }) => { + // Import the test dashboard + const response = await request.post('/api/dashboards/import', { + data: { + dashboard: testDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); + const responseBody = await response.json(); + dashboardUID = responseBody.uid; + }); + + test.afterAll(async ({ request }) => { + // Clean up the imported dashboard + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + }); + + test('Should receive streaming data', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: dashboardUID }); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Live'))).toBeVisible(); + await expect + .poll( + async () => + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body) + .getByRole('row') + .count() + ) + .toBeGreaterThan(5); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts new file mode 100644 index 00000000000..3263b4f3011 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts @@ -0,0 +1,211 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, + }, +}); + +test.describe( + 'Public dashboards', + { + tag: ['@dashboards'], + }, + () => { + test('Create, open and disable a public dashboard', async ({ page, gotoDashboardPage, selectors, request }) => { + // Navigate to dashboard without template variables + let dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' }); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + // Create button should be disabled + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeDisabled(); + + // Create flow shouldn't show these elements + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton) + ).toBeHidden(); + + // Acknowledge checkboxes + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox) + .click({ force: true }); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox) + .click({ force: true }); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox) + .click({ force: true }); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeEnabled(); + + // Create public dashboard + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + .click(); + + // These elements shouldn't be rendered after creating public dashboard + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeHidden(); + + // These elements should be rendered + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + .click(); + + // These elements should be rendered once the Settings dropdown is opened + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch) + ).toBeVisible(); + + // Close the sharing modal + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + // Tag indicating a dashboard is public + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.publicDashboardTag) + ).toBeVisible(); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + .click(); + + // These elements should be rendered once the Settings dropdown is opened + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch) + ).toBeVisible(); + + // Make a request to public dashboards api endpoint without authentication + let copyUrlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput + ); + let url = await copyUrlInput.inputValue(); + let publicDashboardApiUrl = getPublicDashboardAPIUrl(url); + + // Create a new context without authentication + let response = await request.get(publicDashboardApiUrl); + expect(response.status()).toBe(200); + + // Close the sharing modal + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + // Navigate to dashboard without template variables + dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' }); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + // Save url before disabling public dashboard + copyUrlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput + ); + url = await copyUrlInput.inputValue(); + + // Switch off enabling toggle + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeEnabled(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + .click({ force: true }); + + // Url should be disabled + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeDisabled(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeDisabled(); + + // Make a request to public dashboards api endpoint without authentication + publicDashboardApiUrl = getPublicDashboardAPIUrl(url); + response = await request.get(publicDashboardApiUrl); + expect(response.status()).toBe(403); + }); + } +); + +const getPublicDashboardAPIUrl = (url: string): string => { + const accessToken = url.split('/').pop(); + return `/api/public/dashboards/${accessToken}`; +}; diff --git a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts new file mode 100644 index 00000000000..60b928c1301 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, + }, +}); + +test.describe( + 'Public dashboard with template variables', + { + tag: ['@dashboards'], + }, + () => { + test('Create a public dashboard with template variables shows a template variable warning', async ({ + gotoDashboardPage, + selectors, + }) => { + // Navigate to dashboard with template variables + const dashboardPage = await gotoDashboardPage({ + uid: 'HYaGDGIMk', + }); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + // Warning Alert dashboard cannot be made public because it has template variables + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert + ) + ).toBeVisible(); + + // Configuration elements for public dashboards should exist + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts new file mode 100644 index 00000000000..0d991c100e1 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -0,0 +1,191 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + }, +}); + +const DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8'; +const DASHBOARD_UID_2 = 'edediimbjhdz4b'; + +test.describe( + 'Shared dashboards', + { + tag: ['@dashboards'], + }, + () => { + test('Close share externally drawer', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + // Open share externally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally) + .click(); + + await expect(page).toHaveURL(/.*shareView=public_dashboard.*/); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.cancelButton) + .click(); + + await expect(page).not.toHaveURL(/.*shareView=public_dashboard.*/); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container) + ).toBeHidden(); + }); + + test('Create and disable a shared dashboard and check API', async ({ + page, + gotoDashboardPage, + selectors, + request, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID_2 }); + + // Open share externally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally) + .click(); + + // Create button should be disabled + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton + ) + ).toBeDisabled(); + + // Create flow shouldn't show these elements + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton + ) + ).toBeHidden(); + + // Acknowledge checkbox + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox + ) + ).toBeEnabled(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox) + .click({ force: true }); + + // Create shared dashboard + const createResponse = page.waitForResponse( + (response) => + response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards`) && + response.request().method() === 'POST' + ); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton + ) + ).toBeEnabled(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton) + .click(); + + let response = await createResponse; + let publicDashboard = await response.json(); + + // Test API access with the created dashboard + let apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`); + expect(apiResponse.status()).toBe(200); + + // These elements shouldn't be rendered after creating public dashboard + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton + ) + ).toBeHidden(); + + // These elements should be rendered + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton + ) + ).toBeVisible(); + + // Switch off enabling toggle + const updateResponse = page.waitForResponse( + (response) => + response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards/`) && + response.request().method() === 'PATCH' + ); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton + ) + ).toBeEnabled(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton) + .click({ force: true }); + + response = await updateResponse; + expect(response.status()).toBe(200); + + publicDashboard = await response.json(); + + // Test that API access is now forbidden + apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`); + expect(apiResponse.status()).toBe(403); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts new file mode 100644 index 00000000000..d250bb73464 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts @@ -0,0 +1,175 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + }, +}); + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.describe( + 'Share internally', + { + tag: ['@dashboards'], + }, + () => { + test.beforeEach(async ({ page }) => { + // Clear localStorage before each test + await page.evaluate(() => { + window.localStorage.removeItem('grafana.dashboard.link.shareConfiguration'); + }); + }); + + test('Create a locked time range short link', async ({ page, gotoDashboardPage, selectors }) => { + // Navigate to dashboard with specific time range + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }), + }); + + // Open share internally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + + const createResponse = page.waitForResponse( + (response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST' + ); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally) + .click(); + + await expect(page).toHaveURL(/.*shareView=link.*/); + + // Check that the required elements exist + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton) + ).toBeVisible(); + + // Check radio buttons + const radioButtons = dashboardPage.getByGrafanaSelector(selectors.components.RadioButton.container); + await expect(radioButtons).toHaveCount(3); + + // Check localStorage is initially null + const initialConfig = await page.evaluate(() => { + return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + }); + expect(initialConfig).toBeNull(); + + // Wait for the short URL creation + const response = await createResponse; + expect(response.status()).toBe(200); + + const responseBody = await response.json(); + expect(responseBody.url).toContain('goto'); + }); + + test('Create a relative time range short link', async ({ page, gotoDashboardPage, selectors }) => { + // Navigate to dashboard with specific time range + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }), + }); + + // Open share internally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally) + .click(); + + const updateResponse = page.waitForResponse( + (response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST' + ); + + // Toggle the lock time range switch + const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch + ); + await expect(lockTimeRangeSwitch).toBeInViewport(); + await expect(async () => { + await lockTimeRangeSwitch.uncheck({ force: true }); + }).toPass(); + + await expect(async () => { + // Check localStorage configuration + const shareConfig = await page.evaluate(() => { + const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + return config ? JSON.parse(config) : null; + }); + + expect(shareConfig).not.toBeNull(); + expect(shareConfig.useAbsoluteTimeRange).toBe(false); + expect(shareConfig.useShortUrl).toBe(true); + expect(shareConfig.theme).toBe('current'); + }).toPass(); + + // Wait for the API response + const response = await updateResponse; + expect(response.status()).toBe(200); + + const responseBody = await response.json(); + expect(responseBody.url).toContain('goto'); + }); + + test('Share button gets configured link', async ({ page, gotoDashboardPage, selectors }) => { + // Navigate to dashboard with specific time range + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }), + }); + + // Open share internally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally) + .click(); + + // Check localStorage is initially null + const initialConfig = await page.evaluate(() => { + return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + }); + expect(initialConfig).toBeNull(); + + // Configure the sharing options + const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch + ); + await expect(lockTimeRangeSwitch).toBeInViewport(); + await expect(async () => { + await lockTimeRangeSwitch.uncheck({ force: true }); + }).toPass(); + const shortenUrlSwitch = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch + ); + await expect(shortenUrlSwitch).toBeInViewport(); + await expect(async () => { + await shortenUrlSwitch.uncheck({ force: true }); + }).toPass(); + + // Close the drawer + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click(); + + await expect(page).not.toHaveURL(/.*shareView=link.*/); + + await expect(async () => { + // Check that localStorage has been updated with the configuration + const finalConfig = await page.evaluate(() => { + const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + return config ? JSON.parse(config) : null; + }); + + expect(finalConfig).not.toBeNull(); + expect(finalConfig.useAbsoluteTimeRange).toBe(false); + expect(finalConfig.useShortUrl).toBe(false); + expect(finalConfig.theme).toBe('current'); + }).toPass(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts new file mode 100644 index 00000000000..a3390122e92 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { SnapshotCreateResponse } from '../../public/app/features/dashboard/services/SnapshotSrv'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + }, +}); + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.describe( + 'Snapshots', + { + tag: ['@dashboards'], + }, + () => { + test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => { + // Opening a dashboard + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + const panelsToCheck = [ + 'Raw Data Graph', + 'Last non-null', + 'min', + 'Max', + 'The data from graph above with seriesToColumns transform', + ]; + + // Open the sharing drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareSnapshot) + .click(); + + // Publish snapshot + const createSnapshotPromise = page.waitForResponse( + (response) => response.url().includes('/api/snapshots') && response.request().method() === 'POST' + ); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot) + .click(); + + const createResponse = await createSnapshotPromise; + expect(createResponse.status()).toBe(200); + + const responseBody: SnapshotCreateResponse = await createResponse.json(); + + // Navigate to the snapshot URL + const snapshotUrl = getSnapshotUrl(responseBody.key); + await page.goto(snapshotUrl); + + // Validate the dashboard controls are rendered + await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible(); + + // Validate the panels are rendered + for (const title of panelsToCheck) { + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible(); + } + }); + } +); + +const getSnapshotUrl = (uid: string): string => { + return `/dashboard/snapshot/${uid}`; +}; diff --git a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts new file mode 100644 index 00000000000..b31fecf230f --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'HYaGDGIMk'; + +test.use({ + timezoneId: 'Pacific/Easter', +}); + +test.describe( + 'Dashboard templating', + { + tag: ['@dashboards'], + }, + () => { + test('Verify variable interpolation works', async ({ page, gotoDashboardPage }) => { + // Open dashboard global variables and interpolation + await gotoDashboardPage({ uid: DASHBOARD_UID }); + + // Get the actual timezone from the browser context (should be Pacific/Easter due to test.use) + const timeZone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone); + const example = `Example: from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`; + + const expectedItems: string[] = [ + '__dashboard = Templating - Global variables and interpolation', + '__dashboard.name = Templating - Global variables and interpolation', + '__dashboard.uid = HYaGDGIMk', + '__org.name = Main Org.', + '__org.id = 1', + '__user.id = 1', + '__user.login = admin', + '__user.email = admin@localhost', + `Server:raw = A'A"A,BB\\B,CCC`, + `Server:regex = (A'A"A|BB\\\\B|CCC)`, + `Server:lucene = ("A'A\\"A" OR "BB\\\\B" OR "CCC")`, + `Server:glob = {A'A"A,BB\\B,CCC}`, + `Server:pipe = A'A"A|BB\\B|CCC`, + `Server:distributed = A'A"A,Server=BB\\B,Server=CCC`, + `Server:csv = A'A"A,BB\\B,CCC`, + `Server:html = A'A"A, BB\\B, CCC`, + `Server:json = ["A'A\\"A","BB\\\\B","CCC"]`, + `Server:percentencode = %7BA%27A%22A%2CBB%5CB%2CCCC%7D`, + `Server:singlequote = 'A\\'A"A','BB\\B','CCC'`, + `Server:doublequote = "A'A\\"A","BB\\B","CCC"`, + `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, + `Server:date = NaN`, + `Server:text = All`, + `Server:queryparam = var-Server=$__all`, + `1 < 2`, + example, + ]; + + // Get all list items from the markdown content + const listItems = page.locator('.markdown-html li'); + + // Verify we have the expected number of items + await expect(listItems).toHaveCount(26); + + // Get all the text content from list items + const actualItems = await listItems.allTextContents(); + + // Compare each expected item with actual items + for (let i = 0; i < expectedItems.length; i++) { + expect(actualItems[i]).toBe(expectedItems[i]); + } + + // Check link interpolation is working correctly + const exampleLink = page.locator(`a:has-text("${example}")`); + await expect(exampleLink).toHaveAttribute( + 'href', + `https://example.com/?from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}` + ); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts new file mode 100644 index 00000000000..ac505aa9cd4 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts @@ -0,0 +1,97 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = '5SdHCasdf'; + +const USER = 'dashboard-timepicker-test'; +const PASSWORD = 'dashboard-timepicker-test'; + +// Separate user to isolate changes from other tests +test.use({ + user: { + user: USER, + password: PASSWORD, + }, + storageState: { + cookies: [], + origins: [], + }, +}); + +test.describe( + 'Dashboard timepicker', + { + tag: ['@dashboards'], + }, + () => { + test('Shows the correct calendar days with custom timezone set via preferences', async ({ + createUser, + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(USER); + await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Set user preferences for timezone + await page.goto('/profile'); + await page.getByTestId(selectors.components.TimeZonePicker.containerV2).click(); + await page.getByRole('option', { name: 'Asia/Tokyo' }).click(); + await page.getByTestId(selectors.components.UserProfile.preferencesSaveButton).click(); + + // Open dashboard with time range from 8th to end of 10th. + // Will be Tokyo time because of above preference + await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ + timezone: 'Default', + }), + }); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click(); + + // Assert that the calendar shows 08 and 09 and 10 as selected days + const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive'); + await expect(activeTiles).toHaveCount(3); + }); + + test('Shows the correct calendar days with custom timezone set via time picker', async ({ + createUser, + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(USER); + await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Open dashboard with time range from 2022-06-08 00:00:00 to 2022-06-10 23:59:59 in Tokyo time + await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ + timezone: 'Asia/Tokyo', + }), + }); + + // Open dashboard with time range from 8th to end of 10th. + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click(); + + // Assert that the calendar shows 08 and 09 and 10 as selected days + const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive'); + await expect(activeTiles).toHaveCount(3); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts new file mode 100644 index 00000000000..b6479d8007c --- /dev/null +++ b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Embedded dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('open test page', async ({ page, dashboardPage, selectors }) => { + await page.goto('/dashboards/embedding-test'); + + // Verify pie charts are rendered + const pieChartSlices = page.locator( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ); + await expect(pieChartSlices).toHaveCount(5); + + // Verify no url sync + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click(); + await page.locator('label').filter({ hasText: 'Last 1 hour' }).click(); + + // Verify URL remains the same (no sync) + expect(page.url()).toMatch(/\/dashboards\/embedding-test$/); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts new file mode 100644 index 00000000000..0a1978f3d69 --- /dev/null +++ b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +test.describe( + 'Dashboards', + { + tag: ['@dashboards'], + }, + () => { + test('should restore scroll position', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // Verify first panel is visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1')) + ).toBeVisible(); + + // Scroll to the bottom + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + + // The last panel should be visible... + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50')) + ).toBeVisible(); + + // Then we open and close the panel editor + // Click on panel menu (it only shows on hover) + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu('Panel #50')) + .click({ force: true }); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click(); + + // Go back to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // The last panel should still be visible! + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts new file mode 100644 index 00000000000..cd905ac56b8 --- /dev/null +++ b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/TestDashboard.json'; + +test.describe( + 'Import Dashboards Test', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test('Ensure you can import a number of json test dashboards from a specific test directory', async ({ + page, + dashboardPage, + selectors, + }) => { + await page.goto('/dashboard/import'); + + // Fill in the dashboard JSON and name + const textarea = dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.textarea); + await textarea.fill(JSON.stringify(testDashboard)); + + // Submit the JSON + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.submit).click(); + + // Fill in the dashboard name and submit + const nameField = dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.name); + await expect(nameField).toBeVisible(); + await nameField.click(); + await nameField.clear(); + await nameField.fill(testDashboard.title); + + await dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.submit).click(); + + // Wait for dashboard to load and extract UID from URL + await page.waitForURL('**/d/**'); + const url = page.url(); + const urlParts = url.split('/d/'); + if (urlParts.length > 1) { + dashboardUID = urlParts[1].split('/')[0]; + } + + // Verify the dashboard title is present in the breadcrumbs + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb(testDashboard.title)) + ).toBeVisible(); + + // Verify that specific panels from the test dashboard are loaded + await expect(page.getByText('Gauge Example')).toBeVisible(); + await expect(page.getByText('Stat')).toBeVisible(); + await expect(page.getByText('Time series example')).toBeVisible(); + }); + + test.afterEach(async ({ request }) => { + // Clean up the imported dashboard using API + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + dashboardUID = ''; + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts new file mode 100644 index 00000000000..ce1293acc2c --- /dev/null +++ b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts @@ -0,0 +1,300 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; + +test.describe( + 'Variables - Load options from Url', + { + tag: ['@dashboards'], + }, + () => { + test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // Test first variable (A) + const firstVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('input'); + await expect(firstVariableInput).toBeVisible(); + await firstVariableInput.click(); + + // Check dropdown has 10 options + const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(firstDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test second variable (AA) + const secondVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')) + .locator('input'); + + await expect(secondVariableInput).toBeVisible(); + await secondVariableInput.click(); + + // Check dropdown has 10 options + const secondDropdownOptions = dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .locator('..'); + await expect(secondDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test third variable ($__all) + const thirdVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')) + .locator('input'); + + await expect(thirdVariableInput).toBeVisible(); + await thirdVariableInput.click(); + + // Check dropdown has 10 options + const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(thirdDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC')) + ).toBeVisible(); + }); + + test('options set in url should load correct options', async ({ + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'B', + 'var-server': 'BB', + 'var-pod': 'BBB', + }), + }); + + // Test first variable (B) - should be selected from URL + const firstVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')) + .locator('input'); + + await expect(firstVariableInput).toBeVisible(); + await firstVariableInput.click(); + + // Check dropdown has 10 options + const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(firstDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test second variable (BB) - should be selected from URL + const secondVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')) + .locator('input'); + + await expect(secondVariableInput).toBeVisible(); + await secondVariableInput.click(); + + // Check dropdown has 10 options + const secondDropdownOptions = dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .locator('..'); + await expect(secondDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test third variable (BBB) - should be selected from URL + const thirdVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')) + .locator('input'); + + await expect(thirdVariableInput).toBeVisible(); + await thirdVariableInput.click(); + + // Check dropdown has 10 options + const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(thirdDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC')) + ).toBeVisible(); + }); + + test('options set in url that do not exist should load correct options', async ({ + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + // Handle uncaught exceptions that are expected + page.on('pageerror', (error) => { + if (error.message.includes("Couldn't find any field of type string in the results.")) { + // This error is expected and should not fail the test + return; + } + throw error; + }); + + await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'X', + }), + }); + + // Test first variable (X) - invalid value from URL + const firstVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X')) + .locator('input'); + + await expect(firstVariableInput).toBeVisible(); + await firstVariableInput.click(); + + // Check dropdown has 10 options + const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(firstDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible (should fall back to default options) + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click(); + + // Check that second variable shows $__all (should be 2 instances) + const allVariables = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + ); + + await expect(allVariables).toHaveCount(2); + await expect(allVariables.first()).toBeVisible(); + await expect(allVariables.last()).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts new file mode 100644 index 00000000000..99fa52b3a5c --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Constant', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new constant variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Constant" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + + // Select "Constant" type from dropdown + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Constant'); + await typeSelect.locator('input').press('Enter'); + + // Set variable name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + + // Set constant value + const constantInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2 + ); + await constantInput.fill('pesto'); + + // Set label + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click(); + + // Assert the variable was rendered in the markdown content + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: pesto'); + + // Assert the variable is not visible in the dashboard navigation (constant variables are hidden) + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test')) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts new file mode 100644 index 00000000000..30a16a7dae0 --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts @@ -0,0 +1,121 @@ +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +async function fillInCustomVariable( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + name: string, + label: string, + value: string +) { + // Select "Custom" type from dropdown + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Custom'); + await typeSelect.locator('input').press('Enter'); + + // Set variable name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill(name); + + // Set label + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill(label); + + // Set custom values + const customValueInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput + ); + await customValueInput.fill(value); + await customValueInput.blur(); +} + +async function assertPreviewValues( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectedValues: string[] +) { + for (let i = 0; i < expectedValues.length; i++) { + const previewOption = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption) + .nth(i); + await expect(previewOption).toHaveText(expectedValues[i]); + } +} + +test.describe( + 'Variables - Custom', + { + tag: ['@dashboards'], + }, + () => { + test('can add a custom template variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Custom" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + await fillInCustomVariable(dashboardPage, selectors, 'VariableUnderTest', 'Variable under test', 'one,two,three'); + await assertPreviewValues(dashboardPage, selectors, ['one', 'two', 'three']); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one')) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'two' }).click(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: two'); + }); + + test('can add a custom template variable with labels', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Custom" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + await fillInCustomVariable( + dashboardPage, + selectors, + 'VariableUnderTest', + 'Variable under test', + 'One : 1,Two : 2, Three : 3' + ); + await assertPreviewValues(dashboardPage, selectors, ['One', 'Two', 'Three']); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1')) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Two' }).click(); + + // Assert it was rendered (the value "2" should be displayed, not the label "Two") + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 2'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts new file mode 100644 index 00000000000..a769f8438cb --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Datasource', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new datasource variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Datasource" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Data source'); + await typeSelect.locator('input').press('Enter'); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + + // If this is failing, make sure there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" + // Or update to match available gdev datasources for testing + const datasourceSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect + ); + await datasourceSelect.locator('input').fill('Prometheus'); + await datasourceSelect.locator('input').press('Enter'); + + const previewOptions = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ); + await expect(previewOptions.first()).toContainText('gdev-prometheus'); + await expect(previewOptions.last()).toContainText('gdev-slow-prometheus'); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click(); + + // Change the selected variable value + await dashboardPage + .getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus') + ) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .filter({ hasText: 'gdev-slow-prometheus' }) + .click(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: gdev-slow-prometheus-uid'); + await expect(page.locator('.markdown-html').nth(1)).toContainText('VariableUnderTestText: gdev-slow-prometheus'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts new file mode 100644 index 00000000000..dbb670c739e --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts @@ -0,0 +1,84 @@ +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +async function assertPreviewValues( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectedValues: string[] +) { + for (let i = 0; i < expectedValues.length; i++) { + const previewOption = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption) + .nth(i); + await expect(previewOption).toHaveText(expectedValues[i]); + } +} + +test.describe( + 'Variables - Interval', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new interval variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Interval" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Interval'); + await typeSelect.locator('input').press('Enter'); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + + // Set interval values + const intervalInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput + ); + await intervalInput.fill('10s,10m,60m,90m,1h30m'); + await intervalInput.blur(); + await assertPreviewValues(dashboardPage, selectors, ['10s', '10m', '60m', '90m', '1h30m']); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click(); + + // Verify the variable label and initial value, then change the selected value + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test') + ); + + // Find the value element (next sibling) and verify it shows the default value + const variableValue = variableLabel.locator('+ div'); + await expect(variableValue).toHaveText('10s'); + + // Click to open the dropdown + await variableValue.click(); + + // Select '1h30m' from the dropdown + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: '1h30m' }).click(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 1h30m'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts new file mode 100644 index 00000000000..d289142a097 --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -0,0 +1,251 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; +const DASHBOARD_NAME = 'Templating - Nested Template Variables'; + +test.describe( + 'Variables - Query - Add variable', + { + tag: ['@dashboards'], + }, + () => { + test('query variable should be default and default fields should be correct', async ({ + page, + gotoDashboardPage, + selectors, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await expect(nameInput).toBeVisible(); + await expect(nameInput).toHaveAttribute('placeholder', 'Variable name'); + await expect(nameInput).toHaveValue('query0'); + + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await expect(typeSelect).toBeVisible(); + const singleValue = typeSelect.locator('div[class*="-singleValue"]'); + await expect(singleValue).toHaveText('Query'); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await expect(labelInput).toBeVisible(); + await expect(labelInput).toHaveAttribute('placeholder', 'Label name'); + await expect(labelInput).toHaveValue(''); + + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await expect(descriptionInput).toBeVisible(); + await expect(descriptionInput).toHaveAttribute('placeholder', 'Descriptive text'); + await expect(descriptionInput).toHaveValue(''); + + await expect(page.locator('label').filter({ hasText: 'Hide' })).toBeVisible(); + + // Check datasource selector + const datasourceSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect + ); + await expect(datasourceSelect).toBeVisible(); + await expect(datasourceSelect).toHaveAttribute('placeholder', 'gdev-testdata'); + + await expect(page.locator('label').filter({ hasText: 'Refresh' })).toBeVisible(); + await expect(page.locator('label').filter({ hasText: 'On dashboard load' })).toBeVisible(); + + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await expect(regexInput).toBeVisible(); + await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?.*)-(?.*)-.*/'); + await expect(regexInput).toHaveValue(''); + + const sortSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 + ); + await expect(sortSelect).toBeVisible(); + const sortSingleValue = sortSelect.locator('div[class*="-singleValue"]'); + await expect(sortSingleValue).toHaveText('Disabled'); + + // Check Multi-value checkbox + const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' }); + const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]'); + await expect(multiValueCheckbox).toBeChecked({ + checked: false, + }); + + // Check Include All option checkbox + const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' }); + const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]'); + await expect(includeAllCheckbox).toBeChecked({ + checked: false, + }); + + // Check preview and custom all input don't exist initially + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput + ) + ).toBeHidden(); + }); + + test('adding a single value query variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Click the "New" button to add a variable + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('a label'); + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await descriptionInput.fill('a description'); + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill('gdev-testdata'); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-testdata').click(); + const queryInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + await queryInput.fill('*'); + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await regexInput.fill('/.*C.*/'); + await regexInput.blur(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toBeVisible(); + + // Navigate back to dashboard + const submitButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton + ); + await submitButton.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Check variable appears in submenu + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label')) + ).toBeVisible(); + + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems).toHaveCount(4); + const fourthItem = submenuItems.nth(3); + const input = fourthItem.locator('input'); + await input.click(); + + // Check dropdown has 1 option containing 'C' + const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(options).toHaveCount(1); + await expect(options).toContainText('C'); + }); + + test('adding a multi value query variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('a label'); + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await descriptionInput.fill('a description'); + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill('gdev-testdata'); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-testdata').click(); + const queryInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + await queryInput.fill('*'); + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await regexInput.fill('/.*C.*/'); + + // Enable Multi-value + const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' }); + const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]'); + await multiValueCheckbox.click({ force: true }); + await expect(multiValueCheckbox).toBeChecked(); + + // Enable Include All option + const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' }); + const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]'); + await includeAllCheckbox.click({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + const customAllInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput + ); + await expect(customAllInput).toHaveValue(''); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toHaveCount(2); + + // Navigate back to dashboard + const submitButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton + ); + await submitButton.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Check variable appears in submenu + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label')) + ).toBeVisible(); + + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems).toHaveCount(4); + + const fourthItem = submenuItems.nth(3); + const input = fourthItem.locator('input'); + await input.click(); + + // Check dropdown has 3 options (All + filtered results) + const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(options).toHaveCount(3); + + // Check toggle shows Selected (1) + const toggleAll = dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions); + await expect(toggleAll).toHaveText('Selected (1)'); + + // Check options contain 'All' and 'C' + await expect(options.filter({ hasText: 'All' })).toBeVisible(); + // need to use regex here else this will also match "Selected" + await expect(options.filter({ hasText: /^C$/ })).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts new file mode 100644 index 00000000000..4f5a856c5de --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Text box', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new text box variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "text box" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Textbox'); + await typeSelect.locator('input').press('Enter'); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + const textBoxInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2 + ); + await textBoxInput.fill('cat-dog'); + + // Navigate back to the dashboard and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + + const textInput = submenuItem.locator('input'); + await textInput.fill('dog-cat'); + await textInput.blur(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: dog-cat'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts new file mode 100644 index 00000000000..22aacc41c88 --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally'; + +test.describe( + 'Repeating a panel horizontally', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat a panel horizontally', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + // Verify all panels are initially visible + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + } + + // Change to only show panels 1 + 3 + const horizontalVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('horizontal')) + .locator('..') + .locator('input'); + await horizontalVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown panels + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-horizontal=1&var-horizontal=3` }); + + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + // Check correct panels are displayed with proper positioning + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts new file mode 100644 index 00000000000..0e6755c05e5 --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically'; + +test.describe( + 'Repeating a panel vertically', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat a panel vertically', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + // Verify all panels are initially visible + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + } + + // Change to only show panels 1 + 3 + const verticalVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('vertical')) + .locator('..') + .locator('input'); + await verticalVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown panels + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-vertical=1&var-vertical=3` }); + + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + // Check correct panels are displayed with proper positioning + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts new file mode 100644 index 00000000000..c326f55552d --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row'; + +test.describe( + 'Repeating empty rows', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat empty rows vertically', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevTop = Number.NEGATIVE_INFINITY; + const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3']; + + for (const title of rowTitles) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3']; + + // Verify all rows are initially visible + for (const title of rowTitles) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + } + + // Change to only show rows 1 + 3 + const rowVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('row')) + .locator('..') + .locator('input'); + await rowVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown rows + let prevTop = Number.NEGATIVE_INFINITY; + const rowsShown = ['Row title 1', 'Row title 3']; + const rowsNotShown = ['Row title 2']; + + for (const title of rowsShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + + for (const title of rowsNotShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-row=1&var-row=3` }); + + let prevTop = Number.NEGATIVE_INFINITY; + const rowsShown = ['Row title 1', 'Row title 3']; + const rowsNotShown = ['Row title 2']; + + // Check correct rows are displayed with proper positioning + for (const title of rowsShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + + for (const title of rowsNotShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts new file mode 100644 index 00000000000..ccf6056d695 --- /dev/null +++ b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts @@ -0,0 +1,280 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; + +test.describe( + 'Variables - Set options from ui', + { + tag: ['@dashboards'], + }, + () => { + test('clicking a value that is not part of dependents options should change these to All', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'A', + 'var-server': 'AA', + 'var-pod': 'AAA', + }), + }); + + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('..') + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + ) + ).toBeVisible(); + + const serverVariableAllOptions = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + ); + await expect(serverVariableAllOptions).toHaveCount(2); + + const firstServerDropdown = serverVariableAllOptions.nth(0); + await expect(firstServerDropdown).toBeVisible(); + await firstServerDropdown.locator('input').click(); + + const selectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(selectOptions).toHaveCount(10); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')) + ).toBeVisible(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('pod')) + .locator('..') + .locator('input'); + await podVariable.click(); + + // length is 11 because of virtualized select options + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(podSelectOptions).toHaveCount(11); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAC')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAD')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAE')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAF')) + ).toBeVisible(); + }); + + test('adding a value that is not part of dependents options should add the new values dependant options', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'A', + 'var-server': 'AA', + 'var-pod': 'AAA', + }), + }); + + // Click on datacenter variable dropdown + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (2)' + ); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Verify datacenter shows "A,B" + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') + ) + ).toBeVisible(); + + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')) + .locator('input'); + await expect(serverVariable).toBeVisible(); + await serverVariable.click(); + + const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(serverSelectOptions).toHaveCount(11); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD')) + ).toBeVisible(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Click on pod variable dropdown + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA')) + .locator('input'); + await expect(podVariable).toBeVisible(); + await podVariable.click(); + + // Verify pod dropdown has 10 options + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(podSelectOptions).toHaveCount(10); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC')) + ).toBeVisible(); + }); + + test('removing a value that is part of dependents options should remove the new values dependant options', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + // can't use queryParams here because it won't work with the multiple values for each variable + uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`, + }); + + // Click on datacenter variable dropdown + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B')) + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + .click(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + ) + ).toBeVisible(); + + // Click on server variable dropdown + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')) + .locator('input'); + await expect(serverVariable).toBeVisible(); + await serverVariable.click(); + + const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(serverSelectOptions).toHaveCount(10); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')) + ).toBeVisible(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Click on pod variable dropdown + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')) + .locator('input'); + await expect(podVariable).toBeVisible(); + await podVariable.click(); + + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(podSelectOptions).toHaveCount(10); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts new file mode 100644 index 00000000000..a2ae416d84b --- /dev/null +++ b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, // Use legacy sharing component for this test + }, +}); + +test.describe( + 'Snapshots', + { + tag: ['@dashboards'], + }, + () => { + test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const panelsToCheck = [ + 'Raw Data Graph', + 'Last non-null', + 'min', + 'Max', + 'The data from graph above with seriesToColumns transform', + ]; + + // Open the sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select the snapshot tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Snapshot')).click(); + + // Publish snapshot + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.PublishSnapshot) + .click(); + + // Copy link button should be visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlButton) + ).toBeVisible(); + + // Get the snapshot URL from the input field + const urlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlInput + ); + const snapshotUrl = await urlInput.inputValue(); + + // Extract the snapshot key from the URL and navigate to the snapshot + const snapshotKey = getSnapshotKey(snapshotUrl); + await page.goto(`/dashboard/snapshot/${snapshotKey}`); + + // Validate the dashboard controls are rendered + await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible(); + + // Validate the panels are rendered + for (const title of panelsToCheck) { + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible(); + } + }); + } +); + +const getSnapshotKey = (url: string): string => { + return url.split('/').pop() || ''; +}; diff --git a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts new file mode 100644 index 00000000000..54bfdb771ba --- /dev/null +++ b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'yBCC3aKGk'; + +test.describe( + 'Templating dashboard links and variables', + { + tag: ['@dashboards'], + }, + () => { + test('Tests dashboard links and variables in links', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + const verifyLinks = async (variableValue: string) => { + const dashboardLinks = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.link); + + let linkCount = 0; + await expect + .poll(async () => { + linkCount = await dashboardLinks.count(); + return linkCount; + }) + .toBeGreaterThan(13); + + for (let i = 0; i < linkCount; i++) { + const href = await dashboardLinks.nth(i).getAttribute('href'); + expect(href).toContain(variableValue); + } + }; + + // Click on dashboard links dropdown + const dashboardLinksDropdown = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.dropDown); + await expect(dashboardLinksDropdown).toBeVisible(); + await dashboardLinksDropdown.click(); + + // Verify links contain default variable value + await verifyLinks('var-custom=$__all'); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Change variable value from $__all to p2 + const customVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')) + .locator('input'); + await expect(customVariable).toBeVisible(); + await customVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2')) + .click(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Open dashboard links dropdown again + await expect(dashboardLinksDropdown).toBeVisible(); + await dashboardLinksDropdown.click(); + + // Verify all links now contain the p2 value + await verifyLinks('p2'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts new file mode 100644 index 00000000000..20815446223 --- /dev/null +++ b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts @@ -0,0 +1,56 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'AejrN1AMz'; + +test.describe( + 'TextBox - load options scenarios', + { + tag: ['@dashboards'], + }, + () => { + test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await validateTextboxAndMarkup(page, dashboardPage, selectors, 'default value'); + }); + + test('loading variable from url should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + 'var-text': 'not default value', + }), + }); + + await validateTextboxAndMarkup(page, dashboardPage, selectors, 'not default value'); + }); + } +); + +// Helper function to validate textbox and markup +async function validateTextboxAndMarkup( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + value: string +) { + const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItem).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('text')) + ).toBeVisible(); + + const textInput = submenuItem.locator('input'); + await expect(textInput).toBeVisible(); + await expect(textInput).toHaveValue(value); + + const textPanel = page.locator(selectors.components.Panels.Visualization.Text.container('')); + await expect(textPanel).toBeVisible(); + + const headerElement = textPanel.locator('h1'); + await expect(headerElement).toBeVisible(); + await expect(headerElement).toHaveText(`variable: ${value}`); +} diff --git a/e2e-playwright/dashboards-suite/utils/makeDashboard.ts b/e2e-playwright/dashboards-suite/utils/makeDashboard.ts new file mode 100644 index 00000000000..2ada48db7a9 --- /dev/null +++ b/e2e-playwright/dashboards-suite/utils/makeDashboard.ts @@ -0,0 +1,58 @@ +export function makeNewDashboardRequestBody(dashboardName: string, folderUid?: string) { + return { + dashboard: { + annotations: { + list: [ + { + builtIn: 1, + datasource: { type: 'grafana', uid: '-- Grafana --' }, + enable: true, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + editable: true, + fiscalYearStartMonth: 0, + graphTooltip: 0, + links: [], + liveNow: false, + panels: [ + { + datasource: { type: 'testdata', uid: '89_jzlT4k' }, + gridPos: { h: 9, w: 12, x: 0, y: 0 }, + id: 2, + options: { + code: { + language: 'plaintext', + showLineNumbers: false, + showMiniMap: false, + }, + content: '***A nice little happy empty dashboard***', + mode: 'markdown', + }, + pluginVersion: '9.4.0-pre', + title: 'Nothing to see here', + type: 'text', + }, + ], + refresh: '', + revision: 1, + schemaVersion: 38, + tags: [], + templating: { list: [] }, + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + title: dashboardName, + version: 0, + uid: '', + weekStart: '', + }, + message: '', + overwrite: false, + folderUid, + } as const; +} diff --git a/e2e-playwright/dashboards/DashboardLiveTest.json b/e2e-playwright/dashboards/DashboardLiveTest.json new file mode 100644 index 00000000000..7d98377df89 --- /dev/null +++ b/e2e-playwright/dashboards/DashboardLiveTest.json @@ -0,0 +1,107 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.0-pre", + "targets": [ + { + "channel": "plugin/testdata/random-20Hz-stream", + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "measurements", + "refId": "A" + } + ], + "title": "Live", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "New dashboard", + "version": 0, + "uid": "live-e2e-test", + "weekStart": "" +} diff --git a/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json b/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json new file mode 100644 index 00000000000..08b2e3fda11 --- /dev/null +++ b/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json @@ -0,0 +1,255 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "links": [ + { + "title": "", + "url": "/d/${__dashboard.uid}?var-instance=${__data.fields.test1}&${__url_time_range}" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "alias": "test1", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv" + } + ], + "title": "Data links without slug", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "counters_logins{geohash=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + "name": "instance", + "options": [ + { + "selected": true, + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + { + "selected": false, + "text": "9yy21uzzxypg", + "value": "9yy21uzzxypg" + }, + { + "selected": false, + "text": "dr199bpvpcru", + "value": "dr199bpvpcru" + }, + { + "selected": false, + "text": "dre33fzyxcrz", + "value": "dre33fzyxcrz" + }, + { + "selected": false, + "text": "gc6j7crvrcpf", + "value": "gc6j7crvrcpf" + }, + { + "selected": false, + "text": "u6g9zuxvxypv", + "value": "u6g9zuxvxypv" + } + ], + "query": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv", + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "Data Link without slug test", + "uid": "data-link-no-slug", + "version": 3 +} diff --git a/e2e-playwright/dashboards/PanelSandboxDashboard.json b/e2e-playwright/dashboards/PanelSandboxDashboard.json new file mode 100644 index 00000000000..50fb2ad7f57 --- /dev/null +++ b/e2e-playwright/dashboards/PanelSandboxDashboard.json @@ -0,0 +1,57 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Sandbox Panel test", + "type": "sandbox-test-panel" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Sandbox Panel Test", + "uid": "c46b2460-16b7-42a5-82d1-b07fbf431950", + "version": 1, + "weekStart": "" +} diff --git a/e2e-playwright/dashboards/TestDashboard.json b/e2e-playwright/dashboards/TestDashboard.json new file mode 100644 index 00000000000..8f58d09c697 --- /dev/null +++ b/e2e-playwright/dashboards/TestDashboard.json @@ -0,0 +1,208 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "8.3.0-pre", + "title": "Gauge Example", + "type": "gauge" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0-pre", + "title": "Stat", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "title": "Time series example", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 31, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2021-09-01T04:00:00.000Z", + "to": "2021-09-15T04:00:00.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "E2E Test - Import Dashboard", + "uid": "kquZN5H7k", + "version": 4 +} diff --git a/e2e-playwright/dashboards/TestV2Dashboard.json b/e2e-playwright/dashboards/TestV2Dashboard.json new file mode 100644 index 00000000000..da89762c3a6 --- /dev/null +++ b/e2e-playwright/dashboards/TestV2Dashboard.json @@ -0,0 +1,424 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "admjzp8", + "namespace": "default", + "uid": "hrbekBWXeM7nC7GtouIbcngFyuqt8Xx7KlE4AnTwV7AX", + "resourceVersion": "1", + "generation": 1, + "creationTimestamp": "2025-05-16T09:41:56Z", + "labels": { + "grafana.app/deprecatedInternalID": "182" + }, + "annotations": { + "grafana.app/createdBy": "user:cejvsh18uudxcf", + "grafana.app/updatedBy": "user:cejvsh18uudxcf", + "grafana.app/updatedTimestamp": "2025-05-16T09:41:56Z", + "grafana.app/folder": "" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts" + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 8 + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "New Test V2 Dashboard", + "variables": [] + }, + "status": {} +} diff --git a/e2e-playwright/fixtures/exemplars-query-response.json b/e2e-playwright/fixtures/exemplars-query-response.json new file mode 100644 index 00000000000..6c26a9fbf7d --- /dev/null +++ b/e2e-playwright/fixtures/exemplars-query-response.json @@ -0,0 +1,323 @@ +{ + "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/e2e-playwright/fixtures/long-trace-response.json b/e2e-playwright/fixtures/long-trace-response.json new file mode 100644 index 00000000000..80955535638 --- /dev/null +++ b/e2e-playwright/fixtures/long-trace-response.json @@ -0,0 +1,7592 @@ +{ + "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/e2e-playwright/fixtures/tempo-response.json b/e2e-playwright/fixtures/tempo-response.json new file mode 100644 index 00000000000..30136a226a5 --- /dev/null +++ b/e2e-playwright/fixtures/tempo-response.json @@ -0,0 +1,1181 @@ +{ + "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/e2e-playwright/panels-suite/dashlist.spec.ts b/e2e-playwright/panels-suite/dashlist.spec.ts new file mode 100644 index 00000000000..db4a8cccc9d --- /dev/null +++ b/e2e-playwright/panels-suite/dashlist.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables'; + +test.describe( + 'Panels test: DashList panel', + { + tag: '@panels', + }, + () => { + // this is to prevent the fix for https://github.com/grafana/grafana/issues/76800 from regressing + test('should pass current variable values correctly when `Include current template variable values` is set', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // check the initial value of the urls contain the variable value correctly + const panel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Include time range and variables enabled') + ); + await expect(panel).toBeVisible(); + + const links = panel.locator('a'); + const linkCount = await links.count(); + for (let i = 0; i < linkCount; i++) { + const href = await links.nth(i).getAttribute('href'); + expect(href).toContain('var-server=A'); + } + + // update variable to b + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('server')) + .locator('..') + .locator('input'); + await serverVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + // blur the dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // check the urls are updated with the new variable value + await expect(panel).toBeVisible(); + const updatedLinks = panel.locator('a'); + const updatedLinkCount = await updatedLinks.count(); + for (let i = 0; i < updatedLinkCount; i++) { + const href = await updatedLinks.nth(i).getAttribute('href'); + expect(href).toContain('var-server=B'); + } + }); + } +); diff --git a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts new file mode 100644 index 00000000000..4cdb7afb081 --- /dev/null +++ b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; + +const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; + +test.beforeAll(async ({ request }) => { + // Import the dashboard + await request.post('/api/dashboards/import', { + data: { + dashboard: panelSandboxDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); +}); + +test.describe( + 'Panels test: Panel sandbox', + { + tag: ['@panels'], + }, + () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + await page.reload(); + }); + + test('Add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button adds iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeVisible(); + } + }); + + test('Reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeVisible(); + }); + + test('Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); + await expect(input).toHaveValue(''); + + await input.fill('x'); + await expect(input).toHaveValue('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeVisible(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + await page.reload(); + }); + + test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button adds 3 iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeHidden(); + } + }); + + test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + }); + + test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); + + await input.fill('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); + }); + + test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + await page.locator('[data-testid="button-test-globals"]').click(); + await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); + }); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-layer-types.spec.ts b/e2e-playwright/panels-suite/geomap-layer-types.spec.ts new file mode 100644 index 00000000000..ba0d80d002e --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +const MAP_LAYERS_TYPE = 'Map layers Layer type'; +const MAP_LAYERS_DATA = 'Map layers Data'; +const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL'; + +test.describe( + 'Panels test: Geomap layer types', + { + tag: ['@panels'], + }, + () => { + test('Tests changing the layer type', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toBeVisible(); + const field = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE) + ); + await expect(field).toBeVisible(); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('markers'); + + // Heatmap + const input = field.locator('input'); + await input.fill('Heatmap'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('heatmap'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + + // GeoJSON + await input.fill('GeoJSON'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('geojson'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON)) + ).toBeVisible(); + + // Open Street Map + await input.fill('Open Street Map'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('osm-standard'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON)) + ).toBeHidden(); + + // CARTO basemap + await input.fill('CARTO basemap'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('carto'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme')) + ).toBeVisible(); + + // ArcGIS MapServer + await input.fill('ArcGIS MapServer'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('esri-xyz'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance') + ) + ).toBeVisible(); + + // XYZ Tile layer + await input.fill('XYZ Tile layer'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('xyz'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution') + ) + ).toBeVisible(); + + // Night / Day (Alpha) + await input.fill('Night / Day'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('dayNight'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun') + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Route (Alpha) + await input.fill('Route'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('route'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location Mode') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-map-controls.spec.ts b/e2e-playwright/panels-suite/geomap-map-controls.spec.ts new file mode 100644 index 00000000000..de415dc8301 --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +test.describe( + 'Panels test: Geomap layer controls options', + { + tag: ['@panels'], + }, + () => { + test('Tests map controls options', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + // Wait until the query editor has been loaded by ensuring that the QueryEditor select contains the text 'flight_info_by_state.csv' + await expect( + page.locator(selectors.components.Select.singleValue('')).getByText('flight_info_by_state.csv') + ).toBeVisible(); + + const mapControlsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('Map controls') + ); + await expect(mapControlsGroup).toBeVisible(); + + // Show zoom field + const showZoomField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showZoomField); + await expect(showZoomField).toBeVisible(); + const zoomCheckbox = showZoomField.locator('input[type="checkbox"]'); + await zoomCheckbox.check({ force: true }); + await expect(zoomCheckbox).toBeChecked(); + + // Show attribution + const showAttributionField = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.showAttributionField + ); + await expect(showAttributionField).toBeVisible(); + const attributionCheckbox = showAttributionField.locator('input[type="checkbox"]'); + await attributionCheckbox.check({ force: true }); + await expect(attributionCheckbox).toBeChecked(); + + // Show scale + const showScaleField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showScaleField); + await expect(showScaleField).toBeVisible(); + const scaleCheckbox = showScaleField.locator('input[type="checkbox"]'); + await scaleCheckbox.check({ force: true }); + await expect(scaleCheckbox).toBeChecked(); + + // Show measure tool + const showMeasureField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showMeasureField); + await expect(showMeasureField).toBeVisible(); + const measureCheckbox = showMeasureField.locator('input[type="checkbox"]'); + await measureCheckbox.check({ force: true }); + await expect(measureCheckbox).toBeChecked(); + + // Show debug + const showDebugField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showDebugField); + await expect(showDebugField).toBeVisible(); + const debugCheckbox = showDebugField.locator('input[type="checkbox"]'); + await debugCheckbox.check({ force: true }); + await expect(debugCheckbox).toBeChecked(); + + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content); + await expect(panelContent).toBeVisible(); + + // Verify zoom + await expect(panelContent.locator('.ol-zoom')).toBeVisible(); + + // Verify attribution + await expect(panelContent.locator('.ol-attribution')).toBeVisible(); + + // Verify scale + await expect(panelContent.locator('.ol-scale-line')).toBeVisible(); + + // Verify measure tool + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.measureButton)).toBeVisible(); + + // Verify debug tool + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DebugOverlay.wrapper)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts new file mode 100644 index 00000000000..10df0887a0f --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -0,0 +1,194 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +test.describe( + 'Panels test: Geomap spatial operations', + { + tag: ['@panels'], + }, + () => { + test('Tests location auto option', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Auto'); + await locationInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Point')).toBeVisible(); + }); + + test('Tests location coords option', async ({ gotoDashboardPage, dashboardPage, selectors }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Coords'); + await locationInput.press('Enter'); + + const latitudeField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel + ); + await expect(latitudeField).toBeVisible(); + const latitudeInput = latitudeField.locator('input'); + await latitudeInput.fill('Lat'); + await latitudeInput.press('Enter'); + + const longitudeField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel + ); + await expect(longitudeField).toBeVisible(); + const longitudeInput = longitudeField.locator('input'); + await longitudeInput.fill('Lng'); + await longitudeInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Point')).toBeVisible(); + }); + + test('Tests geoshash field column appears in table view', async ({ + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Geohash'); + await locationInput.press('Enter'); + + const geohashField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.geohash.geohashFieldLabel + ); + await expect(geohashField).toBeVisible(); + const geohashFieldInput = geohashField.locator('input'); + await geohashFieldInput.fill('State'); + await geohashFieldInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('State 1')).toBeVisible(); + }); + + test('Tests location lookup option', async ({ gotoDashboardPage, dashboardPage, selectors }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Lookup'); + await locationInput.press('Enter'); + + const lookupField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel + ); + await expect(lookupField).toBeVisible(); + const lookupFieldInput = lookupField.locator('input'); + await lookupFieldInput.fill('State'); + await lookupFieldInput.press('Enter'); + + const gazetteerField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel + ); + await expect(gazetteerField).toBeVisible(); + const gazetteerFieldInput = gazetteerField.locator('input'); + await gazetteerFieldInput.fill('USA States'); + await gazetteerFieldInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Geometry')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/panelEdit_base.spec.ts b/e2e-playwright/panels-suite/panelEdit_base.spec.ts new file mode 100644 index 00000000000..66584d0387b --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_base.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PANEL_UNDER_TEST = 'Lines 500 data points'; + +test.describe( + 'Panels test: Panel edit base', + { + tag: ['@panels'], + }, + () => { + test('Tests various Panel edit scenarios', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'TkZXxlNG3' }); + + const panelTitle = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await expect(panelTitle).toBeVisible(); + + // Check that the panel is visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0); + await expect(panelTitle.locator('[data-testid="uplot-main-div"]').first()).toBeVisible(); + + // Open panel menu and click edit + await panelTitle.hover(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click(); + + // New panel editor opens when navigating from Panel menu + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Queries tab is rendered and open by default + const dataPane = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content); + await expect(dataPane).toBeVisible(); + + // Check that Queries tab is visible and active + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries'))).toBeVisible(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Queries1'); // there's already a query so therefore Query + 1 + + // Check query content is visible and other tabs are not + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden(); + + // Can change to Transform tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Transformations0'); // no transforms so Transform + 0 + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton) + ).toBeVisible(); + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden(); + + // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Alert')).click(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Alert0'); // no alert so Alert + 0 + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden(); + + // Go back to Queries tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries')).click(); + + // Check that Time series is chosen + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Time series' + ); + + // Check that table view works + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('A-series')).toBeVisible(); + + // Change to Text panel + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Text')).click(); + // Check current visualization shows Text + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Text' + ); + + // Data pane should not be rendered + await expect(dataPane).toBeHidden(); + + // Change to Table panel + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Table')).click(); + // Check current visualization shows Table + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Table' + ); + + // Data pane should be rendered + await expect(dataPane).toBeVisible(); + + // Field & Overrides tabs (need to switch to React based vis, i.e. Table) + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Column width') + ) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/panelEdit_queries.spec.ts b/e2e-playwright/panels-suite/panelEdit_queries.spec.ts new file mode 100644 index 00000000000..e60a7cc32b3 --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @@ -0,0 +1,118 @@ +import { Locator, Page } from 'playwright-core'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +test.describe( + 'Panels test: Queries', + { + tag: ['@panels'], + }, + () => { + test('Tests various Panel edit queries scenarios', async ({ selectors, gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: '5SdHCadmz', + queryParams: new URLSearchParams({ editPanel: '3' }), + }); + + // New panel editor opens when navigating from Panel menu + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Queries tab is rendered and open by default + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content)).toBeVisible(); + + // We expect row with refId A to exist and be visible + const initialRows = dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows); + await expect(initialRows).toHaveCount(1); + + // Add query button should be visible and clicking on it should create a new row + await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.addQuery).click(); + + // We expect row with refId A and B to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2); + + // Remove refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Remove query')) + .first() + .click(); + + // We expect row with refId B to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(1); + + // Duplicate refId B + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Duplicate query')) + .first() + .click(); + + // We expect row with refId B and A to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2); + + // Change to CSV Metric Values scenario for A + const scenarioSelect = dashboardPage + .getByGrafanaSelector(selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer) + .first(); + await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').first().click(); + await page.getByText('CSV Metric Values').first().click(); + + // Verify both queries are present in inspector + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + const resultIds = new Set([ + keyTexts[length - 2], // last 2 + keyTexts[length - 1], // last 2 + ]); + + expect(resultIds.has('A:')).toBe(true); + expect(resultIds.has('B:')).toBe(true); + }); + + // Hide response for row with refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response')) + .nth(1) + .click(); + + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + expect(keyTexts[length - 1]).toBe('B:'); + }); + + // Show response for row with refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response')) + .nth(1) + .click(); + + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + const resultIds = new Set([ + keyTexts[length - 2], // last 2 + keyTexts[length - 1], // last 2 + ]); + + expect(resultIds.has('A:')).toBe(true); + expect(resultIds.has('B:')).toBe(true); + }); + }); + } +); + +const expectInspectorResultAndClose = async ( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectCallback: (keys: Locator) => Promise +) => { + await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.queryInspectorButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.refreshButton).click(); + + const keys = page.locator(selectors.components.PanelInspector.Query.jsonObjectKeys('')); + await expect(keys.first()).toBeVisible(); + await expectCallback(keys); + + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click(); +}; diff --git a/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts b/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts new file mode 100644 index 00000000000..ed38aaedcbb --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Panels test: Transformations', + { + tag: ['@panels'], + }, + () => { + test('Tests transformations editor', async ({ selectors, gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'TkZXxlNG3', + queryParams: new URLSearchParams({ editPanel: '47' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.calculationsLabel) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.modeLabel)).toBeVisible(); + }); + + test('Tests case where transformations can be disabled and not clear out panel data', async ({ + selectors, + gotoDashboardPage, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'TkZXxlNG3', + queryParams: new URLSearchParams({ editPanel: '47' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.disableTransformationButton).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts b/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts new file mode 100644 index 00000000000..b88dbbaa196 --- /dev/null +++ b/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' }); + + await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts b/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts new file mode 100644 index 00000000000..2932175da33 --- /dev/null +++ b/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'stackdriver' }); + + await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible(); + await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts b/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts new file mode 100644 index 00000000000..c0f181560f0 --- /dev/null +++ b/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'cloudwatch' }); + + await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts b/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts new file mode 100644 index 00000000000..41badfb29e3 --- /dev/null +++ b/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'elasticsearch' }); + + await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts b/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts new file mode 100644 index 00000000000..a1c1a1f6fbd --- /dev/null +++ b/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'graphite' }); + + await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts b/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts new file mode 100644 index 00000000000..923106ebf0e --- /dev/null +++ b/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'influxdb' }); + + await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts b/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts new file mode 100644 index 00000000000..17373dcdce8 --- /dev/null +++ b/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'jaeger' }); + + await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/loki/loki.spec.ts b/e2e-playwright/plugin-e2e/loki/loki.spec.ts similarity index 53% rename from e2e/plugin-e2e/loki/loki.spec.ts rename to e2e-playwright/plugin-e2e/loki/loki.spec.ts index 25c459140cc..4e7af47b071 100644 --- a/e2e/plugin-e2e/loki/loki.spec.ts +++ b/e2e-playwright/plugin-e2e/loki/loki.spec.ts @@ -3,6 +3,6 @@ import { test, expect } from '@grafana/plugin-e2e'; test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { await createDataSourceConfigPage({ type: 'loki' }); - await expect(await page.getByText('Type: Loki', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + await expect(page.getByText('Type: Loki', { exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); }); diff --git a/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts b/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts new file mode 100644 index 00000000000..7590562c548 --- /dev/null +++ b/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'mssql' }); + + await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/mysql/mocks/mysql.mocks.ts b/e2e-playwright/plugin-e2e/mysql/mocks/mysql.mocks.ts similarity index 100% rename from e2e/plugin-e2e/mysql/mocks/mysql.mocks.ts rename to e2e-playwright/plugin-e2e/mysql/mocks/mysql.mocks.ts diff --git a/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts b/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts new file mode 100644 index 00000000000..9bd0f85b7de --- /dev/null +++ b/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks'; +import { mockDataSourceRequest } from './utils'; + +test.beforeEach(mockDataSourceRequest); + +test( + 'code editor autocomplete should handle table name escaping/quoting', + { + tag: '@plugins', + }, + async ({ explorePage, selectors, page }) => { + await page.getByLabel('Code').check(); + + const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox'); + await editor.fill('S'); + await page.getByLabel('SELECT FROM ').locator('a').click(); + await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); + await page.keyboard.press('Enter'); + + await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``); + + for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) { + await page.keyboard.press('Backspace'); + } + + await page.keyboard.press('Control+I'); + await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/mysql/utils.ts b/e2e-playwright/plugin-e2e/mysql/utils.ts similarity index 100% rename from e2e/plugin-e2e/mysql/utils.ts rename to e2e-playwright/plugin-e2e/mysql/utils.ts diff --git a/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts b/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts new file mode 100644 index 00000000000..9cf6c580927 --- /dev/null +++ b/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts @@ -0,0 +1,176 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { test, expect } from '@grafana/plugin-e2e'; + +import { normalTableName } from './mocks/mysql.mocks'; +import { mockDataSourceRequest } from './utils'; + +test.beforeEach(mockDataSourceRequest); + +test.describe( + 'mysql', + { + tag: '@plugins', + }, + () => { + test('visual query builder should handle macros', async ({ explorePage, page }) => { + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); + await page.getByText(normalTableName, { exact: true }).click(); + + // Open Data operations + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); + const select = page.getByLabel('Select options menu'); + await select.locator(page.getByText('$__timeGroupAlias')).click(); + + // Open column selector + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) + .click(); + await select.locator(page.getByText('createdAt')).click(); + + // Open Interval selector + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) + .click(); + await select.locator(page.getByText('$__interval')).click(); + + await page.getByRole('button', { name: 'Add column' }).click(); + + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); + await select.locator(page.getByText('AVG')).click(); + + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) + .nth(1) + .click(); + await select.locator(page.getByText('bigint')).click(); + + // Validate the query + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue( + `SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` + ); + }); + + test('visual query builder should handle time filter macro', async ({ explorePage, page }) => { + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); + await page.getByText(normalTableName, { exact: true }).click(); + + // Open column selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click(); + const select = page.getByLabel('Select options menu'); + await select.locator(page.getByText('createdAt')).click(); + + // Toggle where row + await page.getByLabel('Filter').last().click(); + + // Click add filter button + await page.getByRole('button', { name: 'Add filter' }).click(); + await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice + + // Open field selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click(); + await select.locator(page.getByText('createdAt')).click(); + + // Open operator selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); + await select.locator(page.getByText('Macros')).click(); + + // Open macros value selector + await explorePage.getByGrafanaSelector('Macros value selector').click(); + await select.locator(page.getByText('timeFilter', { exact: true })).click(); + + // Validate that the timeFilter macro was added + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue( + `SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50` + ); + + // Validate that the timeFilter macro was removed when changed to equals operator + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); + await select.locator(page.getByText('==')).click(); + + await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click(); + await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur(); + + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`); + }); + + test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => { + const queryParams = new URLSearchParams(); + queryParams.set('schemaVersion', '1'); + queryParams.set('orgId', '1'); + const panes = { + mmm: { + datasource: 'P4FDCC188E688367F', + queries: [ + { + refId: 'A', + datasource: { + type: 'mysql', + uid: 'P4FDCC188E688367F', + }, + format: 'table', + rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ", + editorMode: 'builder', + sql: { + columns: [ + { + type: 'function', + parameters: [ + { + type: 'functionParameter', + name: '*', + }, + ], + }, + ], + groupBy: [ + { + type: 'groupBy', + property: { + type: 'string', + }, + }, + ], + limit: 50, + whereJsonTree: { + id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a', + type: 'group', + children1: [ + { + type: 'rule', + id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91', + properties: { + fieldSrc: 'field', + field: 'name', + operator: 'select_any_in', + value: ['a'], + valueSrc: ['value'], + valueType: ['text'], + }, + }, + ], + }, + whereString: "name IN ('a')", + }, + dataset: 'DataMaker', + table: 'normalTable', + }, + ], + }, + }; + queryParams.set('panes', JSON.stringify(panes)); + + await explorePage.goto({ queryParams }); + + // Validate the query + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts b/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts new file mode 100644 index 00000000000..1bab8050be5 --- /dev/null +++ b/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'opentsdb' }); + + await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/README.md b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/README.md similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/README.md rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/README.md diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts new file mode 100644 index 00000000000..676de366071 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts @@ -0,0 +1,31 @@ +import * as e2e from '@grafana/e2e-selectors'; +import { expect, test } from '@grafana/plugin-e2e'; + +// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row +test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } }); + +test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => { + test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({ + page, + alertRuleEditPage, + }) => { + await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule'); + + //add working query + const queryA = alertRuleEditPage.getAlertRuleQueryRow('A'); + await queryA.datasource.set('gdev-prometheus'); + await queryA.locator.getByLabel('Code').click(); + await page.waitForFunction(() => window.monaco); + await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); + await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))'); + + //add broken query + const newQuery = await alertRuleEditPage.clickAddQueryRow(); + await newQuery.datasource.set('gdev-prometheus'); + await newQuery.locator.getByLabel('Code').click(); + await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); + await page.keyboard.insertText('topk(5,'); + + await expect(alertRuleEditPage.evaluate()).not.toBeOK(); + }); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts similarity index 62% rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts index 1a11fdd6a56..8ea363adab1 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts @@ -41,13 +41,15 @@ const scenarios: Scenario[] = [ }, ]; -for (const scenario of scenarios) { - test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { - annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); - await annotationEditPage.datasource.set('gdev-testdata'); - await page.getByLabel('Scenario').last().fill('CSV Content'); - await page.keyboard.press('Tab'); - await annotationEditPage.runQuery(); - await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); - }); -} +test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => { + for (const scenario of scenarios) { + test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { + annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); + await annotationEditPage.datasource.set('gdev-testdata'); + await page.getByLabel('Scenario').last().fill('CSV Content'); + await page.keyboard.press('Tab'); + await annotationEditPage.runQuery(); + await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); + }); + } +}); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts new file mode 100644 index 00000000000..6a1264d9c72 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.addPanel(); + await expect(page.url()).toContain('editPanel'); + }); + + test('add panel in new dashboard', async ({ dashboardPage, page }) => { + const panelEditPage = await dashboardPage.addPanel(); + await expect(panelEditPage.panel.locator).toBeVisible(); + await expect(page.url()).toContain('editPanel'); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts new file mode 100644 index 00000000000..3d6f2e924a4 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { + test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected save data source config to fail when Prometheus server is not running') + ).not.toBeOK(); + }); + + test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + configPage.mockHealthCheckResponse({ status: 200 }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected data source config to be successfully saved') + ).toBeOK(); + }); + }); + + test.describe('test data source with frontend only health check', () => { + test('valid credentials should display a success alert on the page', async ({ + createDataSourceConfigPage, + page, + }) => { + const configPage = await createDataSourceConfigPage({ type: 'zipkin' }); + configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200); + await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411'); + await expect(configPage.saveAndTest()).toBeOK(); + await expect( + configPage, + formatExpectError('Expected data source config to display success alert after save') + ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts new file mode 100644 index 00000000000..c97f2481fca --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('query data response should be OK when query is valid', async ({ explorePage }) => { + await explorePage.datasource.set('gdev-testdata'); + await expect( + explorePage.runQuery(), + formatExpectError('Expected Explore query to execute successfully') + ).toBeOK(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts new file mode 100644 index 00000000000..40771c4a7da --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; +const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; + +// override the feature toggles defined in playwright.config.ts only for tests in this file +test.use({ + featureToggles: { + [TRUTHY_CUSTOM_TOGGLE]: true, + [FALSY_CUSTOM_TOGGLE]: false, + }, +}); + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { + expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); + expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts new file mode 100644 index 00000000000..177bc908af7 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts @@ -0,0 +1,95 @@ +import * as e2e from '@grafana/e2e-selectors'; +import { expect, test } from '@grafana/plugin-e2e'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('Loki editor', () => { + test('Autocomplete features should work as expected.', async ({ page }) => { + // Go to loki datasource in explore + await page.goto( + '/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1' + ); + + const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container); + const queryEditorRows = page.getByTestId('query-editor-rows'); + + async function assertQueryEditorEmpty() { + const queryEditorEmptyText = /^Enter to Rename.+/; + await expect(queryEditor).toHaveText(queryEditorEmptyText); + } + + async function clearInput() { + // Clear focused input + // Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake + await page.waitForTimeout(100); + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + } + + // assert that the query builder is shown by default + await expect(page.getByText('Label filters')).toHaveCount(1); + + // switch to code editor + await page.getByLabel('Code').click(); + + await page.waitForFunction(() => window.monaco); + await expect(queryEditor).toHaveCount(1); + await assertQueryEditorEmpty(); + + // assert editor automatically adds close paren + await queryEditor.click(); + await page.keyboard.type('time('); + await expect(queryEditor).toContainText('time()'); + + // removes closing brace when opening brace is removed + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('avg_over_time('); + await expect(queryEditor).toContainText('avg_over_time()'); + await page.keyboard.press('Backspace'); + await expect(queryEditor).not.toContainText('avg_over_time()'); + await expect(queryEditor).toContainText('avg_over_time'); + + // keeps closing brace when opening brace is removed and inner values exist + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('time(test'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('Backspace'); + await expect(queryEditor).toContainText('timetest'); + + // overrides an automatically inserted paren + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('time()'); + await expect(queryEditor).toContainText('time()'); + + // does not override manually inserted braces + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('))'); + await expect(queryEditor).toContainText('))'); + + // Should execute the query when enter with shift is pressed + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.press('Shift+Enter'); + await expect(page.getByTestId('explore-no-data')).toHaveCount(1); + + // Suggestions plugin + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('av'); + await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1); + await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts new file mode 100644 index 00000000000..0133a3e3712 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts @@ -0,0 +1,105 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('panel edit page', () => { + test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('table panel data assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Table'); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate headers in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); + + test('timeseries panel - table view assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Time series'); + await panelEditPage.refreshPanel(); + await panelEditPage.toggleTableView(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate data elements in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); + }); + + test.describe('dashboard page', () => { + test('getting panel by title', async ({ gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('getting panel by id', async ({ gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect( + panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + }); + + test.describe('explore page', () => { + test('table panel', async ({ explorePage }) => { + const url = + 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; + await explorePage.goto({ + queryParams: new URLSearchParams(url), + }); + await expect( + explorePage.timeSeriesPanel.locator, + formatExpectError('Could not locate time series panel in explore page') + ).toBeVisible(); + await expect( + explorePage.tablePanel.locator, + formatExpectError('Could not locate table panel in explore page') + ).toBeVisible(); + await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts new file mode 100644 index 00000000000..4772a9717e8 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -0,0 +1,214 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; +import { scenarios } from '../mocks/resources'; + +const PANEL_TITLE = 'Table panel E2E test'; +const TABLE_VIZ_NAME = 'Table'; +const TIME_SERIES_VIZ_NAME = 'Time series'; +const STANDARD_OTIONS_CATEGORY = 'Standard options'; +const DISPLAY_NAME_LABEL = 'Display name'; +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('query editor query data', () => { + test('query data response should be OK when query is valid', async ({ panelEditPage }) => { + await panelEditPage.datasource.set('gdev-testdata'); + await expect( + panelEditPage.refreshPanel(), + formatExpectError('Expected panel query to execute successfully') + ).toBeOK(); + }); + + test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ + panelEditPage, + }) => { + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); + await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Expected panel error to be displayed after query execution') + ).toBeVisible(); + }); + }); + + test.describe('query editor with mocked responses', () => { + test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => { + await dashboardPage.mockResourceResponse('scenarios', scenarios); + const panelEditPage = await dashboardPage.addPanel(); + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Scenario').last().click(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Select.option), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText(scenarios.map((s) => s.name)); + }); + + test('mocked query data response', async ({ panelEditPage, selectors }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Did not expect panel error to be displayed after query execution') + ).toBeHidden(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText('val1val2val3val4'); + }); + }); + + test.describe('edit panel plugin settings', () => { + test('change viz to table panel, set panel title and collapse section', async ({ + panelEditPage, + selectors, + page, + }) => { + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), + formatExpectError('Expected panel visualization to be set to table') + ).toHaveText(TABLE_VIZ_NAME); + await panelEditPage.setPanelTitle(PANEL_TITLE); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)), + formatExpectError('Expected panel title to be updated') + ).toBeVisible(); + await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); + await expect( + page.getByText(DISPLAY_NAME_LABEL), + formatExpectError('Expected section to be collapsed') + ).toBeVisible(); + }); + + test('Select time zone in timezone picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = await panelEditPage.getCustomOptions('Axis'); + const timeZonePicker = axisOptions.getSelect('Time zone'); + + await timeZonePicker.selectOption('Europe/Stockholm'); + await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); + }); + + test('select unit in unit picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const unitPicker = standardOptions.getUnitPicker('Unit'); + + await unitPicker.selectOption('Misc > Pixels'); + + await expect(unitPicker).toHaveSelected('Pixels'); + }); + + test('enter value in number input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const lineWith = axisOptions.getNumberInput('Soft min'); + + await lineWith.fill('10'); + + await expect(lineWith).toHaveValue('10'); + }); + + test('enter value in slider', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const graphOptions = panelEditPage.getCustomOptions('Graph styles'); + const lineWidth = graphOptions.getSliderInput('Line width'); + + await lineWidth.fill('10'); + + await expect(lineWidth).toHaveValue('10'); + }); + + test('select value in single value select', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const colorSchemeSelect = standardOptions.getSelect('Color scheme'); + + await colorSchemeSelect.selectOption('Classic palette'); + await expect(colorSchemeSelect).toHaveSelected('Classic palette'); + }); + + test('clear input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const title = panelOptions.getTextInput('Title'); + + await expect(title).toHaveValue('New panel'); + await title.clear(); + await expect(title).toHaveValue(''); + }); + + test('enter value in input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const description = panelOptions.getTextInput('Description'); + + await expect(description).toHaveValue(''); + await description.fill('This is a panel'); + await expect(description).toHaveValue('This is a panel'); + }); + + test('unchecking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + + await showBorder.uncheck(); + await expect(showBorder).toBeChecked({ checked: false }); + }); + + test('checking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + }); + + test('re-selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + + await placement.check('Auto'); + await expect(placement).toHaveChecked('Auto'); + }); + + test('selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + }); + }); + + test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await panelEditPage.backToDashboard(); + await expect(page.url()).not.toContain('editPanel'); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts new file mode 100644 index 00000000000..c6025ca85fc --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { prometheusLabels } from '../mocks/resources'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('variable query with mocked response', async ({ variableEditPage, page }) => { + variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); + variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); + await variableEditPage.datasource.set('gdev-prometheus'); + await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); + await page.keyboard.press('Tab'); + await variableEditPage.runQuery(); + await expect( + variableEditPage, + formatExpectError('Expected variable edit page to display certain label names after query execution') + ).toDisplayPreviews(prometheusLabels.data); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts new file mode 100644 index 00000000000..3deaf6341f7 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +test.describe( + 'plugin-e2e-api-test viewer', + { + tag: ['@plugins'], + }, + () => { + test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { + await page.goto('/'); + const homePageURL = new URL(page.url()); + await page.goto('/datasources', { waitUntil: 'networkidle' }); + const redirectedPageURL = new URL(page.url()); + expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); + }); + + test('current user should have viewer role', async ({ page, request }) => { + await page.goto('/'); + const response = await request.get('/api/user/orgs'); + await expect(response).toBeOK(); + await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); + }); + } +); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/errors.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/errors.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts diff --git a/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts b/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts new file mode 100644 index 00000000000..f898c39fd97 --- /dev/null +++ b/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' }); + + await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts b/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts new file mode 100644 index 00000000000..c2ec230b481 --- /dev/null +++ b/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'zipkin' }); + + await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/scenarios/login.spec.ts b/e2e-playwright/scenarios/login.spec.ts new file mode 100644 index 00000000000..afef3d867d1 --- /dev/null +++ b/e2e-playwright/scenarios/login.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Scenario test: Can login successfully', + { + tag: ['@scenarios'], + }, + async ({ selectors, page }) => { + await page.goto(selectors.pages.Login.url); + + await page.getByTestId(selectors.pages.Login.username).fill('admin'); + await page.getByTestId(selectors.pages.Login.password).fill('admin'); + await page.getByTestId(selectors.pages.Login.submit).click(); + + await page.getByTestId(selectors.pages.Login.skip).click(); + + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + } +); diff --git a/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts b/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts new file mode 100644 index 00000000000..a6bea288915 --- /dev/null +++ b/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts @@ -0,0 +1,54 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Smoke tests', + { + tag: ['@smoke'], + }, + () => { + test('Login, create test data source, create dashboard and panel scenario', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + selectors, + page, + }) => { + const dataSourceConfigPage = await createDataSourceConfigPage({ + name: `e2e-${uuidv4()}`, + type: 'grafana-testdata-datasource', + }); + const { datasource } = dataSourceConfigPage; + await dataSourceConfigPage.saveAndTest({ + path: `/api/datasources/uid/${datasource.uid}?accesscontrol=true`, + }); + + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Select CSV Metric Values scenario + const scenarioSelect = dashboardPage.getByGrafanaSelector( + selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer + ); + await expect(scenarioSelect).toBeVisible(); + await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').click(); + await page.getByText('CSV Metric Values').click(); + + // Verify the graph renders by checking legend + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + + // Verify panel is added to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts b/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts new file mode 100644 index 00000000000..61e17bec7c6 --- /dev/null +++ b/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { GrafanaBootConfig } from '@grafana/runtime'; + +test.describe( + 'Panels smokescreen', + { + tag: ['@smoke'], + }, + () => { + test('Tests each panel type in the panel edit view to ensure no crash', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + // this test can absolutely take longer than the default 30s timeout + test.setTimeout(60000); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Get panel types from window object + const panelTypes = await page.evaluate(() => { + // @grafana/plugin-e2e doesn't export the full bootdata config + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const win = window as typeof window & { grafanaBootData: GrafanaBootConfig['bootData'] }; + return win.grafanaBootData?.settings?.panels ?? {}; + }); + + // Loop through every panel type and ensure no crash + for (const [_, panel] of Object.entries(panelTypes)) { + // Skip hidden and deprecated panels + if (!panel.hideFromList && panel.state !== 'deprecated') { + // Open visualization picker + const vizPicker = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); + await vizPicker.click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click(); + + // Verify panel type is selected + await expect(vizPicker).toHaveText(panel.name); + + // Ensure no unexpected error occurred + await expect(page.getByText('An unexpected error happened')).toBeHidden(); + } + } + }); + } +); diff --git a/e2e/plugin-e2e/start-and-run-suite b/e2e-playwright/start-and-run-suite similarity index 100% rename from e2e/plugin-e2e/start-and-run-suite rename to e2e-playwright/start-and-run-suite diff --git a/e2e-playwright/storybook/verify.spec.ts b/e2e-playwright/storybook/verify.spec.ts new file mode 100644 index 00000000000..9292cae4060 --- /dev/null +++ b/e2e-playwright/storybook/verify.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@grafana/plugin-e2e'; +// very basic test to verify that the button story loads correctly +// this is only intended to catch some basic build errors with storybook +test.describe( + 'Verify storybook', + { + tag: ['@storybook'], + }, + () => { + test('Loads the button story correctly', async ({ page }) => { + await page.goto('?path=/story/inputs-button--basic'); + const iframe = page.locator('#storybook-preview-iframe'); + await expect(iframe).toBeVisible(); + const iframeBody = iframe.contentFrame(); + await expect(iframeBody.getByText('Example button')).toBeVisible(); + }); + } +); diff --git a/e2e/test-plugins/README.md b/e2e-playwright/test-plugins/README.md similarity index 100% rename from e2e/test-plugins/README.md rename to e2e-playwright/test-plugins/README.md diff --git a/e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-app-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-app-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-app-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-app-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-app-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/plugin.json diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/plugin.json diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/.gitignore b/e2e-playwright/test-plugins/grafana-extensionstest-app/.gitignore similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/.gitignore rename to e2e-playwright/test-plugins/grafana-extensionstest-app/.gitignore diff --git a/e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md b/e2e-playwright/test-plugins/grafana-extensionstest-app/CHANGELOG.md similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md rename to e2e-playwright/test-plugins/grafana-extensionstest-app/CHANGELOG.md diff --git a/e2e/test-plugins/grafana-extensionstest-app/README.md b/e2e-playwright/test-plugins/grafana-extensionstest-app/README.md similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/README.md rename to e2e-playwright/test-plugins/grafana-extensionstest-app/README.md diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/constants.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/constants.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json similarity index 80% rename from e2e/test-plugins/grafana-extensionstest-app/package.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/package.json index 5fe3da7246e..d11250bdc4d 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json @@ -3,8 +3,8 @@ "version": "12.1.0-pre", "private": true, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "dev": "webpack -w -c ./webpack.config.ts --env development", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "typecheck": "tsc --noEmit", "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." }, diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/testIds.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/testIds.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/testIds.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/testIds.ts diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts new file mode 100644 index 00000000000..9f3d3f0bf35 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { testIds } from '../testIds'; +import pluginJson from '../plugin.json'; + +test.describe( + 'grafana-extensionstest-app', + { + tag: ['@plugins'], + }, + () => { + test('should display component exposed by another app', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/exposed-components`); + await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts new file mode 100644 index 00000000000..cb2039f6480 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import { testIds } from '../testIds'; + +test.describe( + 'grafana-extensionstest-app', + { + tag: ['@plugins'], + }, + () => { + test('should render component with usePluginComponents hook', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-components`); + await expect( + page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) + ).toHaveText('Hello World!'); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts new file mode 100644 index 00000000000..852fe00fa85 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; +import { testIds } from '../testIds'; + +test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); + + test('should extend main app with link extension from app B', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Open from B').click(); + await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); + }); + + test('should extend main app with basic link extension from app A', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Basic link').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); + + test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({ + page, + }) => { + await page.goto(`/a/${testApp3pluginJson.id}`); + const container = await page.getByTestId(testIds.appC.section1); + await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible(); + }); + + const panelTitle = 'Link with defaults'; + const extensionTitle = 'Open from time series...'; + + const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; + const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; + + test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); + }); + + test('should add link extension (onclick) with defaults to time series panel', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); + }); + + test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { + const panelTitle = 'Link with new name'; + const extensionTitle = 'Open from piechart'; + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); + }); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/utils.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/tests/utils.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/tsconfig.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/tsconfig.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/tsconfig.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/tsconfig.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts similarity index 86% rename from e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts index 3303ed94f3c..564555396a5 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts @@ -1,7 +1,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; import { mergeWithCustomize, unique } from 'webpack-merge'; -import { Configuration } from 'webpack'; +import { type Configuration } from 'webpack'; function skipFiles(f: string): boolean { if (f.includes('/dist/')) { @@ -19,7 +19,7 @@ function skipFiles(f: string): boolean { return true; } -const config = async (env: Record): Promise => { +const config = async (env: Env): Promise => { const baseConfig = await grafanaConfig(env); const customConfig = { plugins: [ diff --git a/e2e/test-plugins/grafana-test-datasource/CHANGELOG.md b/e2e-playwright/test-plugins/grafana-test-datasource/CHANGELOG.md similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/CHANGELOG.md rename to e2e-playwright/test-plugins/grafana-test-datasource/CHANGELOG.md diff --git a/e2e/test-plugins/grafana-test-datasource/README.md b/e2e-playwright/test-plugins/grafana-test-datasource/README.md similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/README.md rename to e2e-playwright/test-plugins/grafana-test-datasource/README.md diff --git a/e2e/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx rename to e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx diff --git a/e2e/test-plugins/grafana-test-datasource/components/QueryEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/QueryEditor.tsx similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/components/QueryEditor.tsx rename to e2e-playwright/test-plugins/grafana-test-datasource/components/QueryEditor.tsx diff --git a/e2e/test-plugins/grafana-test-datasource/datasource.ts b/e2e-playwright/test-plugins/grafana-test-datasource/datasource.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/datasource.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/datasource.ts diff --git a/e2e/test-plugins/grafana-test-datasource/img/logo.svg b/e2e-playwright/test-plugins/grafana-test-datasource/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/img/logo.svg rename to e2e-playwright/test-plugins/grafana-test-datasource/img/logo.svg diff --git a/e2e/test-plugins/grafana-test-datasource/module.ts b/e2e-playwright/test-plugins/grafana-test-datasource/module.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/module.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/module.ts diff --git a/e2e/test-plugins/grafana-test-datasource/package.json b/e2e-playwright/test-plugins/grafana-test-datasource/package.json similarity index 80% rename from e2e/test-plugins/grafana-test-datasource/package.json rename to e2e-playwright/test-plugins/grafana-test-datasource/package.json index 437d542510a..9f976341268 100644 --- a/e2e/test-plugins/grafana-test-datasource/package.json +++ b/e2e-playwright/test-plugins/grafana-test-datasource/package.json @@ -3,8 +3,8 @@ "version": "12.1.0-pre", "private": true, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", - "dev": "webpack -w -c ./webpack.config.ts --env development", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "typecheck": "tsc --noEmit", "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx ." }, diff --git a/e2e/test-plugins/grafana-test-datasource/plugin.json b/e2e-playwright/test-plugins/grafana-test-datasource/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/plugin.json rename to e2e-playwright/test-plugins/grafana-test-datasource/plugin.json diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts b/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts new file mode 100644 index 00000000000..ed38635cd86 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts @@ -0,0 +1,47 @@ +import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; + +// The following tests verify that label and input field association is working correctly. +// If these tests break, e2e tests in external plugins will break too. + +test.describe( + 'grafana-test-datasource', + { + tag: ['@plugins'], + }, + () => { + test.describe('config editor ', () => { + let configPage: DataSourceConfigPage; + test.beforeEach(async ({ createDataSourceConfigPage }) => { + configPage = await createDataSourceConfigPage({ type: 'grafana-e2etest-datasource' }); + }); + + test('text input field', async ({ page }) => { + const field = page.getByRole('textbox', { name: 'API key' }); + await expect(field).toBeEmpty(); + await field.fill('test text'); + await expect(field).toHaveValue('test text'); + }); + + test('switch field', async ({ page }) => { + const field = page.getByLabel('Switch Enabled'); + await expect(field).not.toBeChecked(); + await field.check(); + await expect(field).toBeChecked(); + }); + + test('checkbox field', async ({ page }) => { + const field = page.getByRole('checkbox', { name: 'Checkbox Enabled' }); + await expect(field).not.toBeChecked(); + await field.check({ force: true }); + await expect(field).toBeChecked(); + }); + + test('select field', async ({ page, selectors }) => { + const field = page.getByRole('combobox', { name: 'Auth type' }); + await field.click(); + const option = selectors.components.Select.option; + await expect(configPage.getByGrafanaSelector(option)).toHaveText(['keys', 'credentials']); + }); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts b/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts new file mode 100644 index 00000000000..5c1e636da7e --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'grafana-test-datasource', + { + tag: ['@plugins'], + }, + () => { + test('should render variable editor', async ({ variableEditPage, page }) => { + await variableEditPage.datasource.set('gdev-e2etestdatasource'); + await expect(page.getByRole('textbox', { name: 'Query Text' })).toBeVisible(); + }); + + test('create new, successful variable query', async ({ variableEditPage, page }) => { + await variableEditPage.datasource.set('gdev-e2etestdatasource'); + await page.getByRole('textbox', { name: 'Query Text' }).fill('variableQuery'); + await variableEditPage.runQuery(); + await expect(variableEditPage).toDisplayPreviews(['A', 'B']); + }); + } +); diff --git a/e2e/test-plugins/grafana-test-datasource/tsconfig.json b/e2e-playwright/test-plugins/grafana-test-datasource/tsconfig.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/tsconfig.json rename to e2e-playwright/test-plugins/grafana-test-datasource/tsconfig.json diff --git a/e2e/test-plugins/grafana-test-datasource/types.ts b/e2e-playwright/test-plugins/grafana-test-datasource/types.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/types.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/types.ts diff --git a/e2e/test-plugins/grafana-test-datasource/variables.ts b/e2e-playwright/test-plugins/grafana-test-datasource/variables.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/variables.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/variables.ts diff --git a/e2e/test-plugins/grafana-test-datasource/webpack.config.ts b/e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts similarity index 86% rename from e2e/test-plugins/grafana-test-datasource/webpack.config.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts index 3303ed94f3c..564555396a5 100644 --- a/e2e/test-plugins/grafana-test-datasource/webpack.config.ts +++ b/e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts @@ -1,7 +1,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; import { mergeWithCustomize, unique } from 'webpack-merge'; -import { Configuration } from 'webpack'; +import { type Configuration } from 'webpack'; function skipFiles(f: string): boolean { if (f.includes('/dist/')) { @@ -19,7 +19,7 @@ function skipFiles(f: string): boolean { return true; } -const config = async (env: Record): Promise => { +const config = async (env: Env): Promise => { const baseConfig = await grafanaConfig(env); const customConfig = { plugins: [ diff --git a/e2e-playwright/utils/dashboard-helpers.ts b/e2e-playwright/utils/dashboard-helpers.ts new file mode 100644 index 00000000000..ff73bfe919f --- /dev/null +++ b/e2e-playwright/utils/dashboard-helpers.ts @@ -0,0 +1,35 @@ +import { Page, expect } from '@playwright/test'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * Add a new dashboard + */ +export async function addDashboard(page: Page, title?: string): Promise { + const dashboardTitle = title || `e2e-${uuidv4()}`; + + // Navigate to add dashboard page + await page.goto('/dashboard/new'); + + // Save the dashboard + const saveButton = page.getByTestId('data-testid Save dashboard button'); + await saveButton.click(); + + // Enter dashboard title + const titleInput = page.getByLabel('Save dashboard title field'); + await titleInput.clear(); + await titleInput.fill(dashboardTitle); + + // Click save + const saveAsButton = page.getByTestId('data-testid Save dashboard drawer button'); + await saveAsButton.click(); + + // Wait for success notification + await expect(page.getByText('Dashboard saved')).toBeVisible(); + + // Get the dashboard UID from the URL + const url = page.url(); + const uidMatch = url.match(/\/d\/([^\/\?]+)/); + const uid = uidMatch ? uidMatch[1] : ''; + + return uid; +} diff --git a/e2e-playwright/utils/prometheus-helpers.ts b/e2e-playwright/utils/prometheus-helpers.ts new file mode 100644 index 00000000000..229fee2bf33 --- /dev/null +++ b/e2e-playwright/utils/prometheus-helpers.ts @@ -0,0 +1,52 @@ +import { Page } from '@playwright/test'; + +export async function getResources(page: Page): Promise { + // Mock the Prometheus API responses + await page.route(/__name__/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: ['metric1', 'metric2'], + }), + }); + }); + + await page.route(/metadata/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: { + metric1: [ + { + type: 'counter', + help: 'metric1 help', + unit: '', + }, + ], + metric2: [ + { + type: 'counter', + help: 'metric2 help', + unit: '', + }, + ], + }, + }), + }); + }); + + await page.route(/labels/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: ['__name__', 'action', 'active', 'backend'], + }), + }); + }); +} diff --git a/e2e-playwright/various-suite/bar-gauge.spec.ts b/e2e-playwright/various-suite/bar-gauge.spec.ts new file mode 100644 index 00000000000..ecd07b7eb9b --- /dev/null +++ b/e2e-playwright/various-suite/bar-gauge.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Various Suite test: Bar Gauge Panel', + { + tag: ['@various'], + }, + async ({ gotoDashboardPage, selectors, page }) => { + await gotoDashboardPage({ uid: 'O6f11TZWk' }); + + const barGaugeElement = page + .locator('[data-viz-panel-key="panel-6"]') + .getByTestId(selectors.components.Panels.Visualization.BarGauge.valueV2) + .first(); + + await expect(barGaugeElement).toBeVisible(); + await expect(barGaugeElement).toHaveCSS('color', 'rgb(242, 73, 92)'); + await expect(barGaugeElement).toContainText('100'); + } +); diff --git a/e2e-playwright/various-suite/bookmarks.spec.ts b/e2e-playwright/various-suite/bookmarks.spec.ts new file mode 100644 index 00000000000..9c67884ea7e --- /dev/null +++ b/e2e-playwright/various-suite/bookmarks.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Pin nav items', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + // Set localStorage to dock the navigation + await page.evaluate(() => { + localStorage.setItem('grafana.navigation.docked', 'true'); + }); + }); + + test('should pin the selected menu item and add it as a Bookmarks menu item child', async ({ page, selectors }) => { + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + + const navList = navMenu.locator('ul[aria-label="Navigation"]'); + await expect(navList).toBeVisible(); + + // Check if the Bookmark section is visible + const bookmarksItem = navList.locator('li').nth(1); + await expect(bookmarksItem).toBeVisible(); + await expect(bookmarksItem).toContainText('Bookmarks'); + + // Check if the Administration section is visible + const adminItem = navList.locator('li').last(); + await expect(adminItem).toBeVisible(); + await expect(adminItem).toContainText('Administration'); + + // Click the "Add to Bookmarks" button in the Administration section + const addToBookmarksButton = adminItem.getByLabel('Add to Bookmarks'); + await addToBookmarksButton.click({ force: true }); + + // Check if the Administration menu item is visible in the Bookmarks section + const expandBookmarksButton = bookmarksItem.locator('button[aria-label="Expand section: Bookmarks"]'); + await expect(expandBookmarksButton).toBeVisible(); + await expandBookmarksButton.click({ force: true }); + + const administrationLink = bookmarksItem.locator('a').filter({ hasText: 'Administration' }); + await expect(administrationLink).toBeVisible(); + }); + + test('should unpin the item and remove it from the Bookmarks section', async ({ page, selectors }) => { + // Set Administration as a pinned item + await page.evaluate(() => { + localStorage.setItem( + 'grafana.user.preferences', + JSON.stringify({ + navbar: { bookmarkUrls: ['/admin'] }, + }) + ); + }); + + // Visit the home page + await page.goto('/'); + + // Reload the page to apply the docked navigation + await page.reload(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + + const navList = navMenu.locator('ul[aria-label="Navigation"]'); + await expect(navList).toBeVisible(); + + // Check if the Bookmark section is visible + const bookmarksItem = navList.locator('li').nth(1); + await expect(bookmarksItem).toBeVisible(); + await expect(bookmarksItem).toContainText('Bookmarks'); + + // Expand the Bookmarks section + const expandBookmarksButton = bookmarksItem.getByLabel('Expand section: Bookmarks'); + await expandBookmarksButton.click({ force: true }); + + // Check that Administration is visible in bookmarks + const administrationLink = bookmarksItem.locator('a').filter({ hasText: 'Administration' }); + await expect(administrationLink).toBeVisible(); + + // Click the "Remove from Bookmarks" button + const removeFromBookmarksButton = bookmarksItem.getByLabel('Remove from Bookmarks'); + await removeFromBookmarksButton.click({ force: true }); + + // Check that Administration is no longer in bookmarks + await expect(bookmarksItem.locator('a')).toHaveCount(1); + await expect(bookmarksItem.locator('a').filter({ hasText: 'Administration' })).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/exemplars.spec.ts b/e2e-playwright/various-suite/exemplars.spec.ts new file mode 100644 index 00000000000..1e2f77aa04d --- /dev/null +++ b/e2e-playwright/various-suite/exemplars.spec.ts @@ -0,0 +1,150 @@ +import { Page } from '@playwright/test'; + +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Exemplars', + { + tag: ['@various'], + }, + () => { + const dataSourceName = 'PromExemplar'; + + async function addDataSource(page, selectors, createDataSourceConfigPage) { + // Navigate to add data source page + await createDataSourceConfigPage({ type: 'prometheus', name: dataSourceName }); + + // Add exemplars configuration + const exemplarsAddButton = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.exemplarsAddButton + ); + await exemplarsAddButton.click(); + + const internalLinkSwitch = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.internalLinkSwitch + ); + await internalLinkSwitch.check({ force: true }); + + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.click(); + await page.keyboard.type('http://prom-url:9090'); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await dataSourcePicker.click(); + + const tempoOption = page.getByText('gdev-tempo'); + await tempoOption.scrollIntoViewIfNeeded(); + await expect(tempoOption).toBeVisible(); + await tempoOption.click(); + + // Save the data source + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + } + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + await addDataSource(page, selectors, createDataSourceConfigPage); + await page.goto('/'); + }); + + test('should be able to navigate to configured data source', async ({ page, selectors }) => { + // Mock API responses + await page.route(/api\/ds\/query/, async (route) => { + const postData = route.request().postDataJSON(); + const datasourceType = postData.queries[0].datasource.type; + + if (datasourceType === 'prometheus') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(require('../fixtures/exemplars-query-response.json')), + }); + } else if (datasourceType === 'tempo') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(require('../fixtures/tempo-response.json')), + }); + } else { + await route.fulfill({ status: 200, body: '{}' }); + } + }); + + // Navigate to explore + await page.goto('/explore'); + + // Select data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(dataSourceName); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + + // Switch to code editor + const codeRadioButton = page.getByTestId(selectors.components.RadioButton.container).filter({ hasText: 'Code' }); + await codeRadioButton.click(); + + // Wait for lazy loading Monaco + await waitForMonacoToLoad(page); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.clear(); + await fromField.fill('2021-07-10 17:10:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.clear(); + await toField.fill('2021-07-10 17:30:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + // Type query + const queryField = page.getByTestId(selectors.components.QueryField.container); + await expect(queryField).toBeVisible(); + await queryField.click(); + await page.keyboard.type('exemplar-query_bucket'); + await page.keyboard.press('Shift+Enter'); + + await page.waitForTimeout(1000); + + // Click zoom to data + const zoomToDataButton = page.getByTestId('time-series-zoom-to-data'); + await zoomToDataButton.click(); + + // Hover over exemplar marker and click + const exemplarMarker = page.getByTestId(selectors.components.DataSource.Prometheus.exemplarMarker).first(); + await exemplarMarker.hover(); + + const queryWithTempoLink = page.getByText('Query with gdev-tempo'); + await queryWithTempoLink.click(); + + // Verify trace viewer has span bars + const spanBars = page.getByTestId(selectors.components.TraceViewer.spanBar); + await expect(spanBars).toHaveCount(11); + }); + } +); + +export async function waitForMonacoToLoad(page: Page) { + // Wait for spinner to disappear + const spinner = page.getByTestId('Spinner'); + await expect(spinner).toBeHidden(); + + // Wait for Monaco to be available in window + await page.waitForFunction(() => { + return window.monaco !== undefined; + }); + + // Wait for Monaco editor textarea to exist + const monacoTextarea = page.locator('.monaco-editor').first(); + await expect(monacoTextarea).toBeVisible(); +} diff --git a/e2e-playwright/various-suite/explore.spec.ts b/e2e-playwright/various-suite/explore.spec.ts new file mode 100644 index 00000000000..afbc9d74c32 --- /dev/null +++ b/e2e-playwright/various-suite/explore.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Explore', + { + tag: ['@various'], + }, + () => { + test('Basic path through Explore.', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const exploreContainer = dashboardPage.getByGrafanaSelector(selectors.pages.Explore.General.container); + await expect(exploreContainer).toHaveCount(1); + + const refreshButton = dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2); + await expect(refreshButton).toHaveCount(1); + + const scenarioSelectContainer = dashboardPage.getByGrafanaSelector( + selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer + ); + await expect(scenarioSelectContainer).toBeVisible(); + await scenarioSelectContainer.locator('input[id*="test-data-scenario-select-"]').click(); + + const csvMetricValues = page.getByText('CSV Metric Values'); + await expect(csvMetricValues).toBeVisible(); + await csvMetricValues.click(); + }); + } +); diff --git a/e2e-playwright/various-suite/filter-annotations.spec.ts b/e2e-playwright/various-suite/filter-annotations.spec.ts new file mode 100644 index 00000000000..7e51d607864 --- /dev/null +++ b/e2e-playwright/various-suite/filter-annotations.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Annotations filtering', + { + tag: ['@various'], + }, + () => { + const DASHBOARD_ID = 'ed155665'; + + test('Tests switching filter type updates the UI accordingly', async ({ page, selectors, gotoDashboardPage }) => { + // Navigate to dashboard + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID }); + + // Enter edit mode + const editButton = dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + // Open settings + const settingsButton = dashboardPage.getByGrafanaSelector( + selectors.components.NavToolbar.editDashboard.settingsButton + ); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + // Navigate to annotations tab + const annotationsTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Annotations')); + await annotationsTab.click(); + + // Click "New query" + const newQueryButton = page.getByText('New query'); + await newQueryButton.click(); + + // Set annotation name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Annotations.Settings.name + ); + await nameInput.fill('Red - Panel two'); + + // Test filter type switching + const showInLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel + ); + await expect(showInLabel).toBeVisible(); + + // Test "All panels" option + const annotationsTypeInput = dashboardPage + .getByGrafanaSelector(selectors.components.Annotations.annotationsTypeInput) + .locator('input'); + await annotationsTypeInput.fill('All panels'); + await annotationsTypeInput.press('Enter'); + + const annotationsChoosePanelInput = dashboardPage.getByGrafanaSelector( + selectors.components.Annotations.annotationsChoosePanelInput + ); + await expect(annotationsChoosePanelInput).toBeHidden(); + + // Test "All panels except" option + await annotationsTypeInput.fill('All panels except'); + await annotationsTypeInput.press('Enter'); + await expect(annotationsChoosePanelInput).toBeVisible(); + + // Test "Selected panels" option + await annotationsTypeInput.fill('Selected panels'); + await annotationsTypeInput.press('Enter'); + await expect(annotationsChoosePanelInput).toBeVisible(); + + // Select "Panel two" + await annotationsTypeInput.fill('Panel two'); + await annotationsTypeInput.press('Enter'); + + // Click outside to close dropdown + await page.click('body', { position: { x: 0, y: 0 } }); + + // Go back to dashboard + const backToDashboardButton = dashboardPage.getByGrafanaSelector( + selectors.components.NavToolbar.editDashboard.backToDashboardButton + ); + await expect(backToDashboardButton).toBeVisible(); + await backToDashboardButton.click(); + + // Test annotation controls + const dashboardControls = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls); + await expect(dashboardControls).toBeVisible(); + + // Test "Red - Panel two" annotation toggle + const redPanelTwoLabel = page.getByLabel('Red - Panel two'); + await expect(redPanelTwoLabel).toBeVisible(); + + const redPanelTwoToggleParent = page.getByLabel('Red - Panel two').locator('..'); + const redPanelTwoCheckbox = redPanelTwoToggleParent.locator('input'); + + // Check initial state + await expect(redPanelTwoCheckbox).toBeChecked(); + + // Uncheck and verify + await redPanelTwoCheckbox.uncheck({ force: true }); + await expect(redPanelTwoCheckbox).toBeChecked({ checked: false }); + + // Check again and verify + await redPanelTwoCheckbox.check({ force: true }); + await expect(redPanelTwoCheckbox).toBeChecked(); + + // Test "Red, only panel 1" annotation toggle + const redOnlyPanelOneLabel = page.getByLabel('Red, only panel 1'); + await expect(redOnlyPanelOneLabel).toBeVisible(); + + const redOnlyPanelOneToggleParent = page.getByLabel('Red, only panel 1').locator('..'); + const redOnlyPanelOneCheckbox = redOnlyPanelOneToggleParent.locator('input'); + await expect(redOnlyPanelOneCheckbox).toBeChecked(); + + // Verify annotations in panel + const panelOne = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel one')); + await expect(panelOne).toBeVisible(); + + const annotationMarkers = panelOne.getByTestId(selectors.pages.Dashboard.Annotations.marker); + await expect(annotationMarkers).toHaveCount(4); + }); + } +); diff --git a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts new file mode 100644 index 00000000000..dfa19753b15 --- /dev/null +++ b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const APP_ID = 'sandbox-app-test'; + +test.describe( + 'Datasource sandbox', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + await page.request.post(`/api/plugins/${APP_ID}/settings`, { + data: { + enabled: true, + }, + }); + }); + + test.describe('App Page', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Loads the app page without the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeHidden(); + + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); + }); + + test('Loads the app configuration without the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeHidden(); + + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Loads the app page with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); + + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); + }); + + test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); + + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); + }); + }); + }); + } +); diff --git a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts new file mode 100644 index 00000000000..df0f32aeba0 --- /dev/null +++ b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @@ -0,0 +1,203 @@ +import { random } from 'lodash'; + +import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; + +const DATASOURCE_ID = 'sandbox-test-datasource'; +let DATASOURCE_CONNECTION_ID = ''; +const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; + +// Originally skipped due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts +// TODO: fix and remove skip +test.describe.skip( + 'Datasource sandbox', + { + tag: ['@various', '@wip'], + }, + () => { + let configPage: DataSourceConfigPage; + test.beforeEach(async ({ createDataSourceConfigPage }) => { + // Add the datasource + configPage = await createDataSourceConfigPage({ + type: 'sandbox', + name: DATASOURCE_TYPED_NAME, + }); + }); + + test.describe('Config Editor', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Should not render a sandbox wrapper around the datasource config editor', async ({ page }) => { + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await page.waitForTimeout(300); // wait to prevent false positives because playwright checks too fast + + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeHidden(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Should render a sandbox wrapper around the datasource config editor', async ({ page, selectors }) => { + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); + }); + + test('Should store values in jsonData and secureJsonData correctly', async ({ page }) => { + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + + const valueToStore = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToStore); + await expect(queryInput).toHaveValue(valueToStore); + + const saveButton = page.getByTestId('data-testid Data source settings page Save and test button'); + await saveButton.click(); + + const alert = page.locator('[data-testid="data-testid Alert"]'); + await expect(alert).toBeVisible(); + await expect(alert).toContainText('Sandbox Success'); + + // validate the value was stored + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await expect(queryInput).not.toBeDisabled(); + await expect(queryInput).toHaveValue(valueToStore); + }); + }); + }); + + test.describe('Explore Page', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Should not wrap the query editor in a sandbox wrapper', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + await page.waitForTimeout(300); // wait to prevent false positives because playwright checks too fast + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeHidden(); + }); + + test('Should accept values when typed', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const valueToType = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Should wrap the query editor in a sandbox wrapper', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); + }); + + test('Should accept values when typed', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const valueToType = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); + + // typing the query editor should reflect in the url + await expect(page).toHaveURL(new RegExp(valueToType)); + }); + }); + }); + + test.afterEach(async ({ page }) => { + // Clear cookies after each test + await page.context().clearCookies(); + }); + } +); diff --git a/e2e-playwright/various-suite/gauge.spec.ts b/e2e-playwright/various-suite/gauge.spec.ts new file mode 100644 index 00000000000..3e2d6023c95 --- /dev/null +++ b/e2e-playwright/various-suite/gauge.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport so all gauge panels load properly +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +test.describe( + 'Gauge Panel', + { + tag: ['@various'], + }, + () => { + test('Gauge rendering e2e tests', async ({ gotoDashboardPage, selectors, page }) => { + // open Panel Tests - Gauge + const dashboardPage = await gotoDashboardPage({ uid: '_5rDmaQiz' }); + + // check that gauges are rendered + const gaugeElements = page.locator('.flot-base'); + await expect(gaugeElements).toHaveCount(16); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/graph-auto-migrate.spec.ts b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts new file mode 100644 index 00000000000..8b1234a8160 --- /dev/null +++ b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'XMjIZPmik'; +const DASHBOARD_NAME = 'Panel Tests - Graph Time Regions'; +const UPLOT_MAIN_DIV_SELECTOR = 'uplot-main-div'; +const ANNOTATION_MARKER_SELECTOR = 'data-testid annotation-marker'; + +test.describe( + 'Auto-migrate graph panel', + { + tag: ['@various'], + }, + () => { + test('Graph panel is auto-migrated', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden(); + + await gotoDashboardPage({ uid: DASHBOARD_ID }); + + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeVisible(); + }); + + test('Annotation markers exist for time regions', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden(); + + await gotoDashboardPage({ uid: DASHBOARD_ID }); + + // Check Business Hours panel + const businessHoursPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Business Hours') + ); + await expect(businessHoursPanel).toBeVisible(); + const businessHoursMarker = businessHoursPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(businessHoursMarker).toBeVisible(); + + // Check Sunday's 20-23 panel + const sundayPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title("Sunday's 20-23")); + await expect(sundayPanel).toBeVisible(); + const sundayMarker = sundayPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(sundayMarker).toBeVisible(); + + // Check Each day of week panel + const eachDayPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Each day of week') + ); + await expect(eachDayPanel).toBeVisible(); + const eachDayMarker = eachDayPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(eachDayMarker).toBeVisible(); + + // Scroll to bottom + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + // Check 05:00 panel + const timePanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('05:00')); + await expect(timePanel).toBeVisible(); + const timeMarker = timePanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(timeMarker).toBeVisible(); + + // Check From 22:00 to 00:30 panel + const midnightPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('From 22:00 to 00:30 (crossing midnight)') + ); + await expect(midnightPanel).toBeVisible(); + const midnightMarker = midnightPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(midnightMarker).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/inspect-drawer.spec.ts b/e2e-playwright/various-suite/inspect-drawer.spec.ts new file mode 100644 index 00000000000..60f0dd42b06 --- /dev/null +++ b/e2e-playwright/various-suite/inspect-drawer.spec.ts @@ -0,0 +1,173 @@ +import { Page } from 'playwright-core'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PANEL_UNDER_TEST = 'Value reducers 1'; + +test.describe( + 'Inspect drawer tests', + { + tag: ['@various'], + }, + () => { + test('Tests various Inspect Drawer scenarios', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'wfTJJL5Wz' }); + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await panel.scrollIntoViewIfNeeded(); + await expect(panel).toBeVisible(); + + // Open panel menu + const panelMenu = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)); + await panelMenu.click({ force: true }); + + // Hover over Inspect menu item to show submenu + const inspectMenuItem = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.menuItems('Inspect') + ); + await inspectMenuItem.hover(); + + // Click on Data submenu item + const dataMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Data')); + await dataMenuItem.click(); + + await expectDrawerTabsAndContent(dashboardPage, selectors, page); + + await expectDrawerClose(dashboardPage, selectors); + + await expectSubMenuScenario(dashboardPage, selectors, page, 'Data'); + await expectSubMenuScenario(dashboardPage, selectors, page, 'Query'); + await expectSubMenuScenario(dashboardPage, selectors, page, 'Panel JSON', 'JSON'); + + // Test edit panel scenario + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)) + .click({ force: true }); + const editMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')); + await editMenuItem.click(); + + const queryInspectorButton = dashboardPage.getByGrafanaSelector( + selectors.components.QueryTab.queryInspectorButton + ); + await expect(queryInspectorButton).toBeVisible(); + await queryInspectorButton.click(); + + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeVisible(); + + const queryTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Query')); + await expect(queryTab).toBeVisible(); + + // Query should be the active tab + const activeTab = page.locator('a[class*="-activeTabStyle"]'); + await expect(activeTab).toHaveText('Query'); + + const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content); + await expect(queryContent).toBeVisible(); + }); + } +); + +const expectDrawerTabsAndContent = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, page: Page) => { + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeVisible(); + + const dataTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Data')); + await expect(dataTab).toBeVisible(); + + // Data should be the active tab + const activeTab = page.locator(selectors.components.Tab.active('')); + await expect(activeTab).toHaveText('Data'); + + const dataContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content); + await expect(dataContent).toBeVisible(); + + const statsContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content); + await expect(statsContent).toBeHidden(); + + const jsonContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content); + await expect(jsonContent).toBeHidden(); + + const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content); + await expect(queryContent).toBeHidden(); + + // Test Stats tab + const statsTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Stats')); + await expect(statsTab).toBeVisible(); + await statsTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeHidden(); + + // Test JSON tab + const jsonTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('JSON')); + await expect(jsonTab).toBeVisible(); + await jsonTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeHidden(); + + // Test Query tab + const queryTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Query')); + await expect(queryTab).toBeVisible(); + await queryTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeHidden(); +}; + +const expectDrawerClose = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + // Close using close button + const closeButton = dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close); + await closeButton.click(); + + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeHidden(); +}; + +const expectSubMenuScenario = async ( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + page: Page, + subMenu: string, + tabTitle?: string +) => { + tabTitle = tabTitle ?? subMenu; + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await panel.scrollIntoViewIfNeeded(); + await expect(panel).toBeVisible(); + + // Open panel menu + const panelMenu = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)); + await panelMenu.click({ force: true }); + + // Hover over Inspect menu item to show submenu + const inspectMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Inspect')); + await inspectMenuItem.hover(); + + // Click on submenu item + const subMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems(subMenu)); + await subMenuItem.click(); + + // Tab should be visible and active + const tab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabTitle)); + await expect(tab).toBeVisible(); + + const activeTab = page.locator(selectors.components.Tab.active('')); + await expect(activeTab).toHaveText(tabTitle); + + await expectDrawerClose(dashboardPage, selectors); +}; diff --git a/e2e-playwright/various-suite/keybinds.spec.ts b/e2e-playwright/various-suite/keybinds.spec.ts new file mode 100644 index 00000000000..4ff37889838 --- /dev/null +++ b/e2e-playwright/various-suite/keybinds.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Keyboard shortcuts', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page, selectors }) => { + await page.goto('/'); + + // Wait for the page to load + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Latest from the blog')); + await expect(panelTitle).toBeVisible(); + }); + + test('sequence shortcuts should work', async ({ page, selectors }) => { + // Navigate to explore with 'ge' shortcut + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Navigate to profile with 'gp' shortcut + await page.keyboard.type('gp'); + const preferencesSaveButton = page.getByTestId(selectors.components.UserProfile.preferencesSaveButton); + await expect(preferencesSaveButton).toBeVisible(); + + // Navigate back to home with 'gh' shortcut + await page.keyboard.type('gh'); + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Latest from the blog')); + await expect(panelTitle).toBeVisible(); + }); + + test('ctrl+z should zoom out the time range', async ({ page, selectors }) => { + // Navigate to explore + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.fill('2024-06-05 10:05:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.fill('2024-06-05 10:06:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + await page.keyboard.press('Control+z'); + + const expectedRange = 'Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30'; + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + }); + + test('time range shortcuts should work', async ({ page, selectors }) => { + // Navigate to explore + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.fill('2024-06-05 10:05:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.fill('2024-06-05 10:06:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + let expectedRange = 'Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00'; + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + + // Use time range shortcut to move back + await page.keyboard.press('t'); + await page.keyboard.press('ArrowLeft'); + + expectedRange = 'Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00'; // 1 min back + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + }); + } +); diff --git a/e2e-playwright/various-suite/loki-query-builder.spec.ts b/e2e-playwright/various-suite/loki-query-builder.spec.ts new file mode 100644 index 00000000000..47e3c35299f --- /dev/null +++ b/e2e-playwright/various-suite/loki-query-builder.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const MISSING_LABEL_FILTER_ERROR_MESSAGE = 'Select at least 1 label filter (label and value)'; +const dataSourceName = 'LokiBuilder'; +const finalQuery = 'rate({instance=~"instance1|instance2"} | logfmt | __error__=`` [$__auto]'; + +test.describe( + 'Loki query builder', + { + tag: ['@various'], + }, + () => { + test('should be able to use all modes', async ({ createDataSource, page, dashboardPage, selectors }) => { + await createDataSource({ type: 'loki', name: dataSourceName }); + // Mock API responses + await page.route(/labels\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance', 'job', 'source'] }), + }); + }); + + await page.route(/series?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: [{ instance: 'instance1' }] }), + }); + }); + + await page.route(/values/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance1', 'instance2'] }), + }); + }); + + await page.route(/index\/stats/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ streams: 2, chunks: 2660, bytes: 2721792, entries: 14408 }), + }); + }); + + // Go to Explore and choose Loki data source + await page.goto('/explore'); + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + await page.getByRole('button', { name: dataSourceName }).click(); + + // Start in builder mode, click and choose query pattern + await page.getByTestId(selectors.components.QueryBuilder.queryPatterns).click(); + await page.getByRole('button', { name: 'Log query starters' }).click(); + await page.getByRole('button', { name: 'Use this query' }).first().click(); + await expect(page.getByText('No pipeline errors')).toBeVisible(); + await expect(page.getByText('{} | logfmt | __error__=``')).toBeVisible(); + + // Add operation + await page.getByRole('button', { name: 'Operations', exact: true }).click(); + await page.getByText('Range functions').click(); + await page.getByText('Rate', { exact: true }).click(); + await expect(page.getByText('rate({} | logfmt | __error__=`` [$__auto]')).toBeVisible(); + + // Check for expected error + await expect(page.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeVisible(); + + // Add labels to remove error + await dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.labelSelect).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.inputSelect).fill('instance'); + await page.keyboard.press('Enter'); + + const matchOperatorSelect = dashboardPage.getByGrafanaSelector( + selectors.components.QueryBuilder.matchOperatorSelect + ); + await expect(matchOperatorSelect).toBeVisible(); + await matchOperatorSelect.click({ force: true }); + + const matchOperatorInput = matchOperatorSelect.locator('div').locator('input'); + await matchOperatorInput.fill('=~'); + await page.keyboard.press('Enter'); + + const valueSelect = dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + await valueSelect.click(); + + const valueInput = valueSelect.locator('div').locator('input'); + await valueInput.fill('instance1'); + await page.keyboard.press('Enter'); + await valueInput.fill('instance2'); + await page.keyboard.press('Enter'); + + await expect(page.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeHidden(); + await expect(page.getByText(finalQuery)).toBeVisible(); + + // Change to code editor + await page.getByRole('radio', { name: 'Code' }).click(); + + // We need to test this manually because the final query is split into separate DOM elements + await expect(page.getByText('rate')).toBeVisible(); + await expect(page.getByText('instance1|instance2')).toBeVisible(); + await expect(page.getByText('logfmt')).toBeVisible(); + await expect(page.getByText('__error__')).toBeVisible(); + await expect(page.getByText('$__auto')).toBeVisible(); + + // Checks the explain mode toggle + await page.getByText('Explain').click(); + await expect(page.getByText('Fetch all log lines matching label filters.')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts b/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts new file mode 100644 index 00000000000..8495d43c07c --- /dev/null +++ b/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const dataSourceName = 'LokiEditor'; + +const lokiQueryResult = { + status: 'success', + results: { + A: { + status: 200, + frames: [ + { + schema: { + refId: 'A', + meta: { + typeVersion: [0, 0], + custom: { + frameType: 'LabeledTimeValues', + }, + stats: [ + { + displayName: 'Summary: bytes processed per second', + unit: 'Bps', + value: 223921, + }, + { + displayName: 'Summary: total bytes processed', + unit: 'decbytes', + value: 4156, + }, + { + displayName: 'Summary: exec time', + unit: 's', + value: 0.01856, + }, + ], + executedQueryString: 'Expr: {targetLabelName="targetLabelValue"}', + }, + fields: [ + { + name: 'labels', + type: 'other', + typeInfo: { + frame: 'json.RawMessage', + }, + }, + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + }, + { + name: 'Line', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + { + name: 'tsNs', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + { + name: 'id', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + ], + }, + data: { + values: [ + [ + { + targetLabelName: 'targetLabelValue', + instance: 'server\\1', + job: '"grafana/data"', + nonIndexed: 'value', + place: 'moon', + re: 'one.two$three^four', + source: 'data', + }, + ], + [1700077283237], + [ + '{"_entry":"log text with ANSI \\u001b[31mpart of the text\\u001b[0m [149702545]","counter":"22292","float":"NaN","wave":-0.5877852522916832,"label":"val3","level":"info"}', + ], + ['1700077283237000000'], + ['1700077283237000000_9b025d35'], + ], + }, + }, + ], + }, + }, +}; + +test.use({ + featureToggles: { + logsExploreTableVisualisation: true, + }, +}); + +test.describe( + 'Loki Query Editor', + { + tag: ['@various'], + }, + () => { + test('Should be able to add explore table to dashboard', async ({ + createDataSource, + page, + dashboardPage, + selectors, + }) => { + await createDataSource({ type: 'loki', name: dataSourceName }); + // Mock API responses + await page.route(/labels\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance', 'job', 'source'] }), + }); + }); + + await page.route(/series\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: [{ instance: 'instance1' }] }), + }); + }); + + await page.route(/\/api\/ds\/query\?ds_type=loki/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(lokiQueryResult), + }); + }); + + // Go to Explore and choose Loki data source + await page.goto('/explore'); + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + await page.getByRole('button', { name: dataSourceName }).click(); + + await page.getByRole('radio', { name: 'Code' }).click(); + + // Write a simple query + const queryField = page.getByTestId(selectors.components.QueryField.container).locator('textarea'); + await queryField.fill('query{instance="instance1"}'); + + // Submit the query with Shift+Enter + await queryField.press('Shift+Enter'); + + // Assert the no-data message is not visible + await expect(page.locator('[data-testid="explore-no-data"]')).toBeHidden(); + + // Click on the table toggle + await page.getByRole('radio', { name: 'Table' }).click({ force: true }); + + // One row with two cells initially + const cells = page.locator('[role="cell"]'); + await expect(cells).toHaveCount(2); + + // Find and click on the targetLabelName label + await page.getByText('targetLabelName', { exact: true }).click(); + + // Now we should have a row with 3 columns + await expect(cells).toHaveCount(3); + // And a value of "targetLabelValue" + await expect(cells.getByText('targetLabelValue')).toBeVisible(); + + await page.getByLabel('Add', { exact: true }).click(); + + await page.getByLabel('Add to dashboard').click(); + + const addPanelToDashboardButton = page.getByText('Add panel to dashboard'); + await expect(addPanelToDashboardButton).toBeVisible(); + + await page.getByText('Open dashboard').click(); + + // Check the panel is visible + const panel = page.locator('[data-viz-panel-key="panel-1"]'); + await expect(panel).toBeVisible(); + + // Check the table cells in the panel + const panelCells = panel.locator('[role="table"] [role="cell"]'); + // Should have 3 columns + await expect(panelCells).toHaveCount(3); + + // Cells contain strings found in log line + await expect(page.getByText('"wave":-0.5877852522916832')).toBeVisible(); + + // Column has correct value of "targetLabelValue" + await expect(panel.locator('[role="table"] [role="cell"]').filter({ hasText: 'targetLabelValue' })).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/migrate-to-cloud.spec.ts b/e2e-playwright/various-suite/migrate-to-cloud.spec.ts new file mode 100644 index 00000000000..d2712554ea1 --- /dev/null +++ b/e2e-playwright/various-suite/migrate-to-cloud.spec.ts @@ -0,0 +1,393 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Migrate to Cloud (On-prem)', + { + tag: ['@various', '@wip'], + }, + () => { + test.describe('with mocked calls to the API backend', () => { + test.afterEach(async ({ page }) => { + const disconnectButton = page.getByTestId('migrate-to-cloud-summary-disconnect-button'); + await expect(disconnectButton).toBeVisible(); + await disconnectButton.click(); + }); + + const SESSION_UID = 'fehq6hqd246iox'; + const SNAPSHOT_UID1 = 'cehq6vdjqbqbkx'; + + const SNAPSHOT_RESULTS = [ + { name: 'FolderA', type: 'FOLDER', refId: 'ref-id-folder-a', parentName: 'General' }, + { name: 'FolderB', type: 'FOLDER', refId: 'ref-id-folder-b', parentName: 'General' }, + { name: 'Prometheus', type: 'DATASOURCE', refId: 'prometheus' }, + { name: 'Postgres', type: 'DATASOURCE', refId: 'postgres' }, + { name: 'Loki', type: 'DATASOURCE', refId: 'loki' }, + { name: 'Alert Rule A', type: 'ALERT_RULE', refId: 'alert-rule-a', parentName: 'FolderA' }, + { name: 'Alert Rule B', type: 'ALERT_RULE', refId: 'alert-rule-b', parentName: 'FolderB' }, + { name: 'Alert Rule C', type: 'ALERT_RULE', refId: 'alert-rule-c', parentName: 'FolderB' }, + { name: 'Alert Rule Group A', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-a', parentName: 'FolderA' }, + { name: 'Alert Rule Group B', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-b', parentName: 'FolderB' }, + { name: 'Contact Point A', type: 'CONTACT_POINT', refId: 'contact-point-a' }, + { name: 'Contact Point B', type: 'CONTACT_POINT', refId: 'contact-point-b' }, + { name: 'Contact Point C', type: 'CONTACT_POINT', refId: 'contact-point-c' }, + { name: 'Notification Policy A', type: 'NOTIFICATION_POLICY', refId: 'notification-policy-a' }, + { name: 'Notification Template A', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-a' }, + { name: 'Notification Template B', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-b' }, + { name: 'Notification Template C', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-c' }, + { name: 'Plugin A', type: 'PLUGIN', refId: 'plugin-a' }, + { name: 'Plugin B', type: 'PLUGIN', refId: 'plugin-b' }, + { name: 'Plugin C', type: 'PLUGIN', refId: 'plugin-c' }, + { name: 'Mute Timing A', type: 'MUTE_TIMING', refId: 'mute-timing-a' }, + { name: 'Mute Timing B', type: 'MUTE_TIMING', refId: 'mute-timing-b' }, + ]; + + const MIGRATION_SESSION = { + uid: SESSION_UID, + slug: 'test-slug', + created: '2025-04-02T21:36:08+02:00', + updated: '2025-04-02T21:36:08+02:00', + }; + + const STATS = { + types: SNAPSHOT_RESULTS.reduce>((acc, r) => { + acc[r.type] = (acc[r.type] || 0) + 1; + return acc; + }, {}), + statuses: { + PENDING: SNAPSHOT_RESULTS.length, + }, + total: SNAPSHOT_RESULTS.length, + }; + + // TODO: fix the test. It makes it most of the way through. Probably a network mock issue. + test.skip('creates and uploads a snapshot successfully', async ({ page }) => { + // Visit the migrate to cloud onprem page + await page.goto('/admin/migrate-to-cloud'); + + // Open the connect modal and enter the token + const connectButton = page.getByTestId('migrate-to-cloud-connect-session-modal-button'); + await expect(connectButton).toBeVisible(); + await connectButton.click(); + + const tokenInput = page.getByTestId('migrate-to-cloud-connect-session-modal-token-input'); + await expect(tokenInput).toBeVisible(); + await tokenInput.focus(); + await tokenInput.fill('test'); + + // Mock API responses + await page.route(/api\/cloudmigration\/migration/, async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MIGRATION_SESSION), + }); + } else if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + sessions: [MIGRATION_SESSION], + }), + }); + } + }); + + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [], + }), + }); + }); + + // Click the connect button to create the token + const connectSessionButton = page.getByTestId('migrate-to-cloud-connect-session-modal-connect-button'); + await expect(connectSessionButton).toBeVisible(); + await connectSessionButton.click(); + + // Check the 'Include all' resources checkbox + const includeAllCheckbox = page.getByTestId( + 'migrate-to-cloud-configure-snapshot-checkbox-resource-include-all' + ); + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + // And validate that all resources are indeed checked + for (const resourceType of [ + 'alert_rule', + 'alert_rule_group', + 'contact_point', + 'dashboard', + 'datasource', + 'folder', + 'library_element', + 'mute_timing', + 'notification_policy', + 'notification_template', + 'plugin', + ]) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).toBeChecked(); + } + + // Remove one of the resources that has dependencies + // Mute Timings are dependencies of Alert Rules, which are dependencies of Alert Rule Groups + const muteTimingCheckbox = page.getByTestId( + 'migrate-to-cloud-configure-snapshot-checkbox-resource-mute_timing' + ); + await muteTimingCheckbox.uncheck({ force: true }); + await expect(muteTimingCheckbox).not.toBeChecked(); + + // Validate that those resources are now unchecked + for (const resourceType of ['alert_rule', 'alert_rule_group', 'include-all']) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).not.toBeChecked(); + } + + // Check everything again because we can + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + // Validate that those resources are now checked again + for (const resourceType of ['alert_rule', 'alert_rule_group', 'mute_timing']) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).toBeChecked(); + } + + // Mock snapshot creation + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + }), + }); + }); + + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [ + { + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'CREATING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + }, + ], + }), + }); + }); + + let getSnapshotCalled = false; + await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\?resultPage=1&resultLimit=50/, + async (route) => { + if (!getSnapshotCalled) { + getSnapshotCalled = true; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'CREATING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: [], + stats: { + types: {}, + statuses: {}, + total: 0, + }, + }), + }); + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'PENDING_UPLOAD', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })), + stats: STATS, + }), + }); + } + } + ); + + // Build the snapshot + const buildSnapshotButton = page.getByTestId('migrate-to-cloud-configure-snapshot-build-snapshot-button'); + await expect(buildSnapshotButton).toBeVisible(); + await buildSnapshotButton.click(); + + // Mock upload + const uploadSnapshot = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\/upload/, + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true }), + }); + } + ); + + // Upload the snapshot + const uploadButton = page.getByTestId('migrate-to-cloud-summary-upload-snapshot-button'); + await expect(uploadButton).toBeVisible(); + + await uploadButton.focus(); + await uploadButton.click({ force: true }); + + // Mock uploading status + const getSnapshotListUploading = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [ + { + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'UPLOADING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + }, + ], + }), + }); + } + ); + + // Simulate the snapshot being uploaded + let getSnapshotUploadingCalls = 0; + const getSnapshotUploading = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\?resultPage=1&resultLimit=50/, + async (route) => { + if (getSnapshotUploadingCalls <= 1) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: getSnapshotUploadingCalls === 1 ? 'PROCESSING' : 'UPLOADING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })), + stats: STATS, + }), + }); + getSnapshotUploadingCalls++; + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'FINISHED', + created: '2025-03-27T12:00:00Z', + finished: '2025-03-27T12:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'OK' })), + stats: { + types: STATS.types, + statuses: SNAPSHOT_RESULTS.reduce( + (acc, r) => { + const status = (r as { status?: string }).status || 'UNKNOWN'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, + {} as Record + ), + total: SNAPSHOT_RESULTS.length, + }, + }), + }); + } + } + ); + + await uploadSnapshot; + await getSnapshotListUploading; + await getSnapshotUploading; + + // At least some of the items are marked with "Uploaded to cloud" status + await expect(page.getByText('Uploaded to cloud')).toBeVisible(); + + // We can now reconfigure the snapshot + const reconfigureButton = page.getByTestId('migrate-to-cloud-summary-reconfigure-snapshot-button'); + await expect(reconfigureButton).toBeVisible(); + await reconfigureButton.click(); + + // Check the 'Include all' resources checkbox + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + }); + }); + + test.describe('with a fake GMS backend implementation', () => { + test.afterEach(async ({ page }) => { + const disconnectButton = page.getByTestId('migrate-to-cloud-summary-disconnect-button'); + await expect(disconnectButton).toBeVisible(); + await disconnectButton.click(); + }); + + // Manually crafted base64 token for testing, does not contain any sensitive data + const TEST_TOKEN = + 'eyJUb2tlbiI6ImdsY19kZXZfZXlKdklqb2lNVEl6TkNJc0ltNGlPaUpuY21GbVlXNWhMV05zYjNWa0xXMXBaM0poZEdsdmJuTXRNVEl6TkNJc0ltc2lPaUowWlhOMElpd2liU0k2ZXlKeUlqb2laR1YyTFhWekxXTmxiblJ5WVd3aWZYMEsiLCJJbnN0YW5jZSI6eyJTdGFja0lEIjoxMjM0LCJTbHVnIjoidGVzdC1zbHVnIiwiUmVnaW9uU2x1ZyI6ImRldi11cy1jZW50cmFsIiwiQ2x1c3RlclNsdWciOiJkZXYtdXMtY2VudHJhbC0wIn19Cg=='; + + test('creates a snapshot successfully', async ({ page }) => { + // Visit the migrate to cloud onprem page + await page.goto('/admin/migrate-to-cloud'); + + // Open the connect modal and enter the token + const connectButton = page.getByTestId('migrate-to-cloud-connect-session-modal-button'); + await expect(connectButton).toBeVisible(); + await connectButton.click(); + + const tokenInput = page.getByTestId('migrate-to-cloud-connect-session-modal-token-input'); + await expect(tokenInput).toBeVisible(); + await tokenInput.focus(); + await tokenInput.fill(TEST_TOKEN); + + // Click the connect button to create the token + const connectSessionButton = page.getByTestId('migrate-to-cloud-connect-session-modal-connect-button'); + await expect(connectSessionButton).toBeVisible(); + await connectSessionButton.click(); + + // Build the snapshot + const buildSnapshotButton = page.getByTestId('migrate-to-cloud-configure-snapshot-build-snapshot-button'); + await expect(buildSnapshotButton).toBeVisible(); + await buildSnapshotButton.click(); + + // And the rebuild button should be visible + const reconfigureButton = page.getByTestId('migrate-to-cloud-summary-reconfigure-snapshot-button'); + await expect(reconfigureButton).toBeVisible(); + + // We don't upload the snapshot yet because we need to create a mock server to validate the uploaded items, + // similarly to what the SMTP (tester) server does + }); + }); + } +); diff --git a/e2e-playwright/various-suite/navigation.spec.ts b/e2e-playwright/various-suite/navigation.spec.ts new file mode 100644 index 00000000000..24defee5d6a --- /dev/null +++ b/e2e-playwright/various-suite/navigation.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Docked Navigation', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + // This is a breakpoint where the mega menu can be docked (and docked is the default state) + await page.setViewportSize({ width: 1280, height: 800 }); + }); + + test('should remain un-docked when reloading the page', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + + // Reload the page + await page.reload(); + await expect(navMenu).toBeHidden(); + }); + + test('Can re-dock after undock', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + const openMenuButton = page.getByRole('button', { name: 'Open menu' }); + await openMenuButton.click(); + + const dockButton = page.getByRole('button', { name: 'Dock menu' }); + await dockButton.click(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + }); + + test('should remain in same state when navigating to another page', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + // Navigate + const openMenuButton = page.getByRole('button', { name: 'Open menu' }); + await openMenuButton.click(); + + const administrationLink = page.getByRole('link', { name: 'Administration' }); + await administrationLink.click(); + + // Still undocked + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + + // Dock the menu + await openMenuButton.click(); + const dockButton = page.getByRole('button', { name: 'Dock menu' }); + await dockButton.click(); + + // Navigate + const usersLink = page.getByRole('link', { name: 'Users' }); + await usersLink.click(); + + // Still docked + await expect(navMenu).toBeVisible(); + }); + + test('should undock on smaller viewport sizes', async ({ page, selectors }) => { + await page.setViewportSize({ width: 1120, height: 1080 }); + await page.reload(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/pie-chart.spec.ts b/e2e-playwright/various-suite/pie-chart.spec.ts new file mode 100644 index 00000000000..6ead086de5b --- /dev/null +++ b/e2e-playwright/various-suite/pie-chart.spec.ts @@ -0,0 +1,21 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Pie Chart Panel', + { + tag: ['@various'], + }, + () => { + test('Pie Chart rendering e2e tests', async ({ page }) => { + // Open Panel Tests - Pie Chart + await page.goto('/d/lVE-2YFMz/panel-tests-pie-chart'); + + // Check that there are 5 pie chart slices + const pieChartSlices = page.locator( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ); + await expect(pieChartSlices).toHaveCount(5); + }); + } +); diff --git a/e2e-playwright/various-suite/prometheus-annotations.spec.ts b/e2e-playwright/various-suite/prometheus-annotations.spec.ts new file mode 100644 index 00000000000..a14d054cab6 --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-annotations.spec.ts @@ -0,0 +1,137 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { addDashboard } from '../utils/dashboard-helpers'; +import { getResources } from '../utils/prometheus-helpers'; + +test.describe( + 'Prometheus annotations', + { + tag: ['@various'], + }, + () => { + const DATASOURCE_NAME = 'aprometheusAnnotationDS'; + + test('should navigate to variable query editor', async ({ page, selectors, createDataSourceConfigPage }) => { + const annotationName = 'promAnnotation'; + + await createDataSourceConfigPage({ type: 'prometheus', name: DATASOURCE_NAME }); + + // Add a new dashboard + await addDashboard(page); + + // Navigate to annotations + await navigateToAnnotations(page, selectors); + + // Add Prometheus annotation + await addPrometheusAnnotation(page, selectors, annotationName); + + // Open metrics browser + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + // Type in the metric selector + const selectMetricInput = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetricInput).toBeVisible(); + await selectMetricInput.fill('met'); + + // Select metric1 from the list + const metricList = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList + ); + await expect(metricList).toBeVisible(); + const metric1Option = metricList.getByText('metric1'); + await metric1Option.click(); + + // Use the query + const useQueryButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQueryButton).toBeVisible(); + await useQueryButton.click(); + + // Verify query field contains metric1 + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + await expect(queryField).toContainText('metric1'); + + // Check for other parts of the annotations + // Min step + const minStepInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.minStep); + await expect(minStepInput).toBeVisible(); + + // Title + const titleInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.title); + await titleInput.scrollIntoViewIfNeeded(); + await expect(titleInput).toBeVisible(); + + // Tags + const tagsInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.tags); + await tagsInput.scrollIntoViewIfNeeded(); + await expect(tagsInput).toBeVisible(); + + // Text + const textInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.text); + await textInput.scrollIntoViewIfNeeded(); + await expect(textInput).toBeVisible(); + + // Series value as timestamp + const seriesValueSwitch = page.getByTestId( + selectors.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp + ); + await seriesValueSwitch.scrollIntoViewIfNeeded(); + await expect(seriesValueSwitch).toBeVisible(); + + // Go back to dashboard + const backButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.backToDashboardButton); + await expect(backButton).toBeVisible(); + await backButton.click(); + + // Check that annotation exists + await expect(page.getByText(annotationName)).toBeVisible(); + }); + + /** + * Click dashboard settings and then the annotations tab + */ + async function navigateToAnnotations(page: Page, selectors: E2ESelectorGroups) { + const editButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + const settingsButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.settingsButton); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + const annotationsTab = page.getByTestId(selectors.components.Tab.title('Annotations')); + await annotationsTab.click(); + } + + async function addPrometheusAnnotation(page: Page, selectors: E2ESelectorGroups, annotationName: string) { + const addAnnotationButton = page.getByTestId( + selectors.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2 + ); + await addAnnotationButton.click(); + + await getResources(page); + + const nameInput = page.getByTestId(selectors.pages.Dashboard.Settings.Annotations.Settings.name); + await nameInput.clear(); + await nameInput.fill(annotationName); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(DATASOURCE_NAME); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + } + } +); diff --git a/e2e-playwright/various-suite/prometheus-config.spec.ts b/e2e-playwright/various-suite/prometheus-config.spec.ts new file mode 100644 index 00000000000..a92c9b7c2bd --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-config.spec.ts @@ -0,0 +1,193 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// Todo: Fix datasource creation +test.describe.skip( + 'Prometheus config', + { + tag: ['@various'], + }, + () => { + const DATASOURCE_ID = 'Prometheus'; + const DATASOURCE_TYPED_NAME = 'PrometheusDatasourceInstance'; + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + // Navigate to add data source page + await page.goto('/datasources/new'); + + // Select the Prometheus data source + const prometheusPlugin = page.getByRole('button', { name: DATASOURCE_ID }); + await prometheusPlugin.scrollIntoViewIfNeeded(); + await expect(prometheusPlugin).toBeVisible(); + await prometheusPlugin.click(); + }); + + test('should have the following components: connection settings, managed alerts, scrape interval, query timeout, default editor, disable metric lookup, prometheus type, cache level, incremental querying, disable recording rules, custom query parameters, http method', async ({ + page, + selectors, + }) => { + // connection settings + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await expect(connectionSettings).toBeVisible(); + + // managed alerts + const manageAlerts = page.locator(`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`); + await manageAlerts.scrollIntoViewIfNeeded(); + await expect(manageAlerts).toBeVisible(); + + // scrape interval + const scrapeInterval = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.scrapeInterval); + await scrapeInterval.scrollIntoViewIfNeeded(); + await expect(scrapeInterval).toBeVisible(); + + // query timeout + const queryTimeout = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.queryTimeout); + await queryTimeout.scrollIntoViewIfNeeded(); + await expect(queryTimeout).toBeVisible(); + + // default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + + // disable metric lookup + const disableMetricLookup = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}` + ); + await disableMetricLookup.scrollIntoViewIfNeeded(); + await expect(disableMetricLookup).toBeVisible(); + + // prometheus type + const prometheusType = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.prometheusType); + await prometheusType.scrollIntoViewIfNeeded(); + await expect(prometheusType).toBeVisible(); + + // cache level + const cacheLevel = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.cacheLevel); + await cacheLevel.scrollIntoViewIfNeeded(); + await expect(cacheLevel).toBeVisible(); + + // incremental querying + const incrementalQuerying = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}` + ); + await incrementalQuerying.scrollIntoViewIfNeeded(); + await expect(incrementalQuerying).toBeVisible(); + + // disable recording rules + const disableRecordingRules = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}` + ); + await disableRecordingRules.scrollIntoViewIfNeeded(); + await expect(disableRecordingRules).toBeVisible(); + + // custom query parameters + const customQueryParameters = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.customQueryParameters + ); + await customQueryParameters.scrollIntoViewIfNeeded(); + await expect(customQueryParameters).toBeVisible(); + + // http method + const httpMethod = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.httpMethod); + await httpMethod.scrollIntoViewIfNeeded(); + await expect(httpMethod).toBeVisible(); + }); + + test('should save the default editor when navigating to explore', async ({ page, selectors }) => { + // Click on default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + await defaultEditor.click(); + + // Select 'Builder' option + await selectOption(page, 'Builder'); + + // Set connection settings + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.fill('http://prom-url:9090'); + + // Set data source name + const nameInput = page.getByTestId(selectors.pages.DataSource.name); + await nameInput.clear(); + await nameInput.fill(DATASOURCE_TYPED_NAME); + + // Save and test + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + + // Navigate to explore + await page.goto('/explore'); + + // Select the data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + // Type the data source name and press enter + await page.keyboard.type(DATASOURCE_TYPED_NAME); + await page.keyboard.press('Enter'); + + // Verify the builder metric select is visible + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + }); + + test('should allow a user to add the version when the Prom type is selected', async ({ page, selectors }) => { + // Click on prometheus type + const prometheusType = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.prometheusType); + await prometheusType.scrollIntoViewIfNeeded(); + await expect(prometheusType).toBeVisible(); + await prometheusType.click(); + + // Select 'Prometheus' option + await selectOption(page, 'Prometheus'); + + // Verify prometheus version is visible + const prometheusVersion = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.prometheusVersion + ); + await prometheusVersion.scrollIntoViewIfNeeded(); + await expect(prometheusVersion).toBeVisible(); + }); + + test('should have a cache level component', async ({ page, selectors }) => { + // Verify cache level is visible + const cacheLevel = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.cacheLevel); + await cacheLevel.scrollIntoViewIfNeeded(); + await expect(cacheLevel).toBeVisible(); + }); + + test('should allow a user to select a query overlap window when incremental querying is selected', async ({ + page, + selectors, + }) => { + // Check the incremental querying checkbox + const incrementalQuerying = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}` + ); + await incrementalQuerying.scrollIntoViewIfNeeded(); + await expect(incrementalQuerying).toBeVisible(); + await incrementalQuerying.check({ force: true }); + + // Verify query overlap window is visible + const queryOverlapWindow = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.queryOverlapWindow + ); + await queryOverlapWindow.scrollIntoViewIfNeeded(); + await expect(queryOverlapWindow).toBeVisible(); + }); + } +); + +async function selectOption(page, option) { + const optionElement = page.getByRole('option').filter({ hasText: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/prometheus-editor.spec.ts b/e2e-playwright/various-suite/prometheus-editor.spec.ts new file mode 100644 index 00000000000..2aafd3cb9aa --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-editor.spec.ts @@ -0,0 +1,287 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { getResources } from '../utils/prometheus-helpers'; + +// TODO: fix some tests. Race conditions with other tests in the file cause some to fail. +test.describe.skip( + 'Prometheus query editor', + { + tag: ['@various', '@wip'], + }, + () => { + const DATASOURCE_ID = 'Prometheus'; + + type EditorType = 'Code' | 'Builder'; + + /** + * Create and save a Prometheus data source, navigate to code or builder + */ + async function navigateToEditor(page: Page, selectors: E2ESelectorGroups, editorType: string, name: string) { + // Navigate to add data source page + await page.goto('/datasources/new'); + + // Select the Prometheus data source + const prometheusPlugin = page.getByRole('button', { name: DATASOURCE_ID }); + await prometheusPlugin.scrollIntoViewIfNeeded(); + await expect(prometheusPlugin).toBeVisible(); + await prometheusPlugin.click(); + + // Choose default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + await defaultEditor.click(); + + await selectOption(page, editorType, selectors); + + // Add URL for DS to save without error + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.fill('http://prom-url:9090'); + + // Name the DS + const nameInput = page.getByTestId(selectors.pages.DataSource.name); + await nameInput.clear(); + await nameInput.fill(name); + + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + + // Visit explore + await page.goto('/explore'); + + // Choose the right DS + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByRole('button', { name: `${name} ${DATASOURCE_ID}`, exact: true }); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + } + + test('should have a kickstart component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const queryPatterns = page.getByTestId(selectors.components.QueryBuilder.queryPatterns); + await queryPatterns.scrollIntoViewIfNeeded(); + await expect(queryPatterns).toBeVisible(); + }); + + test('should have an explain component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const explain = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.explain); + await explain.scrollIntoViewIfNeeded(); + await expect(explain).toBeVisible(); + }); + + test('should have an editor toggle component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const editorToggle = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.editorToggle); + await editorToggle.scrollIntoViewIfNeeded(); + await expect(editorToggle).toBeVisible(); + }); + + test('should have an options component with legend, format, step, type and exemplars', async ({ + page, + selectors, + }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + // Open options + const options = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.options); + await options.scrollIntoViewIfNeeded(); + await expect(options).toBeVisible(); + await options.click(); + + // Check options + const legend = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.legend); + await legend.scrollIntoViewIfNeeded(); + await expect(legend).toBeVisible(); + + const format = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.format); + await format.scrollIntoViewIfNeeded(); + await expect(format).toBeVisible(); + + const step = page.locator('[data-test-id="prometheus-step"]'); + await step.scrollIntoViewIfNeeded(); + await expect(step).toBeVisible(); + + const type = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.type); + await type.scrollIntoViewIfNeeded(); + await expect(type).toBeVisible(); + + const exemplars = page.getByTestId('prometheus-exemplars'); + await exemplars.scrollIntoViewIfNeeded(); + await expect(exemplars).toBeVisible(); + }); + + test.describe('Code editor', () => { + test('navigates to the code editor with editor type as code', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + }); + + test('navigates to the code editor and opens the metrics browser with metric search, labels, label values, and all components', async ({ + page, + selectors, + }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + + await getResources(page); + + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + const selectMetric = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetric).toBeVisible(); + + const labelNamesFilter = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelNamesFilter + ); + await expect(labelNamesFilter).toBeVisible(); + + const labelValuesFilter = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter + ); + await expect(labelValuesFilter).toBeVisible(); + + const useQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQuery).toBeVisible(); + + const useAsRateQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useAsRateQuery + ); + await expect(useAsRateQuery).toBeVisible(); + + const validateSelector = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.validateSelector + ); + await expect(validateSelector).toBeVisible(); + + const clear = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.clear + ); + await expect(clear).toBeVisible(); + }); + + test('selects a metric in the metrics browser and uses the query', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + + await getResources(page); + + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + const selectMetric = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetric).toBeVisible(); + await selectMetric.fill('met'); + + const metricList = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList + ); + await expect(metricList).toBeVisible(); + + const metricOption = metricList.getByText('metric1'); + await expect(metricOption).toBeVisible(); + await metricOption.click(); + + const useQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQuery).toBeVisible(); + await useQuery.click(); + + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + await expect(queryField).toContainText('metric1'); + }); + }); + + test.describe('Query builder', () => { + test('navigates to the query builder with editor type as code', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + }); + + test('the query builder contains metric select, label filters and operations', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + + const labelSelect = page.getByTestId(selectors.components.QueryBuilder.labelSelect); + await expect(labelSelect).toBeVisible(); + + const matchOperatorSelect = page.getByTestId(selectors.components.QueryBuilder.matchOperatorSelect); + await expect(matchOperatorSelect).toBeVisible(); + + const valueSelect = page.getByTestId(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + }); + + test('can select a metric and provide a hint', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + + await page.getByText('metric1').click(); + + const hints = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.builder.hints); + await expect(hints).toContainText('hint: add rate'); + }); + + test('should have the metrics explorer opened via the metric select', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + + await selectOption(page, 'Metrics explorer', selectors); + + const metricsExplorer = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer + ); + await expect(metricsExplorer).toBeVisible(); + }); + }); + } +); + +async function selectOption(page: Page, option: string, selectors: E2ESelectorGroups) { + const optionElement = page.getByTestId(selectors.components.Select.option).filter({ hasText: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts new file mode 100644 index 00000000000..a2b4dfcd672 --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts @@ -0,0 +1,171 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { addDashboard } from '../utils/dashboard-helpers'; +import { getResources } from '../utils/prometheus-helpers'; + +test.describe.skip( + 'Prometheus variable query editor', + { + tag: ['@various', '@wip'], + }, + () => { + const DATASOURCE_NAME = 'prometheusVariableDS'; + + /** + * Click dashboard settings and then the variables tab + */ + async function navigateToVariables(page, selectors) { + const editButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + const settingsButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.settingsButton); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + const variablesTab = page.getByTestId(selectors.components.Tab.title('Variables')); + await variablesTab.click(); + } + + /** + * Begin the process of adding a query type variable for a Prometheus data source + */ + async function addPrometheusQueryVariable(page, selectors, variableName) { + const addVariableButton = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2); + await addVariableButton.click(); + + const nameInput = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2); + await nameInput.clear(); + await nameInput.fill(variableName); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(DATASOURCE_NAME); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + + await getResources(page); + } + + /** + * Create a Prometheus variable and navigate to the query editor to check that it is available to use. + */ + async function variableFlowToQueryEditor(page, selectors, variableName, queryType) { + await addDashboard(page); + await navigateToVariables(page, selectors); + await addPrometheusQueryVariable(page, selectors, variableName); + + // Select query type + const queryTypeSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.variableQueryEditor.queryType + ); + await queryTypeSelect.click(); + await selectOption(page, queryType); + + // Apply the variable + const applyButton = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton); + await applyButton.click(); + + // Close to return to dashboard + const backToDashboardButton = page.getByTestId( + selectors.components.NavToolbar.editDashboard.backToDashboardButton + ); + await expect(backToDashboardButton).toBeVisible(); + await backToDashboardButton.click(); + + // Add visualization + const createNewPanelButton = page.getByTestId(selectors.pages.AddDashboard.itemButton('Create new panel button')); + await expect(createNewPanelButton).toBeVisible(); + await createNewPanelButton.click(); + + // Close the data source picker modal + const closeButton = page.getByRole('button', { name: 'Close menu' }); + await closeButton.click({ force: true }); + + // Select prom data source from the data source list + const dataSourcePickerInput = page.getByTestId(selectors.components.DataSourcePicker.inputV2); + await dataSourcePickerInput.click(); + await dataSourcePickerInput.fill(DATASOURCE_NAME); + await page.keyboard.press('Enter'); + + // Confirm the variable exists in the correct input + switch (queryType) { + case 'Label names': + const labelSelect = page.getByTestId(selectors.components.QueryBuilder.labelSelect); + await expect(labelSelect).toBeVisible(); + await labelSelect.click(); + await selectOption(page, variableName); + break; + case 'Label values': + const valueSelect = page.getByTestId(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + await valueSelect.click(); + await selectOption(page, variableName); + break; + case 'Metrics': + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + await selectOption(page, variableName); + break; + default: + // do nothing + break; + } + } + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + await createDataSourceConfigPage({ type: 'prometheus', name: DATASOURCE_NAME }); + }); + + test('should navigate to variable query editor', async ({ page, selectors }) => { + await addDashboard(page); + await navigateToVariables(page, selectors); + }); + + test('should select a query type for a Prometheus variable query', async ({ page, selectors }) => { + await addDashboard(page); + await navigateToVariables(page, selectors); + await addPrometheusQueryVariable(page, selectors, 'labelsVariable'); + + // Select query type + const queryTypeSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.variableQueryEditor.queryType + ); + await queryTypeSelect.click(); + await selectOption(page, 'Label names'); + }); + + test('should create a label names variable that is selectable in the label select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'labelnames', 'Label names'); + }); + + test('should create a label values variable that is selectable in the label values select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'labelvalues', 'Label values'); + }); + + test('should create a metric names variable that is selectable in the metric select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'metrics', 'Metrics'); + }); + } +); + +async function selectOption(page, option) { + const optionElement = page.getByRole('option', { name: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/query-editor.spec.ts b/e2e-playwright/various-suite/query-editor.spec.ts new file mode 100644 index 00000000000..ee1e4e3e372 --- /dev/null +++ b/e2e-playwright/various-suite/query-editor.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Query editor', + { + tag: ['@various'], + }, + () => { + test('Undo should work in query editor for prometheus', async ({ page }) => { + // Visit the explore page + await page.goto('/explore'); + + // Click on the data source picker + const dataSourcePicker = page.getByTestId('data-testid Data source picker select container'); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + // Select the prometheus data source + const prometheusOption = page.getByText('gdev-prometheus'); + await expect(prometheusOption).toBeVisible(); + await prometheusOption.click(); + + const queryText = `rate(http_requests_total{job="grafana"}[5m])`; + + // Click on the Code radio button + const codeRadioButton = page.getByRole('radio', { name: 'Code' }); + await expect(codeRadioButton).toBeVisible(); + await codeRadioButton.click(); + + // Wait for Monaco editor to load + await page.waitForSelector('.monaco-editor'); + + // Type the query text and then backspace + const queryField = page.locator('.monaco-editor textarea'); + await queryField.fill(queryText); + await queryField.press('Backspace'); + + // Verify the text is truncated + await expect(page.getByText(queryText.slice(0, -1))).toBeVisible(); + + // Use undo (Ctrl+Z) + await queryField.press('Control+z'); + + // Verify the full query text is restored + await expect(page.getByText(queryText)).toBeVisible(); + + // Verify no error alerts are visible + const errorAlert = page.getByTestId('alert-error'); + await expect(errorAlert).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/return-to-previous.spec.ts b/e2e-playwright/various-suite/return-to-previous.spec.ts new file mode 100644 index 00000000000..9c6b9f7748e --- /dev/null +++ b/e2e-playwright/various-suite/return-to-previous.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'ReturnToPrevious button', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page, selectors }) => { + // Navigate to alerting list + await page.goto('/alerting/list'); + + // Click on the first group toggle to expand it + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.first().click(); + + // Click on the toggle to expand the content + const toggle = page.getByTestId(selectors.components.AlertRules.toggle); + await toggle.click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // Store the alert rule URL for later comparison + const alertRuleUrl = page.url(); + + // Click on "View panel" link + const viewPanelLink = page.getByRole('link', { name: 'View panel' }); + await viewPanelLink.click(); + + // Store the URL for use in tests + test.info().annotations.push({ + type: 'alertRuleUrl', + description: alertRuleUrl, + }); + }); + + test('should appear when changing context and go back to alert rule when clicking "Back"', async ({ + page, + selectors, + }) => { + // Check whether all elements of RTP are available + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeVisible(); + + const dismissButton = page.getByTestId(selectors.components.ReturnToPrevious.dismissButton); + await expect(dismissButton).toBeVisible(); + + const backButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(backButton).toBeVisible(); + + // Check that the button contains the expected text + await expect(backButton.getByText('Back to e2e-ReturnToPrevious-test')).toBeVisible(); + + // Click the back button + await backButton.click(); + + // Check whether the RTP button leads back to alert rule + const alertRuleUrl = test.info().annotations.find((a) => a.type === 'alertRuleUrl')?.description; + expect(page.url()).toBe(alertRuleUrl); + }); + + test('should disappear when clicking "Dismiss"', async ({ page, selectors }) => { + const dismissButton = page.getByTestId(selectors.components.ReturnToPrevious.dismissButton); + await expect(dismissButton).toBeVisible(); + await dismissButton.click(); + + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeHidden(); + }); + + test('should not persist when going back to the alert rule details view', async ({ page, selectors }) => { + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeVisible(); + + // Navigate back to alerting list + await page.goto('/alerting/list'); + + // Click on the first group toggle + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.first().click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // The ReturnToPrevious button should not exist + const rtpButtonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(rtpButtonGroup).toBeHidden(); + }); + + test('should override the button label and change the href when user changes alert rules', async ({ + page, + selectors, + }) => { + const backButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(backButton.getByText('Back to e2e-ReturnToPrevious-test')).toBeVisible(); + + // Navigate back to alerting list + await page.goto('/alerting/list'); + + // Click on the last group toggle (different alert rule) + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.last().click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // Store the second alert rule URL + const alertRule2Url = page.url(); + + // Click on "View panel" link + const viewPanelLink = page.getByRole('link', { name: 'View panel' }); + await viewPanelLink.click(); + + // Check that the button now shows the new alert rule name + const newBackButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(newBackButton.getByText('Back to e2e-ReturnToPrevious-test-2')).toBeVisible(); + + // Click the back button + await newBackButton.click(); + + // The ReturnToPrevious button should disappear + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeHidden(); + + // Check whether the RTP button leads back to the second alert rule + expect(page.url()).toBe(alertRule2Url); + }); + } +); diff --git a/e2e-playwright/various-suite/solo-route.spec.ts b/e2e-playwright/various-suite/solo-route.spec.ts new file mode 100644 index 00000000000..81c9e8baf7b --- /dev/null +++ b/e2e-playwright/various-suite/solo-route.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Solo Route', + { + tag: ['@various'], + }, + () => { + test('Can view panels with shared queries in fullscreen', async ({ page, selectors }) => { + // open Panel Tests - Bar Gauge + const soloPanelUrl = selectors.pages.SoloPanel.url('ZqZnVvFZz/datasource-tests-shared-queries?orgId=1&panelId=4'); + await page.goto(soloPanelUrl); + + // Check that there are 6 canvas elements + const canvasElements = page.locator('canvas'); + await expect(canvasElements).toHaveCount(6); + }); + + test('Can view solo panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'TkZXxlNG3/panel-tests-graph-ng?orgId=1&from=1699954597665&to=1699956397665&panelId=54&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Interpolation: Step before')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + + test('Can view solo repeated panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=A')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + + test('Can view solo in repeated row and panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server = B, pod = Rob')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/trace-view-scrolling.spec.ts b/e2e-playwright/various-suite/trace-view-scrolling.spec.ts new file mode 100644 index 00000000000..4343ed5eb13 --- /dev/null +++ b/e2e-playwright/various-suite/trace-view-scrolling.spec.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +// TODO for some reason, this test gives "connection refused" errors in CI +test.describe.skip( + 'Trace view', + { + tag: ['@various'], + }, + () => { + test('Can lazy load big traces', async ({ page, selectors }) => { + // Load the fixture data + const fixturePath = join(__dirname, '../fixtures/long-trace-response.json'); + const longTraceResponse = JSON.parse(readFileSync(fixturePath, 'utf8')); + + // Mock the API response + await page.route('*/**/api/traces/trace', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(longTraceResponse), + }); + }); + + // Navigate to Explore page + await page.goto(selectors.pages.Explore.url); + + // Select the Jaeger data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await dataSourcePicker.click(); + const datasourceList = page.getByTestId(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-jaeger').click(); + + // Check that gdev-jaeger is visible in the query editor + await expect(page.getByText('gdev-jaeger')).toBeVisible(); + + // Type the query + const queryField = page + .getByTestId(selectors.components.QueryField.container) + .locator('[contenteditable="true"]'); + await queryField.fill('trace'); + + // Use Shift+Enter to execute the query + await queryField.press('Shift+Enter'); + + // Get the initial count of span bars + const initialSpanBars = page.getByTestId(selectors.components.TraceViewer.spanBar); + const initialSpanBarCount = await initialSpanBars.count(); + + await initialSpanBars.last().scrollIntoViewIfNeeded(); + await expect + .poll(async () => await page.getByTestId(selectors.components.TraceViewer.spanBar).count()) + .toBeGreaterThan(initialSpanBarCount); + }); + } +); diff --git a/e2e-playwright/various-suite/verify-i18n.spec.ts b/e2e-playwright/various-suite/verify-i18n.spec.ts new file mode 100644 index 00000000000..f6a2a00eb9b --- /dev/null +++ b/e2e-playwright/various-suite/verify-i18n.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const I18N_USER = 'i18n-test'; +const I18N_PASSWORD = 'i18n-test'; + +// Separate user to isolate changes from other tests +test.use({ + user: { + user: I18N_USER, + password: I18N_PASSWORD, + }, + storageState: { + cookies: [], + origins: [], + }, +}); + +test.describe( + 'Verify i18n', + { + tag: ['@various'], + }, + () => { + // Map between languages in the language picker and the corresponding translation of the 'Language' label + const languageMap: Record = { + Deutsch: 'Sprache', + Español: 'Idioma', + Français: 'Langue', + 'Português Brasileiro': 'Idioma', + '中文(简体)': '语言', + English: 'Language', + }; + + // Basic test which loops through the defined languages in the picker + // and verifies that the corresponding label is translated correctly + test('loads all the languages correctly', async ({ page, selectors, createUser }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(I18N_USER); + await page.getByTestId(selectors.pages.Login.password).fill(I18N_PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Navigate to profile page + await page.goto('/profile'); + + const LANGUAGE_SELECTOR = '[id="language-preference-select"]'; + + // Loop through each language and test the translation + for (const [language, label] of Object.entries(languageMap)) { + // Check that the language selector is not disabled + const languageSelector = page.locator(LANGUAGE_SELECTOR); + await expect(languageSelector).not.toBeDisabled(); + + // Click on the language selector + await languageSelector.click(); + + // Clear and type the language name + await languageSelector.clear(); + await languageSelector.fill(language); + + // Press down arrow and enter to select the option + await languageSelector.press('ArrowDown'); + await languageSelector.press('Enter'); + + // Click the save preferences button + const saveButton = page.getByTestId(selectors.components.UserProfile.preferencesSaveButton); + await saveButton.click(); + + // Check that the language label is visible + const languageLabel = page.locator('label').filter({ hasText: label }); + await expect(languageLabel).toBeVisible(); + + // Verify the language selector has the correct value + await expect(languageSelector).toHaveValue(language); + } + }); + } +); diff --git a/e2e-playwright/various-suite/visualization-suggestions.spec.ts b/e2e-playwright/various-suite/visualization-suggestions.spec.ts new file mode 100644 index 00000000000..964af821caa --- /dev/null +++ b/e2e-playwright/various-suite/visualization-suggestions.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Visualization suggestions', + { + tag: ['@various'], + }, + () => { + test('Should be shown and clickable', async ({ page, selectors, gotoPanelEditPage }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage + .getByGrafanaSelector(selectors.components.RadioButton.container) + .filter({ hasText: 'Suggestions' }) + .click(); + + // Verify we see suggestions + const lineChartCard = panelEditPage.getByGrafanaSelector( + selectors.components.VisualizationPreview.card('Line chart') + ); + await expect(lineChartCard).toBeVisible(); + + // Verify search works + const searchInput = page.getByPlaceholder('Search for...'); + await searchInput.fill('Table'); + + // Should no longer see line chart + await expect(lineChartCard).toBeHidden(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + + // Verify table header is visible + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header) + ).toBeVisible(); + }); + } +); diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index d2e1a074b27..469bdbfc9ed 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -5,11 +5,11 @@ import { v4 as uuidv4 } from 'uuid'; import { selectors as rawSelectors } from '@grafana/e2e-selectors'; import { selectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors'; +import { AzureQueryType } from '../../public/app/plugins/datasource/azuremonitor/types/query'; import { AzureMonitorDataSourceJsonData, AzureMonitorDataSourceSecureJsonData, - AzureQueryType, -} from '../../public/app/plugins/datasource/azuremonitor/types'; +} from '../../public/app/plugins/datasource/azuremonitor/types/types'; import { e2e } from '../utils'; const provisioningPath = `provisioning/datasources/azmonitor-ds.yaml`; diff --git a/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts b/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts deleted file mode 100644 index bb1ccee1f72..00000000000 --- a/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' }); - - await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts b/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts deleted file mode 100644 index db3ab66e985..00000000000 --- a/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'stackdriver' }); - - await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible(); - await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts b/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts deleted file mode 100644 index e834252208d..00000000000 --- a/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'cloudwatch' }); - - await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts b/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts deleted file mode 100644 index f1232c11d0f..00000000000 --- a/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'elasticsearch' }); - - await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/graphite/graphite.spec.ts b/e2e/plugin-e2e/graphite/graphite.spec.ts deleted file mode 100644 index 9bf58625c4b..00000000000 --- a/e2e/plugin-e2e/graphite/graphite.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'graphite' }); - - await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/influxdb/influxdb.spec.ts b/e2e/plugin-e2e/influxdb/influxdb.spec.ts deleted file mode 100644 index 1538690c9df..00000000000 --- a/e2e/plugin-e2e/influxdb/influxdb.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'influxdb' }); - - await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/jaeger/jaeger.spec.ts b/e2e/plugin-e2e/jaeger/jaeger.spec.ts deleted file mode 100644 index c7e1bc88951..00000000000 --- a/e2e/plugin-e2e/jaeger/jaeger.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'jaeger' }); - - await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mssql/mssql.spec.ts b/e2e/plugin-e2e/mssql/mssql.spec.ts deleted file mode 100644 index 125024935ea..00000000000 --- a/e2e/plugin-e2e/mssql/mssql.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'mssql' }); - - await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mysql/mysql.spec.ts b/e2e/plugin-e2e/mysql/mysql.spec.ts deleted file mode 100644 index 525e0074918..00000000000 --- a/e2e/plugin-e2e/mysql/mysql.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks'; -import { mockDataSourceRequest } from './utils'; - -test.beforeEach(mockDataSourceRequest); - -test('code editor autocomplete should handle table name escaping/quoting', async ({ explorePage, selectors, page }) => { - await page.getByLabel('Code').check(); - - const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox'); - await editor.fill('S'); - await page.getByLabel('SELECT FROM
').locator('a').click(); - await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); - await page.keyboard.press('Enter'); - - await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``); - - for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) { - await page.keyboard.press('Backspace'); - } - - await page.keyboard.press('Control+I'); - await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts b/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts deleted file mode 100644 index 68450533ee1..00000000000 --- a/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { selectors } from '@grafana/e2e-selectors'; -import { test, expect } from '@grafana/plugin-e2e'; - -import { normalTableName } from './mocks/mysql.mocks'; -import { mockDataSourceRequest } from './utils'; - -test.beforeEach(mockDataSourceRequest); - -test('visual query builder should handle macros', async ({ explorePage, page }) => { - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); - await page.getByText(normalTableName, { exact: true }).click(); - - // Open Data operations - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); - const select = page.getByLabel('Select options menu'); - await select.locator(page.getByText('$__timeGroupAlias')).click(); - - // Open column selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')).click(); - await select.locator(page.getByText('createdAt')).click(); - - // Open Interval selector - await explorePage - .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) - .click(); - await select.locator(page.getByText('$__interval')).click(); - - await page.getByRole('button', { name: 'Add column' }).click(); - - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); - await select.locator(page.getByText('AVG')).click(); - - await explorePage - .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) - .nth(1) - .click(); - await select.locator(page.getByText('bigint')).click(); - - // Validate the query - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue( - `SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` - ); -}); - -test('visual query builder should handle time filter macro', async ({ explorePage, page }) => { - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); - await page.getByText(normalTableName, { exact: true }).click(); - - // Open column selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click(); - const select = page.getByLabel('Select options menu'); - await select.locator(page.getByText('createdAt')).click(); - - // Toggle where row - await page.getByLabel('Filter').last().click(); - - // Click add filter button - await page.getByRole('button', { name: 'Add filter' }).click(); - await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice - - // Open field selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click(); - await select.locator(page.getByText('createdAt')).click(); - - // Open operator selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); - await select.locator(page.getByText('Macros')).click(); - - // Open macros value selector - await explorePage.getByGrafanaSelector('Macros value selector').click(); - await select.locator(page.getByText('timeFilter', { exact: true })).click(); - - // Validate that the timeFilter macro was added - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50`); - - // Validate that the timeFilter macro was removed when changed to equals operator - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); - await select.locator(page.getByText('==')).click(); - - await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click(); - await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur(); - - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`); -}); - -test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => { - const queryParams = new URLSearchParams(); - queryParams.set('schemaVersion', '1'); - queryParams.set('orgId', '1'); - const panes = { - mmm: { - datasource: 'P4FDCC188E688367F', - queries: [ - { - refId: 'A', - datasource: { - type: 'mysql', - uid: 'P4FDCC188E688367F', - }, - format: 'table', - rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ", - editorMode: 'builder', - sql: { - columns: [ - { - type: 'function', - parameters: [ - { - type: 'functionParameter', - name: '*', - }, - ], - }, - ], - groupBy: [ - { - type: 'groupBy', - property: { - type: 'string', - }, - }, - ], - limit: 50, - whereJsonTree: { - id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a', - type: 'group', - children1: [ - { - type: 'rule', - id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91', - properties: { - fieldSrc: 'field', - field: 'name', - operator: 'select_any_in', - value: ['a'], - valueSrc: ['value'], - valueType: ['text'], - }, - }, - ], - }, - whereString: "name IN ('a')", - }, - dataset: 'DataMaker', - table: 'normalTable', - }, - ], - }, - }; - queryParams.set('panes', JSON.stringify(panes)); - - await explorePage.goto({ queryParams }); - - // Validate the query - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`); -}); diff --git a/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts b/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts deleted file mode 100644 index c5e38fcbd10..00000000000 --- a/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'opentsdb' }); - - await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts deleted file mode 100644 index 118cfa7e6f7..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as e2e from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row -test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } }); -test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({ - page, - alertRuleEditPage, -}) => { - await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule'); - - //add working query - const queryA = alertRuleEditPage.getAlertRuleQueryRow('A'); - await queryA.datasource.set('gdev-prometheus'); - await queryA.locator.getByLabel('Code').click(); - await page.waitForFunction(() => window.monaco); - await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); - await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))'); - - //add broken query - const newQuery = await alertRuleEditPage.clickAddQueryRow(); - await newQuery.datasource.set('gdev-prometheus'); - await newQuery.locator.getByLabel('Code').click(); - await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); - await page.keyboard.insertText('topk(5,'); - - await expect(alertRuleEditPage.evaluate()).not.toBeOK(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts deleted file mode 100644 index b37ab2722bf..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.addPanel(); - await expect(page.url()).toContain('editPanel'); -}); - -test('add panel in new dashboard', async ({ dashboardPage, page }) => { - const panelEditPage = await dashboardPage.addPanel(); - await expect(panelEditPage.panel.locator).toBeVisible(); - await expect(page.url()).toContain('editPanel'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts deleted file mode 100644 index bf7ebe37653..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; - -test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { - test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); - await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); - await expect( - configPage.saveAndTest(), - formatExpectError('Expected save data source config to fail when Prometheus server is not running') - ).not.toBeOK(); - }); - - test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); - configPage.mockHealthCheckResponse({ status: 200 }); - await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); - await expect( - configPage.saveAndTest(), - formatExpectError('Expected data source config to be successfully saved') - ).toBeOK(); - }); -}); - -test.describe('test data source with frontend only health check', () => { - test('valid credentials should display a success alert on the page', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'zipkin' }); - configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200); - await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411'); - await expect(configPage.saveAndTest()).toBeOK(); - await expect( - configPage, - formatExpectError('Expected data source config to display success alert after save') - ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts deleted file mode 100644 index a4823228495..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; - -test('query data response should be OK when query is valid', async ({ explorePage }) => { - await explorePage.datasource.set('gdev-testdata'); - await expect(explorePage.runQuery(), formatExpectError('Expected Explore query to execute successfully')).toBeOK(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts deleted file mode 100644 index 48ded217e40..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; -const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; - -// override the feature toggles defined in playwright.config.ts only for tests in this file -test.use({ - featureToggles: { - [TRUTHY_CUSTOM_TOGGLE]: true, - [FALSY_CUSTOM_TOGGLE]: false, - }, -}); - -test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { - expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); - expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts deleted file mode 100644 index c55cc76b42c..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as e2e from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -test.describe('Loki editor', () => { - test('Autocomplete features should work as expected.', async ({ page }) => { - // Go to loki datasource in explore - await page.goto( - '/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1' - ); - - const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container); - const queryEditorRows = page.getByTestId('query-editor-rows'); - - async function assertQueryEditorEmpty() { - const queryEditorEmptyText = /^Enter to Rename.+/; - await expect(queryEditor).toHaveText(queryEditorEmptyText); - } - - async function clearInput() { - // Clear focused input - // Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake - await page.waitForTimeout(100); - await page.keyboard.press('ControlOrMeta+A'); - await page.keyboard.press('Backspace'); - } - - // assert that the query builder is shown by default - await expect(page.getByText('Label filters')).toHaveCount(1); - - // switch to code editor - await page.getByLabel('Code').click(); - - await page.waitForFunction(() => window.monaco); - await expect(queryEditor).toHaveCount(1); - await assertQueryEditorEmpty(); - - // assert editor automatically adds close paren - await queryEditor.click(); - await page.keyboard.type('time('); - await expect(queryEditor).toContainText('time()'); - - // removes closing brace when opening brace is removed - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('avg_over_time('); - await expect(queryEditor).toContainText('avg_over_time()'); - await page.keyboard.press('Backspace'); - await expect(queryEditor).not.toContainText('avg_over_time()'); - await expect(queryEditor).toContainText('avg_over_time'); - - // keeps closing brace when opening brace is removed and inner values exist - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('time(test'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('Backspace'); - await expect(queryEditor).toContainText('timetest'); - - // overrides an automatically inserted paren - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('time()'); - await expect(queryEditor).toContainText('time()'); - - // does not override manually inserted braces - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('))'); - await expect(queryEditor).toContainText('))'); - - // Should execute the query when enter with shift is pressed - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.press('Shift+Enter'); - await expect(page.getByTestId('explore-no-data')).toHaveCount(1); - - // Suggestions plugin - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('av'); - await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1); - await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts deleted file mode 100644 index 29bd1b834dc..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { successfulDataQuery } from '../mocks/queries'; - -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test.describe('panel edit page', () => { - test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => { - const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['Field', 'Max', 'Mean', 'Last']); - }); - - test('table panel data assertions', async ({ panelEditPage }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Table'); - await panelEditPage.refreshPanel(); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['col1', 'col2']); - await expect(panelEditPage.panel.data, formatExpectError('Could not locate headers in table panel')).toContainText([ - 'val1', - 'val2', - 'val3', - 'val4', - ]); - }); - - test('timeseries panel - table view assertions', async ({ panelEditPage }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Time series'); - await panelEditPage.refreshPanel(); - await panelEditPage.toggleTableView(); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['col1', 'col2']); - await expect( - panelEditPage.panel.data, - formatExpectError('Could not locate data elements in table panel') - ).toContainText(['val1', 'val2', 'val3', 'val4']); - }); -}); - -test.describe('dashboard page', () => { - test('getting panel by title', async ({ gotoDashboardPage }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.goto(); - const panel = await dashboardPage.getPanelByTitle('Colored background'); - await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); - }); - - test('getting panel by id', async ({ gotoDashboardPage }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.goto(); - const panel = await dashboardPage.getPanelByTitle('Colored background'); - await expect(panel.fieldNames, formatExpectError('Could not locate header elements in table panel')).toContainText([ - 'Field', - 'Max', - 'Mean', - 'Last', - ]); - }); -}); - -test.describe('explore page', () => { - test('table panel', async ({ explorePage }) => { - const url = - 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; - await explorePage.goto({ - queryParams: new URLSearchParams(url), - }); - await expect( - explorePage.timeSeriesPanel.locator, - formatExpectError('Could not locate time series panel in explore page') - ).toBeVisible(); - await expect( - explorePage.tablePanel.locator, - formatExpectError('Could not locate table panel in explore page') - ).toBeVisible(); - await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts deleted file mode 100644 index f8bc623f267..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { successfulDataQuery } from '../mocks/queries'; -import { scenarios } from '../mocks/resources'; - -const PANEL_TITLE = 'Table panel E2E test'; -const TABLE_VIZ_NAME = 'Table'; -const TIME_SERIES_VIZ_NAME = 'Time series'; -const STANDARD_OTIONS_CATEGORY = 'Standard options'; -const DISPLAY_NAME_LABEL = 'Display name'; -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test.describe('query editor query data', () => { - test('query data response should be OK when query is valid', async ({ panelEditPage }) => { - await panelEditPage.datasource.set('gdev-testdata'); - await expect( - panelEditPage.refreshPanel(), - formatExpectError('Expected panel query to execute successfully') - ).toBeOK(); - }); - - test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ - panelEditPage, - }) => { - await panelEditPage.datasource.set('gdev-testdata'); - const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); - await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); - await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); - await expect( - panelEditPage.panel.getErrorIcon(), - formatExpectError('Expected panel error to be displayed after query execution') - ).toBeVisible(); - }); -}); - -test.describe('query editor with mocked responses', () => { - test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => { - await dashboardPage.mockResourceResponse('scenarios', scenarios); - const panelEditPage = await dashboardPage.addPanel(); - await panelEditPage.datasource.set('gdev-testdata'); - const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); - await queryEditorRow.getByLabel('Scenario').last().click(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Select.option), - formatExpectError('Expected certain select options to be displayed after clicking on the select input') - ).toHaveText(scenarios.map((s) => s.name)); - }); - - test('mocked query data response', async ({ panelEditPage, selectors }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization(TABLE_VIZ_NAME); - await panelEditPage.refreshPanel(); - await expect( - panelEditPage.panel.getErrorIcon(), - formatExpectError('Did not expect panel error to be displayed after query execution') - ).not.toBeVisible(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body), - formatExpectError('Expected certain select options to be displayed after clicking on the select input') - ).toHaveText('val1val2val3val4'); - }); -}); - -test.describe('edit panel plugin settings', () => { - test('change viz to table panel, set panel title and collapse section', async ({ - panelEditPage, - selectors, - page, - }) => { - await panelEditPage.setVisualization(TABLE_VIZ_NAME); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), - formatExpectError('Expected panel visualization to be set to table') - ).toHaveText(TABLE_VIZ_NAME); - await panelEditPage.setPanelTitle(PANEL_TITLE); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)), - formatExpectError('Expected panel title to be updated') - ).toBeVisible(); - await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); - await expect( - page.getByText(DISPLAY_NAME_LABEL), - formatExpectError('Expected section to be collapsed') - ).toBeVisible(); - }); - - test('Select time zone in timezone picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = await panelEditPage.getCustomOptions('Axis'); - const timeZonePicker = axisOptions.getSelect('Time zone'); - - await timeZonePicker.selectOption('Europe/Stockholm'); - await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); - }); - - test('select unit in unit picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const standardOptions = panelEditPage.getStandardOptions(); - const unitPicker = standardOptions.getUnitPicker('Unit'); - - await unitPicker.selectOption('Misc > Pixels'); - - await expect(unitPicker).toHaveSelected('Pixels'); - }); - - test('enter value in number input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const lineWith = axisOptions.getNumberInput('Soft min'); - - await lineWith.fill('10'); - - await expect(lineWith).toHaveValue('10'); - }); - - test('enter value in slider', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const graphOptions = panelEditPage.getCustomOptions('Graph styles'); - const lineWidth = graphOptions.getSliderInput('Line width'); - - await lineWidth.fill('10'); - - await expect(lineWidth).toHaveValue('10'); - }); - - test('select value in single value select', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const standardOptions = panelEditPage.getStandardOptions(); - const colorSchemeSelect = standardOptions.getSelect('Color scheme'); - - await colorSchemeSelect.selectOption('Classic palette'); - await expect(colorSchemeSelect).toHaveSelected('Classic palette'); - }); - - test('clear input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const panelOptions = panelEditPage.getPanelOptions(); - const title = panelOptions.getTextInput('Title'); - - await expect(title).toHaveValue('New panel'); - await title.clear(); - await expect(title).toHaveValue(''); - }); - - test('enter value in input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const panelOptions = panelEditPage.getPanelOptions(); - const description = panelOptions.getTextInput('Description'); - - await expect(description).toHaveValue(''); - await description.fill('This is a panel'); - await expect(description).toHaveValue('This is a panel'); - }); - - test('unchecking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const showBorder = axisOptions.getSwitch('Show border'); - - await expect(showBorder).toBeChecked({ checked: false }); - await showBorder.check(); - await expect(showBorder).toBeChecked(); - - await showBorder.uncheck(); - await expect(showBorder).toBeChecked({ checked: false }); - }); - - test('checking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const showBorder = axisOptions.getSwitch('Show border'); - - await expect(showBorder).toBeChecked({ checked: false }); - await showBorder.check(); - await expect(showBorder).toBeChecked(); - }); - - test('re-selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const placement = axisOptions.getRadioGroup('Placement'); - - await placement.check('Right'); - await expect(placement).toHaveChecked('Right'); - - await placement.check('Auto'); - await expect(placement).toHaveChecked('Auto'); - }); - - test('selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const placement = axisOptions.getRadioGroup('Placement'); - - await placement.check('Right'); - await expect(placement).toHaveChecked('Right'); - }); -}); - -test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { - const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); - await panelEditPage.backToDashboard(); - await expect(page.url()).not.toContain('editPanel'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts deleted file mode 100644 index b93d9a8e5dd..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { prometheusLabels } from '../mocks/resources'; - -test('variable query with mocked response', async ({ variableEditPage, page }) => { - variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); - variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); - await variableEditPage.datasource.set('gdev-prometheus'); - await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); - await page.keyboard.press('Tab'); - await variableEditPage.runQuery(); - await expect( - variableEditPage, - formatExpectError('Expected variable edit page to display certain label names after query execution') - ).toDisplayPreviews(prometheusLabels.data); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts deleted file mode 100644 index 7c7711fa625..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { - await page.goto('/'); - const homePageURL = new URL(page.url()); - await page.goto('/datasources', { waitUntil: 'networkidle' }); - const redirectedPageURL = new URL(page.url()); - expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); -}); - -test('current user should have viewer role', async ({ page, request }) => { - await page.goto('/'); - const response = await request.get('/api/user/orgs'); - await expect(response).toBeOK(); - await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); -}); diff --git a/e2e/plugin-e2e/postgres/postgres.spec.ts b/e2e/plugin-e2e/postgres/postgres.spec.ts deleted file mode 100644 index aafdcbee40b..00000000000 --- a/e2e/plugin-e2e/postgres/postgres.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' }); - - await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/zipkin/zipkin.spec.ts b/e2e/plugin-e2e/zipkin/zipkin.spec.ts deleted file mode 100644 index 5a58dc656fb..00000000000 --- a/e2e/plugin-e2e/zipkin/zipkin.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'zipkin' }); - - await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/storybook/verify.spec.ts b/e2e/storybook/verify.spec.ts index 2610b1d5bf6..f6210d890d1 100644 --- a/e2e/storybook/verify.spec.ts +++ b/e2e/storybook/verify.spec.ts @@ -3,7 +3,7 @@ // NOTE: storybook must already be running (`yarn storybook`) for this test to work describe('Verify storybook', () => { it('Loads the button story correctly', () => { - cy.visit('?path=/story/buttons-button--basic'); + cy.visit('?path=/story/inputs-button--basic'); getIframeBody().find('button:contains("Example button")').should('be.visible'); }); }); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts deleted file mode 100644 index 12d270ae565..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; -import { testIds } from '../testIds'; -import pluginJson from '../plugin.json'; - -test('should display component exposed by another app', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/exposed-components`); - await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts deleted file mode 100644 index a0d6d2246a0..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import pluginJson from '../plugin.json'; -import { testIds } from '../testIds'; - -test('should render component with usePluginComponents hook', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-components`); - await expect( - page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) - ).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts deleted file mode 100644 index 008d3ec5baf..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import pluginJson from '../plugin.json'; -import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; -import { testIds } from '../testIds'; - -test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Go to A').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should extend main app with link extension from app B', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Open from B').click(); - await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); -}); - -test('should extend main app with basic link extension from app A', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Basic link').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({ - page, -}) => { - await page.goto(`/a/${testApp3pluginJson.id}`); - const container = await page.getByTestId(testIds.appC.section1); - await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible(); -}); - -const panelTitle = 'Link with defaults'; -const extensionTitle = 'Open from time series...'; - -const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; -const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; - -test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ - gotoDashboardPage, - page, -}) => { - const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); -}); - -test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); -}); - -test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { - const panelTitle = 'Link with new name'; - const extensionTitle = 'Open from piechart'; - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); -}); diff --git a/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts b/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts deleted file mode 100644 index b72ff0580b5..00000000000 --- a/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; - -// The following tests verify that label and input field association is working correctly. -// If these tests break, e2e tests in external plugins will break too. - -test.describe('config editor ', () => { - let configPage: DataSourceConfigPage; - test.beforeEach(async ({ createDataSourceConfigPage }) => { - configPage = await createDataSourceConfigPage({ type: 'grafana-e2etest-datasource' }); - }); - - test('text input field', async ({ page }) => { - const field = page.getByRole('textbox', { name: 'API key' }); - await expect(field).toBeEmpty(); - await field.fill('test text'); - await expect(field).toHaveValue('test text'); - }); - - test('switch field', async ({ page }) => { - const field = page.getByLabel('Switch Enabled'); - await expect(field).not.toBeChecked(); - await field.check(); - await expect(field).toBeChecked(); - }); - - test('checkbox field', async ({ page }) => { - const field = page.getByRole('checkbox', { name: 'Checkbox Enabled' }); - await expect(field).not.toBeChecked(); - await field.check({ force: true }); - await expect(field).toBeChecked(); - }); - - test('select field', async ({ page, selectors }) => { - const field = page.getByRole('combobox', { name: 'Auth type' }); - await field.click(); - const option = selectors.components.Select.option; - await expect(configPage.getByGrafanaSelector(option)).toHaveText(['keys', 'credentials']); - }); -}); diff --git a/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts b/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts deleted file mode 100644 index 72565e5a885..00000000000 --- a/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('should render variable editor', async ({ variableEditPage, page }) => { - await variableEditPage.datasource.set('gdev-e2etestdatasource'); - await expect(page.getByRole('textbox', { name: 'Query Text' })).toBeVisible(); -}); - -test('create new, successful variable query', async ({ variableEditPage, page }) => { - await variableEditPage.datasource.set('gdev-e2etestdatasource'); - await page.getByRole('textbox', { name: 'Query Text' }).fill('variableQuery'); - await variableEditPage.runQuery(); - await expect(variableEditPage).toDisplayPreviews(['A', 'B']); -}); diff --git a/eslint.config.js b/eslint.config.js index 055bb6e4158..d5bd122f12c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -43,7 +43,7 @@ module.exports = [ 'data/', 'deployment_tools_config.json', 'devenv', - 'e2e/test-plugins', + 'e2e-playwright/test-plugins', 'e2e/tmp', 'packages/grafana-ui/src/components/Icon/iconBundle.ts', 'pkg', @@ -176,6 +176,13 @@ module.exports = [ 'react/react-in-jsx-scope': 'off', }, }, + { + name: 'grafana/story-rules', + files: ['packages/grafana-ui/src/**/*.story.tsx'], + rules: { + '@grafana/consistent-story-titles': 'error', + }, + }, { name: 'grafana/public-dashboards-overrides', files: ['public/dashboards/scripted*.js'], diff --git a/go.mod b/go.mod index 51f4e0058d0..0d79814c684 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/grafana/grafana go 1.24.4 require ( - buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling - buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.2-20240902100956-02fd72488966.2 // @grafana/observability-traces-and-profiling - cloud.google.com/go/kms v1.21.0 // @grafana/grafana-backend-group - cloud.google.com/go/storage v1.52.0 // @grafana/grafana-backend-group + buf.build/gen/go/parca-dev/parca/connectrpc/go v1.18.1-20250703125925-3f0fcf4bff96.1 // @grafana/observability-traces-and-profiling + buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1 // @grafana/observability-traces-and-profiling + cloud.google.com/go/kms v1.22.0 // @grafana/grafana-backend-group + cloud.google.com/go/storage v1.55.0 // @grafana/grafana-backend-group connectrpc.com/connect v1.18.1 // @grafana/observability-traces-and-profiling cuelang.org/go v0.11.1 // @grafana/grafana-as-code filippo.io/age v1.2.1 // @grafana/identity-access-team @@ -58,8 +58,6 @@ require ( 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.132.0 // @grafana/grafana-app-platform-squad - github.com/go-git/go-billy/v5 v5.6.2 // @grafana/grafana-app-platform-squad - github.com/go-git/go-git/v5 v5.14.0 // @grafana/grafana-app-platform-squad github.com/go-jose/go-jose/v3 v3.0.4 // @grafana/identity-access-team github.com/go-jose/go-jose/v4 v4.1.0 // indirect; @grafana/identity-access-team github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group @@ -84,12 +82,12 @@ require ( github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group - github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group + github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20250716142237-8308539caa27 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // @grafana/identity-access-team - github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 // @grafana/identity-access-team + github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // @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/dskit v0.0.0-20250611075409-46f51e1ce914 // @grafana/grafana-backend-group @@ -97,16 +95,16 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.39.0 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.38.2 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-aws-sdk v1.0.2 // @grafana/aws-datasources + github.com/grafana/grafana-app-sdk v0.39.2 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.39.1 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-aws-sdk v1.0.4 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.4.1 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group github.com/grafana/grafana-plugin-sdk-go v0.278.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20250625082556-a4828b879bbb // @grafana-app-platform-squad + github.com/grafana/nanogit v0.0.0-20250709085038-55508a6a9f40 // @grafana-app-platform-squad github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling @@ -138,6 +136,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend github.com/microsoft/go-mssqldb v1.8.0 // @grafana/partner-datasources github.com/migueleliasweb/go-github-mock v1.1.0 // @grafana/grafana-app-platform-squad + github.com/mitchellh/copystructure v1.2.0 // @grafana/grafana-operator-experience-squad github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c //@grafana/identity-access-team github.com/mocktools/go-smtp-mock/v2 v2.3.1 // @grafana/grafana-backend-group github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-backend @@ -194,7 +193,8 @@ require ( go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/zap v1.27.0 // @grafana/identity-access-team - gocloud.dev v0.40.0 // @grafana/grafana-app-platform-squad + gocloud.dev v0.42.0 // @grafana/grafana-app-platform-squad + gocloud.dev/secrets/hashivault v0.42.0 // @grafana/grafana-operator-experience-squad golang.org/x/crypto v0.39.0 // @grafana/grafana-backend-group golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // @grafana/alerting-backend golang.org/x/mod v0.25.0 // indirect; @grafana/grafana-backend-group @@ -205,22 +205,23 @@ require ( golang.org/x/time v0.11.0 // @grafana/grafana-backend-group golang.org/x/tools v0.34.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent - google.golang.org/api v0.233.0 // @grafana/grafana-backend-group + google.golang.org/api v0.235.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.73.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.6 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group gopkg.in/yaml.v2 v2.4.0 // @grafana/alerting-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend - k8s.io/api v0.33.1 // @grafana/grafana-app-platform-squad - k8s.io/apimachinery v0.33.1 // @grafana/grafana-app-platform-squad - k8s.io/apiserver v0.33.1 // @grafana/grafana-app-platform-squad - k8s.io/client-go v0.33.1 // @grafana/grafana-app-platform-squad - k8s.io/component-base v0.33.1 // @grafana/grafana-app-platform-squad + k8s.io/api v0.33.2 // @grafana/grafana-app-platform-squad + k8s.io/apimachinery v0.33.2 // @grafana/grafana-app-platform-squad + k8s.io/apiserver v0.33.2 // @grafana/grafana-app-platform-squad + k8s.io/client-go v0.33.2 // @grafana/grafana-app-platform-squad + k8s.io/component-base v0.33.2 // @grafana/grafana-app-platform-squad k8s.io/klog/v2 v2.130.1 // @grafana/grafana-app-platform-squad k8s.io/kube-aggregator v0.33.1 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // @grafana/grafana-app-platform-squad k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // @grafana/partner-datasources + pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // @grafana-app-platform-squad xorm.io/builder v0.3.6 // @grafana/grafana-backend-group @@ -234,9 +235,9 @@ require ( github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b // @grafana/identity-access-team github.com/grafana/grafana/apps/investigations v0.0.0-20250627191313-2f1a6ae1712b // @fcjack @matryer github.com/grafana/grafana/apps/playlist v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/apps/secret v0.0.0-20250711114246-c9b2126c4ad5 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/pkg/aggregator v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-operator-experience-squad + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250711114246-c9b2126c4ad5 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-app-platform-squad // This needs to be here for other projects that import grafana/grafana @@ -248,13 +249,13 @@ require ( require ( cel.dev/expr v0.23.1 // indirect - cloud.google.com/go v0.120.0 // indirect + cloud.google.com/go v0.121.1 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.5.0 // indirect - cloud.google.com/go/longrunning v0.6.6 // indirect - cloud.google.com/go/monitoring v1.24.0 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect @@ -263,7 +264,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // 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/to v0.4.1 // 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 @@ -276,7 +277,6 @@ require ( github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect @@ -295,16 +295,16 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect @@ -348,7 +348,6 @@ require ( 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.6 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect @@ -372,7 +371,6 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect @@ -410,12 +408,17 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.4 // indirect github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/memberlist v0.5.2 // indirect github.com/hashicorp/serf v0.10.2 // indirect + github.com/hashicorp/vault/api v1.16.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect @@ -423,7 +426,6 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jaegertracing/jaeger-idl v0.5.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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 @@ -437,7 +439,6 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect @@ -461,7 +462,6 @@ require ( github.com/miekg/dns v1.1.63 // 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/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -492,7 +492,6 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect - github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect @@ -508,6 +507,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/asm v1.2.0 // indirect @@ -519,7 +519,6 @@ require ( github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -538,7 +537,6 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect @@ -553,7 +551,7 @@ require ( go.mongodb.org/mongo-driver v1.16.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect @@ -576,9 +574,9 @@ require ( golang.org/x/term v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect @@ -586,9 +584,8 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/telebot.v3 v3.2.1 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect - k8s.io/kms v0.33.1 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect + k8s.io/kms v0.33.2 // indirect modernc.org/libc v1.65.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.10.0 // indirect @@ -618,3 +615,8 @@ exclude github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // This was retracted, but seems to be known by the Go module proxy, and is // otherwise pulled in as a transitive dependency. exclude k8s.io/client-go v12.0.0+incompatible + +// This package contains test data for github.com/RoaringBitmap/roaring, which is +// only used to run tests and not required for building the Grafana binary. +// Since the test data doesn't contain a license file we exclude it. +exclude github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 diff --git a/go.sum b/go.sum index 7639e6acf66..1590f1901e9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 h1:Ur1tEsOxct4cuBumorzL/Zl9rRedJKdkPqufih3WsqQ= -buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1/go.mod h1:gC0oJPXUcGXzgiyUzMPBIgIfHbdoovWQD3/njIe5EVA= -buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.2-20240902100956-02fd72488966.2 h1:oRhx9wmKP52zEp1/PWWjaNxWtxWFrKXYSF1UJygjbzQ= -buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.2-20240902100956-02fd72488966.2/go.mod h1:w3CrNzdvwGJ4FwUlhshojc2FDXDN+3ou5nlcLTu7dHs= +buf.build/gen/go/parca-dev/parca/connectrpc/go v1.18.1-20250703125925-3f0fcf4bff96.1 h1:MXQkdpJwbCwskYLQ3dUv/5gmk8fSsUwu78u+kXebbek= +buf.build/gen/go/parca-dev/parca/connectrpc/go v1.18.1-20250703125925-3f0fcf4bff96.1/go.mod h1:pjl83IqpNF7Lm/lOPMLQ5IIfCg+yTu2A9OgYTcEncCs= +buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1 h1:9nqE/pDc/HXAXiD5pZncPywjAzWgKuBkFFYgdK2lVU8= +buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.36.2-20250703125925-3f0fcf4bff96.1/go.mod h1:1M7nlq2ljfzb95x9LaA2j1gYIvDkVZii58mGvTa9ExM= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= @@ -46,8 +46,8 @@ cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFO cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -194,8 +194,8 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= 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= @@ -330,8 +330,8 @@ cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGE cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs= -cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -351,8 +351,8 @@ cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4 cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/kms v1.21.0 h1:x3EeWKuYwdlo2HLse/876ZrKjk2L5r7Uexfm8+p6mSI= -cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk= +cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= +cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= @@ -368,8 +368,8 @@ cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhX cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= -cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= @@ -393,8 +393,8 @@ cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhI cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= -cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -558,8 +558,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.52.0 h1:ROpzMW/IwipKtatA69ikxibdzQSiXJrY9f6IgBa9AlA= -cloud.google.com/go/storage v1.52.0/go.mod h1:4wrBAbAYUvYkbrf19ahGm4I5kDQhESSqN3CGEkMGvOY= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -579,8 +579,8 @@ cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= -cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= @@ -689,8 +689,8 @@ github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSY github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= +github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= @@ -744,7 +744,6 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -763,7 +762,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdko github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/RoaringBitmap/gocroaring v0.4.0/go.mod h1:NieMwz7ZqwU2DD73/vvYwv7r4eWBKuPVSXZIpsaMwCI= -github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= github.com/RoaringBitmap/roaring v0.9.1/go.mod h1:h1B7iIUOmnAeb5ytYMvnHJwxMc6LUrwBnzXWRuqTQUc= github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= @@ -804,8 +802,6 @@ github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqR github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -855,16 +851,16 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDO github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3/go.mod h1:aqsLGsPs+rJfwDBwWHLcIV8F7AFcikFTPLwUD4RwORQ= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFieZUfVdINOiCTvChOMPfdLnmiLzs= @@ -873,18 +869,18 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJ github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 h1:teOWtElLARLOhpYWwupjLbY9j5I/yZ/H1I8jg41An78= github.com/aws/aws-sdk-go-v2/service/oam v1.18.3/go.mod h1:wGhpdyftHX6/1U4egowHkYdypwBMjpb+KjAAprv6z20= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 h1:PwbxovpcJvb25k019bkibvJfCpCmIANOFrXZIFPmRzk= github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6/go.mod h1:Z4xLt5mXspLKjBV92i165wAJ/3T6TIv4n7RtIS8pWV0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= @@ -1055,8 +1051,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -1201,8 +1195,6 @@ github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7M github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= -github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= @@ -1212,14 +1204,6 @@ github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3 github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= -github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= 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= @@ -1563,8 +1547,8 @@ 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.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -1589,10 +1573,10 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/alerting v0.0.0-20250716142237-8308539caa27 h1:buoRKSKUO2QGJ16j2mY8T8S0KTjwB9uLtBrNvbYjkmM= github.com/grafana/alerting v0.0.0-20250716142237-8308539caa27/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q= -github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed h1:k5Ng33zE9fCawqfEVybOasXY7/FQD5Qg2J92ePneeVM= -github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= -github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= -github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= @@ -1607,12 +1591,12 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.39.0 h1:WC2E9BKXWDX/e2bajdAFjQEyyWf9BFp7Yzbjm6T1T1w= -github.com/grafana/grafana-app-sdk v0.39.0/go.mod h1:xRyBQOttgWTc3tGe9pI0upnpEPVhzALf7Mh/61O4zyY= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= -github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= -github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= +github.com/grafana/grafana-aws-sdk v1.0.4 h1:D14UAehsOqpjliHmHzveRQ1p43KCsMzdmb7GovWj+SY= +github.com/grafana/grafana-aws-sdk v1.0.4/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 h1:S4kHwr//AqhtL9xHBtz1gqVgZQeCRGTxjgsRBAkpjKY= @@ -1637,12 +1621,12 @@ github.com/grafana/grafana/apps/investigations v0.0.0-20250627191313-2f1a6ae1712 github.com/grafana/grafana/apps/investigations v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:8RlQ4U9lccPEBD/QxV4zyIMh9+lzjS/7xGpiqn3cHLY= github.com/grafana/grafana/apps/playlist v0.0.0-20250627191313-2f1a6ae1712b h1:elfpvk06igCjE0yL+/urc69UDOt1B/sPfdNg9X9kUMc= github.com/grafana/grafana/apps/playlist v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:fPtx6dwGm0PweQRVbgtthMapJMvXobBcORbndb7Dgd4= +github.com/grafana/grafana/apps/secret v0.0.0-20250711114246-c9b2126c4ad5 h1:+fMhUoqwGdY8ntH0GL2icJa3uk+bTiIMicawDG2r9Uc= +github.com/grafana/grafana/apps/secret v0.0.0-20250711114246-c9b2126c4ad5/go.mod h1:TIrKvhgo2j6lvVeOZ3TUmXbI4I48d6v7QcadL/f6SKQ= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250627191313-2f1a6ae1712b h1:ei01IFqmnXkOrrVvsT3CYe+i5xYra3SCX7Wsu3PMsDU= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:+H4Va9jDJlGQJjAN+OFD/hLx2I/yEzDRMQLaKecvgAc= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250627191313-2f1a6ae1712b h1:e0dG1tPpuv4NHCAPP235Gip/MBQB8fQ2XW2bwHqoWh4= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:u0+k7KLCvGi6zHWsc2B7r+tmGcYjN/qR+gn51pl104E= -github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250627191313-2f1a6ae1712b h1:rkQO7exsDLdr4KGA7kgEnkQnbJGePbDIP1SUQptLRs8= -github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:9YjiHZzii2DZfocRDJbqSeC8M3GWenU5yexeHHxsZ4Y= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250711114246-c9b2126c4ad5 h1:f4fopIH6eQRoZ/E7bstn69UtDAHleIdQ6DrdzEs++Ug= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250711114246-c9b2126c4ad5/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b h1:QyJLJn3xwFTIXu9KPZujsrIUN0X8DdiR9b2h75L0AfI= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:6OKkPWDB8PetDXqMVMOWL35iTCEUdpATwwpuew0k8+o= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -1655,8 +1639,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 h1:ZYk42718k github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608/go.mod h1:f3JSoxBTPXX5ec4FxxeC19nTBSxoTz+cBgS3cYLMcr0= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20250625082556-a4828b879bbb h1:JR672zBiCkfiKuRRsU76TRC2q12q0VfZl2SP8jBIAJQ= -github.com/grafana/nanogit v0.0.0-20250625082556-a4828b879bbb/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco= +github.com/grafana/nanogit v0.0.0-20250709085038-55508a6a9f40 h1:wsIgOI4Ou1o/UtxtJlemLufpVBpMdcXVJxedk0wLoCM= +github.com/grafana/nanogit v0.0.0-20250709085038-55508a6a9f40/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250620093340-be61a673dee6 h1:oJnbhG6ZNy10AjsgNeAtAKeGHogIGOMfAsBH6fYYa5M= @@ -1735,6 +1719,10 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= @@ -1756,6 +1744,7 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= @@ -1776,6 +1765,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hetznercloud/hcloud-go/v2 v2.19.1 h1:UU/7h3uc/rdgspM8xkQF7wokmwZXePWDXcLqrQRRzzY= @@ -1816,8 +1807,6 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= github.com/jaegertracing/jaeger-idl v0.5.0/go.mod h1:ON90zFo9eoyXrt9F/KN8YeF3zxcnujaisMweFY/rg5k= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -1878,8 +1867,6 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -2176,8 +2163,6 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -2317,6 +2302,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= @@ -2353,11 +2340,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx 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/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 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= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -2482,8 +2466,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= @@ -2570,8 +2552,8 @@ go.opentelemetry.io/collector/pdata v1.30.0 h1:j3jyq9um436r6WzWySzexP2nLnFdmL5uV go.opentelemetry.io/collector/pdata v1.30.0/go.mod h1:0Bxu1ktuj4wE7PIASNSvd0SdBscQ1PLtYasymJ13/Cs= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= @@ -2659,8 +2641,10 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= -gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng= -gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ= +gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw= +gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM= +gocloud.dev/secrets/hashivault v0.42.0 h1:kPWIIu1AP6ApHf7HwUrzGrwyRJEfTd46JNUNgnbpsFA= +gocloud.dev/secrets/hashivault v0.42.0/go.mod h1:LXprr1XLEAT7BVZ+Y66dJEHQMzDsowIExj5Ktr9HLvM= 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= @@ -3273,8 +3257,8 @@ google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjY google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= -google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI= -google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c= +google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -3425,21 +3409,21 @@ google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3543,8 +3527,6 @@ gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs= gopkg.in/telebot.v3 v3.2.1/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -3572,19 +3554,19 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/api v0.0.0-20190813020757-36bff7324fb7/go.mod h1:3Iy+myeAORNCLgjd/Xu9ebwN7Vh59Bw0vh9jhoX+V58= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -3592,8 +3574,8 @@ k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.33.1 h1:jJKrFhsbVofpyLF+G8k+drwOAF9CMQpxilHa5Uilb8Q= -k8s.io/kms v0.33.1/go.mod h1:C1I8mjFFBNzfUZXYt9FZVJ8MJl7ynFbGgZFbBzkBJ3E= +k8s.io/kms v0.33.2 h1:GFwNXX4CZGQCg9DPOaJi1/+iKidCtB9/OIAGdzRo8FI= +k8s.io/kms v0.33.2/go.mod h1:C1I8mjFFBNzfUZXYt9FZVJ8MJl7ynFbGgZFbBzkBJ3E= k8s.io/kube-aggregator v0.33.1 h1:PigQUqAvd6Y4hBjQAqhKz3lEJC2VHLL4bSOEuS06a40= k8s.io/kube-aggregator v0.33.1/go.mod h1:16/wlU5Lj7hNJSv7JSu5FLvxyrgiJVLCHzfVoECAsuI= k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= @@ -3661,6 +3643,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/go.work b/go.work index 044c12b9a93..8d1fff1ed6a 100644 --- a/go.work +++ b/go.work @@ -12,9 +12,9 @@ use ( ./apps/iam ./apps/investigations ./apps/playlist + ./apps/secret ./pkg/aggregator ./pkg/apimachinery - ./pkg/apis/secret // @grafana/grafana-operator-experience-squad ./pkg/apiserver ./pkg/build ./pkg/build/wire // skip:golangci-lint diff --git a/go.work.sum b/go.work.sum index a106af447b4..45af5346b64 100644 --- a/go.work.sum +++ b/go.work.sum @@ -8,236 +8,481 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-2025042515311 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= buf.build/go/protovalidate v0.12.0 h1:4GKJotbspQjRCcqZMGVSuC8SjwZ/FmgtSuKDpKUTZew= buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0= +cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= +cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go v0.118.1/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M= +cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= cloud.google.com/go/accessapproval v1.8.3 h1:axlU03FRiXDNupsmPG7LKzuS4Enk1gf598M62lWVB74= cloud.google.com/go/accessapproval v1.8.3/go.mod h1:3speETyAv63TDrDmo5lIkpVueFkQcQchkiw/TAMbBo4= +cloud.google.com/go/accessapproval v1.8.6 h1:UkmDPCKvj24bkGVrvgJPcgSDkmIPw/bAmOiDb9avOiE= +cloud.google.com/go/accessapproval v1.8.6/go.mod h1:FfmTs7Emex5UvfnnpMkhuNkRCP85URnBFt5ClLxhZaQ= cloud.google.com/go/accesscontextmanager v1.9.3 h1:8zVoeiBa4erMCLEXltOcqVEsZhS26JZ5/Vrgs59eQiI= cloud.google.com/go/accesscontextmanager v1.9.3/go.mod h1:S1MEQV5YjkAKBoMekpGrkXKfrBdsi4x6Dybfq6gZ8BU= +cloud.google.com/go/accesscontextmanager v1.9.6 h1:2LnncRqfYB8NEdh9+FeYxAt9POTW/0zVboktnRlO11w= +cloud.google.com/go/accesscontextmanager v1.9.6/go.mod h1:884XHwy1AQpCX5Cj2VqYse77gfLaq9f8emE2bYriilk= cloud.google.com/go/aiplatform v1.74.0 h1:rE2P5H7FOAFISAZilmdkapbk4CVgwfVs6FDWlhGfuy0= cloud.google.com/go/aiplatform v1.74.0/go.mod h1:hVEw30CetNut5FrblYd1AJUWRVSIjoyIvp0EVUh51HA= +cloud.google.com/go/aiplatform v1.89.0 h1:niSJYc6ldWWVM9faXPo1Et1MVSQoLvVGriD7fwbJdtE= +cloud.google.com/go/aiplatform v1.89.0/go.mod h1:TzZtegPkinfXTtXVvZZpxx7noINFMVDrLkE7cEWhYEk= cloud.google.com/go/analytics v0.26.0 h1:O2kWr2Sd4ep3I+YJ4aiY0G4+zWz6sp4eTce+JVns9TM= cloud.google.com/go/analytics v0.26.0/go.mod h1:KZWJfs8uX/+lTjdIjvT58SFa86V9KM6aPXwZKK6uNVI= +cloud.google.com/go/analytics v0.28.1 h1:W2ft49J/LeEj9A07Jsd5Q2kAzajK0j0IffOyyzbxw04= +cloud.google.com/go/analytics v0.28.1/go.mod h1:iPaIVr5iXPB3JzkKPW1JddswksACRFl3NSHgVHsuYC4= cloud.google.com/go/apigateway v1.7.3 h1:Mn7cC5iWJz+cSMS/Hb+N2410CpZ6c8XpJKaexBl0Gxs= cloud.google.com/go/apigateway v1.7.3/go.mod h1:uK0iRHdl2rdTe79bHW/bTsKhhXPcFihjUdb7RzhTPf4= +cloud.google.com/go/apigateway v1.7.6 h1:do+u3rjDYuTxD2ypRfv4uwTMoy/VHFLclvaYcb5Mv6I= +cloud.google.com/go/apigateway v1.7.6/go.mod h1:SiBx36VPjShaOCk8Emf63M2t2c1yF+I7mYZaId7OHiA= cloud.google.com/go/apigeeconnect v1.7.3 h1:Wlr+30Tha0SMCvQYZKdrh+HkpOyl0CQFSlzeY/Gg1gs= cloud.google.com/go/apigeeconnect v1.7.3/go.mod h1:2ZkT5VCAqhYrDqf4dz7lGp4N/+LeNBSfou8Qs5bIuSg= +cloud.google.com/go/apigeeconnect v1.7.6 h1:ijEJSni5xROOn1YyiHgqcW0B0TWr0di9VgIi2gvyNjY= +cloud.google.com/go/apigeeconnect v1.7.6/go.mod h1:zqDhHY99YSn2li6OeEjFpAlhXYnXKl6DFb/fGu0ye2w= cloud.google.com/go/apigeeregistry v0.9.3 h1:j9CJg/oC884OX5cDpiwNt1ZlDXNV6Zb9Mp1YmRrOG0k= cloud.google.com/go/apigeeregistry v0.9.3/go.mod h1:oNCP2VjOeI6U8yuOuTmU4pkffdcXzR5KxeUD71gF+Dg= +cloud.google.com/go/apigeeregistry v0.9.6 h1:TgdjAoGoRY81DEc2LYsYvi/OqCFImMzAk/TVKiSRsQw= +cloud.google.com/go/apigeeregistry v0.9.6/go.mod h1:AFEepJBKPtGDfgabG2HWaLH453VVWWFFs3P4W00jbPs= cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ= cloud.google.com/go/appengine v1.9.3 h1:jrcanSzj9J1erevZuxldvsDwY+0k/DeFFzlnSfPGfL8= cloud.google.com/go/appengine v1.9.3/go.mod h1:DtLsE/z3JufM/pCEIyVYebJ0h9UNPpN64GZQrYgOSyM= +cloud.google.com/go/appengine v1.9.6 h1:JJyY8icMmQeWfQ+d36IhkGvd3Guzvw0UAkvxT0wmUx8= +cloud.google.com/go/appengine v1.9.6/go.mod h1:jPp9T7Opvzl97qytaRGPwoH7pFI3GAcLDaui1K8PNjY= cloud.google.com/go/area120 v0.9.3 h1:dPQ07rW4eku8OgNWDOaQaVGcE4+XfhH8BSbVwdVQ+wU= cloud.google.com/go/area120 v0.9.3/go.mod h1:F3vxS/+hqzrjJo55Xvda3Jznjjbd+4Foo43SN5eMd8M= +cloud.google.com/go/area120 v0.9.6 h1:iJrZ6AleZr4l+q0/fWVANFOhs90KiSB1Ccait5OYyNg= +cloud.google.com/go/area120 v0.9.6/go.mod h1:qKSokqe0iTmwBDA3tbLWonMEnh0pMAH4YxiceiHUed4= cloud.google.com/go/artifactregistry v1.16.1 h1:ZNXGB6+T7VmWdf6//VqxLdZ/sk0no8W0ujanHeJwDRw= cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= +cloud.google.com/go/artifactregistry v1.17.1 h1:A20kj2S2HO9vlyBVyVFHPxArjxkXvLP5LjcdE7NhaPc= +cloud.google.com/go/artifactregistry v1.17.1/go.mod h1:06gLv5QwQPWtaudI2fWO37gfwwRUHwxm3gA8Fe568Hc= cloud.google.com/go/asset v1.20.4 h1:6oNgjcs5KCPGBD71G0IccK6TfeFsEtBTyQ3Q+Dn09bs= cloud.google.com/go/asset v1.20.4/go.mod h1:DP09pZ+SoFWUZyPZx26xVroHk+6+9umnQv+01yfJxbM= +cloud.google.com/go/asset v1.21.1 h1:i55wWC/EwVdHMyJgRfbLp/L6ez4nQuOpZwSxkuqN9ek= +cloud.google.com/go/asset v1.21.1/go.mod h1:7AzY1GCC+s1O73yzLM1IpHFLHz3ws2OigmCpOQHwebk= cloud.google.com/go/assuredworkloads v1.12.3 h1:RU1WhF1zMggdXAZ+ezYTn4Eh/FdiX7sz8lLXGERn4Po= cloud.google.com/go/assuredworkloads v1.12.3/go.mod h1:iGBkyMGdtlsxhCi4Ys5SeuvIrPTeI6HeuEJt7qJgJT8= +cloud.google.com/go/assuredworkloads v1.12.6 h1:ip/shfJYx6lrHBWYADjrrrubcm7uZzy50TTF5tPG7ek= +cloud.google.com/go/assuredworkloads v1.12.6/go.mod h1:QyZHd7nH08fmZ+G4ElihV1zoZ7H0FQCpgS0YWtwjCKo= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/automl v1.14.4 h1:vkD+hQ75SMINMgJBT/KDpFYvfQLzJbtIQZdw0AWq8Rs= cloud.google.com/go/automl v1.14.4/go.mod h1:sVfsJ+g46y7QiQXpVs9nZ/h8ntdujHm5xhjHW32b3n4= +cloud.google.com/go/automl v1.14.7 h1:ZLj48Ur2Qcso4M3bgOtjsOmeV5Ee92N14wuOc8OW+L0= +cloud.google.com/go/automl v1.14.7/go.mod h1:8a4XbIH5pdvrReOU72oB+H3pOw2JBxo9XTk39oljObE= cloud.google.com/go/baremetalsolution v1.3.3 h1:OL+KT+wCumdDhG44aeqGAdkwdT8Wa4Lh+o4INM+CQjw= cloud.google.com/go/baremetalsolution v1.3.3/go.mod h1:uF9g08RfmXTF6ZKbXxixy5cGMGFcG6137Z99XjxLOUI= +cloud.google.com/go/baremetalsolution v1.3.6 h1:9bdGlpY1LgLONQjFsDwrkjLzdPTlROpfU+GhA97YpOk= +cloud.google.com/go/baremetalsolution v1.3.6/go.mod h1:7/CS0LzpLccRGO0HL3q2Rofxas2JwjREKut414sE9iM= cloud.google.com/go/batch v1.12.0 h1:lXuTaELvU0P0ARbTFxxdpOC/dFnZZeGglSw06BtO//8= cloud.google.com/go/batch v1.12.0/go.mod h1:CATSBh/JglNv+tEU/x21Z47zNatLQ/gpGnpyKOzbbcM= +cloud.google.com/go/batch v1.12.2 h1:gWQdvdPplptpvrkqF6ibtxZkOsYKLTFbxYawHa/TvCg= +cloud.google.com/go/batch v1.12.2/go.mod h1:tbnuTN/Iw59/n1yjAYKV2aZUjvMM2VJqAgvUgft6UEU= cloud.google.com/go/beyondcorp v1.1.3 h1:ezavJc0Gzh4N8zBskO/DnUVMWPa8lqH/tmQSyaknmCA= cloud.google.com/go/beyondcorp v1.1.3/go.mod h1:3SlVKnlczNTSQFuH5SSyLuRd4KaBSc8FH/911TuF/Cc= +cloud.google.com/go/beyondcorp v1.1.6 h1:4FcR+4QmcNGkhVij6TrYS4AQVNLBo7PBXKxNrKzpclQ= +cloud.google.com/go/beyondcorp v1.1.6/go.mod h1:V1PigSWPGh5L/vRRmyutfnjAbkxLI2aWqJDdxKbwvsQ= cloud.google.com/go/bigquery v1.66.2 h1:EKOSqjtO7jPpJoEzDmRctGea3c2EOGoexy8VyY9dNro= cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= +cloud.google.com/go/bigquery v1.69.0 h1:rZvHnjSUs5sHK3F9awiuFk2PeOaB8suqNuim21GbaTc= +cloud.google.com/go/bigquery v1.69.0/go.mod h1:TdGLquA3h/mGg+McX+GsqG9afAzTAcldMjqhdjHTLew= cloud.google.com/go/bigtable v1.35.0 h1:UEacPwaejN2mNbz67i1Iy3G812rxtgcs6ePj1TAg7dw= cloud.google.com/go/bigtable v1.35.0/go.mod h1:EabtwwmTcOJFXp+oMZAT/jZkyDIjNwrv53TrS4DGrrM= +cloud.google.com/go/bigtable v1.37.0 h1:Q+x7y04lQ0B+WXp03wc1/FLhFt4CwcQdkwWT0M4Jp3w= +cloud.google.com/go/bigtable v1.37.0/go.mod h1:HXqddP6hduwzrtiTCqZPpj9ij4hGZb4Zy1WF/dT+yaU= cloud.google.com/go/billing v1.20.1 h1:xMlO3hc5BI0s23tRB40bL40xSpxUR1x3E07Y5/VWcjU= cloud.google.com/go/billing v1.20.1/go.mod h1:DhT80hUZ9gz5UqaxtK/LNoDELfxH73704VTce+JZqrY= +cloud.google.com/go/billing v1.20.4 h1:pqM5/c9UGydB9H90IPCxSvfCNLUPazAOSMsZkz5q5P4= +cloud.google.com/go/billing v1.20.4/go.mod h1:hBm7iUmGKGCnBm6Wp439YgEdt+OnefEq/Ib9SlJYxIU= cloud.google.com/go/binaryauthorization v1.9.3 h1:X8JRfmk0/vyRqLusEyAPr0nZCK6RKae9omB4lrit0XI= cloud.google.com/go/binaryauthorization v1.9.3/go.mod h1:f3xcb/7vWklDoF+q2EaAIS+/A/e1278IgiYxonRX+Jk= +cloud.google.com/go/binaryauthorization v1.9.5 h1:T0zYEroXT+y0O/x/yZd5SwQdFv4UbUINjvJyJKzDm0Q= +cloud.google.com/go/binaryauthorization v1.9.5/go.mod h1:CV5GkS2eiY461Bzv+OH3r5/AsuB6zny+MruRju3ccB8= cloud.google.com/go/certificatemanager v1.9.3 h1:2UP31fg7b+y3F0OmNbPHOKPEJ+6LOMfxAXX4p8xGCy4= cloud.google.com/go/certificatemanager v1.9.3/go.mod h1:O5T4Lg/dHbDHLFFooV2Mh/VsT3Mj2CzPEWRo4qw5prc= +cloud.google.com/go/certificatemanager v1.9.5 h1:+ZPglfDurCcsv4azizDFpBucD1IkRjWjbnU7zceyjfY= +cloud.google.com/go/certificatemanager v1.9.5/go.mod h1:kn7gxT/80oVGhjL8rurMUYD36AOimgtzSBPadtAeffs= cloud.google.com/go/channel v1.19.2 h1:oHyO3QAZ6kdf6SwqnUTBz50ND6Nk2rxZtboUiF4dgLE= cloud.google.com/go/channel v1.19.2/go.mod h1:syX5opXGXFt17DHCyCdbdlM464Tx0gHMi46UlEWY9Gg= +cloud.google.com/go/channel v1.19.5 h1:UI+ZsRkS15hi9DRF+WAvTVLVuSeZiRmvCU8cjkjOwUU= +cloud.google.com/go/channel v1.19.5/go.mod h1:vevu+LK8Oy1Yuf7lcpDbkQQQm5I7oiY5fFTn3uwfQLY= cloud.google.com/go/cloudbuild v1.22.0 h1:zmDznviZpvkCla0adbp7jJsMYZ9bABCbcPK2cBUHwg8= cloud.google.com/go/cloudbuild v1.22.0/go.mod h1:p99MbQrzcENHb/MqU3R6rpqFRk/X+lNG3PdZEIhM95Y= +cloud.google.com/go/cloudbuild v1.22.2 h1:4LlrIFa3IFLgD1mGEXmUE4cm9fYoU71OLwTvjM7Dg3c= +cloud.google.com/go/cloudbuild v1.22.2/go.mod h1:rPyXfINSgMqMZvuTk1DbZcbKYtvbYF/i9IXQ7eeEMIM= cloud.google.com/go/clouddms v1.8.4 h1:CDOd1nwmP4uek+nZhl4bhRIpzj8jMqoMRqKAfKlgLhw= cloud.google.com/go/clouddms v1.8.4/go.mod h1:RadeJ3KozRwy4K/gAs7W74ZU3GmGgVq5K8sRqNs3HfA= +cloud.google.com/go/clouddms v1.8.7 h1:IWJbQBEECTaNanDRN1XdR7FU53MJ1nylTl3s9T3MuyI= +cloud.google.com/go/clouddms v1.8.7/go.mod h1:DhWLd3nzHP8GoHkA6hOhso0R9Iou+IGggNqlVaq/KZ4= cloud.google.com/go/cloudtasks v1.13.3 h1:rXdznKjCa7WpzmvR2plrn2KJ+RZC1oYxPiRWNQjjf3k= cloud.google.com/go/cloudtasks v1.13.3/go.mod h1:f9XRvmuFTm3VhIKzkzLCPyINSU3rjjvFUsFVGR5wi24= +cloud.google.com/go/cloudtasks v1.13.6 h1:Fwan19UiNoFD+3KY0MnNHE5DyixOxNzS1mZ4ChOdpy0= +cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36GeomEuy9gL4gLmU= cloud.google.com/go/compute v1.34.0 h1:+k/kmViu4TEi97NGaxAATYtpYBviOWJySPZ+ekA95kk= cloud.google.com/go/compute v1.34.0/go.mod h1:zWZwtLwZQyonEvIQBuIa0WvraMYK69J5eDCOw9VZU4g= +cloud.google.com/go/compute v1.38.0 h1:MilCLYQW2m7Dku8hRIIKo4r0oKastlD74sSu16riYKs= +cloud.google.com/go/compute v1.38.0/go.mod h1:oAFNIuXOmXbK/ssXm3z4nZB8ckPdjltJ7xhHCdbWFZM= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/contactcenterinsights v1.17.1 h1:xJoZbX0HM1zht8KxAB38hs2v4Hcl+vXGLo454LrdwxA= cloud.google.com/go/contactcenterinsights v1.17.1/go.mod h1:n8OiNv7buLA2AkGVkfuvtW3HU13AdTmEwAlAu46bfxY= +cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q= +cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= cloud.google.com/go/container v1.42.2 h1:8ncSEBjkng6ucCICauaUGzBomoM2VyYzleAum1OFcow= cloud.google.com/go/container v1.42.2/go.mod h1:y71YW7uR5Ck+9Vsbst0AF2F3UMgqmsN4SP8JR9xEsR8= +cloud.google.com/go/container v1.43.0 h1:A6J92FJPfxTvyX7MHF+w4t2W9WCqvHOi9UB5SAeSy3w= +cloud.google.com/go/container v1.43.0/go.mod h1:ETU9WZ1KM9ikEKLzrhRVao7KHtalDQu6aPqM34zDr/U= cloud.google.com/go/containeranalysis v0.13.3 h1:1D8U75BeotZxrG4jR6NYBtOt+uAeBsWhpBZmSYLakQw= cloud.google.com/go/containeranalysis v0.13.3/go.mod h1:0SYnagA1Ivb7qPqKNYPkCtphhkJn3IzgaSp3mj+9XAY= +cloud.google.com/go/containeranalysis v0.14.1 h1:1SoHlNqL3XrhqcoozB+3eoHif2sRUFtp/JeASQTtGKo= +cloud.google.com/go/containeranalysis v0.14.1/go.mod h1:28e+tlZgauWGHmEbnI5UfIsjMmrkoR1tFN0K2i71jBI= cloud.google.com/go/datacatalog v1.24.3 h1:3bAfstDB6rlHyK0TvqxEwaeOvoN9UgCs2bn03+VXmss= cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= +cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE= +cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14= cloud.google.com/go/dataflow v0.10.3 h1:+7IfIXzYWSybIIDGK9FN2uqBsP/5b/Y0pBYzNhcmKSU= cloud.google.com/go/dataflow v0.10.3/go.mod h1:5EuVGDh5Tg4mDePWXMMGAG6QYAQhLNyzxdNQ0A1FfW4= +cloud.google.com/go/dataflow v0.11.0 h1:AdhB4cAkMOC9NtrHJxpKOVvO/VqBLaIyk0tEEhbGjYM= +cloud.google.com/go/dataflow v0.11.0/go.mod h1:gNHC9fUjlV9miu0hd4oQaXibIuVYTQvZhMdPievKsPk= cloud.google.com/go/dataform v0.10.3 h1:ZpGkZV8OyhUhvN/tfLffU2ki5ERTtqOunkIaiVAhmw0= cloud.google.com/go/dataform v0.10.3/go.mod h1:8SruzxHYCxtvG53gXqDZvZCx12BlsUchuV/JQFtyTCw= +cloud.google.com/go/dataform v0.12.0 h1:0eCPTPUC/RZ863aVfXTJLkg0tEpdpn62VD6ywSmmzxM= +cloud.google.com/go/dataform v0.12.0/go.mod h1:PuDIEY0lSVuPrZqcFji1fmr5RRvz3DGz4YP/cONc8g4= cloud.google.com/go/datafusion v1.8.3 h1:FTMtsf2nfGGlDCuE84/RvVaCcTIYE7WQSB0noeO0cwI= cloud.google.com/go/datafusion v1.8.3/go.mod h1:hyglMzE57KRf0Rf/N2VRPcHCwKfZAAucx+LATY6Jc6Q= +cloud.google.com/go/datafusion v1.8.6 h1:GZ6J+CR8CEeWAj8luRCtr8GvImSQRkArIIqGiZOnzBA= +cloud.google.com/go/datafusion v1.8.6/go.mod h1:fCyKJF2zUKC+O3hc2F9ja5EUCAbT4zcH692z8HiFZFw= cloud.google.com/go/datalabeling v0.9.3 h1:PqoA3gnOWaLcHCnqoZe4jh3jmiv6+Z7W2xUUkw/j4jE= cloud.google.com/go/datalabeling v0.9.3/go.mod h1:3LDFUgOx+EuNUzDyjU7VElO8L+b5LeaZEFA/ZU1O1XU= +cloud.google.com/go/datalabeling v0.9.6 h1:VOZ5U+78ttnhNCEID7qdeogqZQzK5N+LPHIQ9Q3YDsc= +cloud.google.com/go/datalabeling v0.9.6/go.mod h1:n7o4x0vtPensZOoFwFa4UfZgkSZm8Qs0Pg/T3kQjXSM= cloud.google.com/go/dataplex v1.22.0 h1:j4hD6opb+gq9CJNPFIlIggoW8Kjymg8Wmy2mdHmQoiw= cloud.google.com/go/dataplex v1.22.0/go.mod h1:g166QMCGHvwc3qlTG4p34n+lHwu7JFfaNpMfI2uO7b8= +cloud.google.com/go/dataplex v1.25.3 h1:Xr0Toh6wyBlmL3H4EPu1YKwxUtkDSzzq+IP0iLc88kk= +cloud.google.com/go/dataplex v1.25.3/go.mod h1:wOJXnOg6bem0tyslu4hZBTncfqcPNDpYGKzed3+bd+E= cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU= cloud.google.com/go/dataproc/v2 v2.11.0 h1:6aRpyoRfNOP+r2+pGb7HeHtF+SYQID8kzztfHuK0plk= cloud.google.com/go/dataproc/v2 v2.11.0/go.mod h1:9vgGrn57ra7KBqz+B2KD+ltzEXvnHAUClFgq/ryU99g= +cloud.google.com/go/dataproc/v2 v2.11.2 h1:KhC8wdLILpAs17yeTG6Miwg1v0nOP/OXD+9QNg3w6AQ= +cloud.google.com/go/dataproc/v2 v2.11.2/go.mod h1:xwukBjtfiO4vMEa1VdqyFLqJmcv7t3lo+PbLDcTEw+g= cloud.google.com/go/dataqna v0.9.3 h1:lGUj2FYs650EUPDMV6plWBAoh8qH9Bu1KCz1PUYF2VY= cloud.google.com/go/dataqna v0.9.3/go.mod h1:PiAfkXxa2LZYxMnOWVYWz3KgY7txdFg9HEMQPb4u1JA= +cloud.google.com/go/dataqna v0.9.7 h1:qTRAG/E3T63Xj1orefRlwupfwH9c9ERUAnWSRGp75so= +cloud.google.com/go/dataqna v0.9.7/go.mod h1:4ac3r7zm7Wqm8NAc8sDIDM0v7Dz7d1e/1Ka1yMFanUM= cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM= cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= cloud.google.com/go/datastream v1.13.0 h1:C5AeEdze55feJVb17a40QmlnyH/aMhn/uf3Go3hIqPA= cloud.google.com/go/datastream v1.13.0/go.mod h1:GrL2+KC8mV4GjbVG43Syo5yyDXp3EH+t6N2HnZb1GOQ= +cloud.google.com/go/datastream v1.14.1 h1:j+y0lUKm9pbDjJn0YcWxPI/hXNGUQ80GE6yrFuJC/JA= +cloud.google.com/go/datastream v1.14.1/go.mod h1:JqMKXq/e0OMkEgfYe0nP+lDye5G2IhIlmencWxmesMo= cloud.google.com/go/deploy v1.26.2 h1:1c2Cd3jdb0mrKHHfyzSQ5DRmxgYd07tIZZzuMNrwDxU= cloud.google.com/go/deploy v1.26.2/go.mod h1:XpS3sG/ivkXCfzbzJXY9DXTeCJ5r68gIyeOgVGxGNEs= +cloud.google.com/go/deploy v1.27.2 h1:C0VqBhFyQFp6+xgPHZAD7LeRA4XGy5YLzGmPQ2NhlLk= +cloud.google.com/go/deploy v1.27.2/go.mod h1:4NHWE7ENry2A4O1i/4iAPfXHnJCZ01xckAKpZQwhg1M= cloud.google.com/go/dialogflow v1.66.0 h1:/kfpZw20/3v4sC8czEIuvn3Bu3qOne5aHDYlRYHbu18= cloud.google.com/go/dialogflow v1.66.0/go.mod h1:BPiRTnnXP/tHLot5h/U62Xcp+i6ekRj/bq6uq88p+Lw= +cloud.google.com/go/dialogflow v1.68.2 h1:bXpoqPRf37KKxB79PKr20B/TAU/Z5iA0FnB6C5N2jrA= +cloud.google.com/go/dialogflow v1.68.2/go.mod h1:E0Ocrhf5/nANZzBju8RX8rONf0PuIvz2fVj3XkbAhiY= cloud.google.com/go/dlp v1.21.0 h1:9kz7+gaB/0gBZsDUnNT1asDihNZSrRFSeUTBcBdUAkk= cloud.google.com/go/dlp v1.21.0/go.mod h1:Y9HOVtPoArpL9sI1O33aN/vK9QRwDERU9PEJJfM8DvE= +cloud.google.com/go/dlp v1.23.0 h1:3xWRKylXxhysaQaV+DLev1YcIywFUCc7yJEE6R7ZGDQ= +cloud.google.com/go/dlp v1.23.0/go.mod h1:vVT4RlyPMEMcVHexdPT6iMVac3seq3l6b8UPdYpgFrg= cloud.google.com/go/documentai v1.35.2 h1:hswVobCWUTXtmn+4QqUIVkai7sDOe0QS2KB3IpqLkik= cloud.google.com/go/documentai v1.35.2/go.mod h1:oh/0YXosgEq3hVhyH4ZQ7VNXPaveRO4eLVM3tBSZOsI= +cloud.google.com/go/documentai v1.37.0 h1:7fla8GcarupO15eatRTUveXCob6DOSW1Wa+1i63CM3Q= +cloud.google.com/go/documentai v1.37.0/go.mod h1:qAf3ewuIUJgvSHQmmUWvM3Ogsr5A16U2WPHmiJldvLA= cloud.google.com/go/domains v0.10.3 h1:wnqN5YwMrtLSjn+HB2sChgmZ6iocOta4Q41giQsiRjY= cloud.google.com/go/domains v0.10.3/go.mod h1:m7sLe18p0PQab56bVH3JATYOJqyRHhmbye6gz7isC7o= +cloud.google.com/go/domains v0.10.6 h1:TI+Aavwc31KD8huOquJz0ISchCq1zSEWc9M+JcPJyxc= +cloud.google.com/go/domains v0.10.6/go.mod h1:3xzG+hASKsVBA8dOPc4cIaoV3OdBHl1qgUpAvXK7pGY= cloud.google.com/go/edgecontainer v1.4.1 h1:SwQuHQiheVfL7b5ar/AXDberiaqr/yiue8X55AdWnZU= cloud.google.com/go/edgecontainer v1.4.1/go.mod h1:ubMQvXSxsvtEjJLyqcPFrdWrHfvjQxdoyt+SUrAi5ek= +cloud.google.com/go/edgecontainer v1.4.3 h1:9tfGCicvrki927T+hGMB0yYmwIbRuZY6JR1/awrKiZ0= +cloud.google.com/go/edgecontainer v1.4.3/go.mod h1:q9Ojw2ox0uhAvFisnfPRAXFTB1nfRIOIXVWzdXMZLcE= cloud.google.com/go/errorreporting v0.3.2 h1:isaoPwWX8kbAOea4qahcmttoS79+gQhvKsfg5L5AgH8= cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww= cloud.google.com/go/essentialcontacts v1.7.3 h1:Paw495vxVyKuAgcQ2NQk09iRZBhPYRytknydEnvzcv4= cloud.google.com/go/essentialcontacts v1.7.3/go.mod h1:uimfZgDbhWNCmBpwUUPHe4vcMY2azsq/axC9f7vZFKI= +cloud.google.com/go/essentialcontacts v1.7.6 h1:ysHZ4gr4plW1CL1Ur/AucUUfh20hDjSFbfjxSK0q/sk= +cloud.google.com/go/essentialcontacts v1.7.6/go.mod h1:/Ycn2egr4+XfmAfxpLYsJeJlVf9MVnq9V7OMQr9R4lA= cloud.google.com/go/eventarc v1.15.1 h1:RMymT7R87LaxKugOKwooOoheWXUm1NMeOfh3CVU9g54= cloud.google.com/go/eventarc v1.15.1/go.mod h1:K2luolBpwaVOujZQyx6wdG4n2Xum4t0q1cMBmY1xVyI= +cloud.google.com/go/eventarc v1.15.5 h1:bZW7ZMM+XXNErg6rOZcgxUzAgz4vpReRDP3ZiGf7/sI= +cloud.google.com/go/eventarc v1.15.5/go.mod h1:vDCqGqyY7SRiickhEGt1Zhuj81Ya4F/NtwwL3OZNskg= cloud.google.com/go/filestore v1.9.3 h1:vTXQI5qYKZ8dmCyHN+zVfaMyXCYbyZNM0CkPzpPUn7Q= cloud.google.com/go/filestore v1.9.3/go.mod h1:Me0ZRT5JngT/aZPIKpIK6N4JGMzrFHRtGHd9ayUS4R4= +cloud.google.com/go/filestore v1.10.2 h1:LjoAyp9TvVNBns3sUUzPaNsQiGpR2BReGmTS3bUCuBE= +cloud.google.com/go/filestore v1.10.2/go.mod h1:w0Pr8uQeSRQfCPRsL0sYKW6NKyooRgixCkV9yyLykR4= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/functions v1.19.3 h1:V0vCHSgFTUqKn57+PUXp1UfQY0/aMkveAw7wXeM3Lq0= cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= +cloud.google.com/go/functions v1.19.6 h1:vJgWlvxtJG6p/JrbXAkz83DbgwOyFhZZI1Y32vUddjY= +cloud.google.com/go/functions v1.19.6/go.mod h1:0G0RnIlbM4MJEycfbPZlCzSf2lPOjL7toLDwl+r0ZBw= cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc= cloud.google.com/go/gkebackup v1.6.3 h1:djdExe/QgoKdp1gnIO1G5BoO1o/yGQOQJJEZ4QKTEXQ= cloud.google.com/go/gkebackup v1.6.3/go.mod h1:JJzGsA8/suXpTDtqI7n9RZW97PXa2CIp+n8aRC/y57k= +cloud.google.com/go/gkebackup v1.8.0 h1:eBqOt61yEChvj7I/GDPBbdCCRdUPudD1qrQYfYWV3Ok= +cloud.google.com/go/gkebackup v1.8.0/go.mod h1:FjsjNldDilC9MWKEHExnK3kKJyTDaSdO1vF0QeWSOPU= cloud.google.com/go/gkeconnect v0.12.1 h1:YVpR0vlHSP/wD74PXEbKua4Aamud+wiYm4TiewNjD3M= cloud.google.com/go/gkeconnect v0.12.1/go.mod h1:L1dhGY8LjINmWfR30vneozonQKRSIi5DWGIHjOqo58A= +cloud.google.com/go/gkeconnect v0.12.4 h1:67/rnPmF/I1Wmf7jWyKH+z4OWjU8ZUI0Vmzxvmzf3KY= +cloud.google.com/go/gkeconnect v0.12.4/go.mod h1:bvpU9EbBpZnXGo3nqJ1pzbHWIfA9fYqgBMJ1VjxaZdk= cloud.google.com/go/gkehub v0.15.3 h1:yZ6lNJ9rNIoQmWrG14dB3+BFjS/EIRBf7Bo6jc5QWlE= cloud.google.com/go/gkehub v0.15.3/go.mod h1:nzFT/Q+4HdQES/F+FP1QACEEWR9Hd+Sh00qgiH636cU= +cloud.google.com/go/gkehub v0.15.6 h1:9iogrmNNa+drDPf/zkLH/6KGgUf7FuuyokmithoGwMQ= +cloud.google.com/go/gkehub v0.15.6/go.mod h1:sRT0cOPAgI1jUJrS3gzwdYCJ1NEzVVwmnMKEwrS2QaM= cloud.google.com/go/gkemulticloud v1.5.1 h1:JWe6PDNpNU88ZYvQkTd7w28fgeIs/gg6i0hcjUkgZ3M= cloud.google.com/go/gkemulticloud v1.5.1/go.mod h1:OdmhfSPXuJ0Kn9dQ2I3Ou7XZ3QK8caV4XVOJZwrIa3s= +cloud.google.com/go/gkemulticloud v1.5.3 h1:334aZmOzIt3LVBpguCof8IHaLaftcZlx+L0TGBukYkY= +cloud.google.com/go/gkemulticloud v1.5.3/go.mod h1:KPFf+/RcfvmuScqwS9/2MF5exZAmXSuoSLPuaQ98Xlk= cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= +cloud.google.com/go/grafeas v0.3.11 h1:CobnwnyeY1j1Defi5vbEircI+jfrk3ci5m004ZjiFP4= +cloud.google.com/go/grafeas v0.3.11/go.mod h1:dcQyG2+T4tBgG0MvJAh7g2wl/xHV2w+RZIqivwuLjNg= +cloud.google.com/go/grafeas v0.3.15 h1:lBjwKmhpiqOAFaE0xdqF8CqO74a99s8tUT5mCkBBxPs= +cloud.google.com/go/grafeas v0.3.15/go.mod h1:irwcwIQOBlLBotGdMwme8PipnloOPqILfIvMwlmu8Pk= cloud.google.com/go/gsuiteaddons v1.7.4 h1:f3eMYsCDdg2AeldIPdKmBRxN1WoiTpE3RvX5orcm/I8= cloud.google.com/go/gsuiteaddons v1.7.4/go.mod h1:gpE2RUok+HUhuK7RPE/fCOEgnTffS0lCHRaAZLxAMeE= +cloud.google.com/go/gsuiteaddons v1.7.7 h1:sk0SxpCGIA7tIO//XdiiG29f2vrF6Pq/dsxxyBGiRBY= +cloud.google.com/go/gsuiteaddons v1.7.7/go.mod h1:zTGmmKG/GEBCONsvMOY2ckDiEsq3FN+lzWGUiXccF9o= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4= cloud.google.com/go/iap v1.10.3 h1:OWNYFHPyIBNHEAEFdVKOltYWe0g3izSrpFJW6Iidovk= cloud.google.com/go/iap v1.10.3/go.mod h1:xKgn7bocMuCFYhzRizRWP635E2LNPnIXT7DW0TlyPJ8= +cloud.google.com/go/iap v1.11.2 h1:VIioCrYsyWiRGx7Y8RDNylpI6d4t1Qx5ZgSLUVmWWPo= +cloud.google.com/go/iap v1.11.2/go.mod h1:Bh99DMUpP5CitL9lK0BC8MYgjjYO4b3FbyhgW1VHJvg= cloud.google.com/go/ids v1.5.3 h1:wbFF7twu0XScFr+dtsVxTTttbFIRYt/SJjZiHFidtYE= cloud.google.com/go/ids v1.5.3/go.mod h1:a2MX8g18Eqs7yxD/pnEdid42SyBUm9LIzSWf8Jux9OY= +cloud.google.com/go/ids v1.5.6 h1:uKGuaWozDcjg3wyf54Gd7tCH2YK8BFeH9qo1xBNiPKE= +cloud.google.com/go/ids v1.5.6/go.mod h1:y3SGLmEf9KiwKsH7OHvYYVNIJAtXybqsD2z8gppsziQ= cloud.google.com/go/iot v1.8.3 h1:aPWYQ+A1NX6ou/5U0nFAiXWdVT8OBxZYVZt2fBl2gWA= cloud.google.com/go/iot v1.8.3/go.mod h1:dYhrZh+vUxIQ9m3uajyKRSW7moF/n0rYmA2PhYAkMFE= +cloud.google.com/go/iot v1.8.6 h1:A3AhugnIViAZkC3/lHAQDaXBIk2ZOPBZS0XQCyZsjjc= +cloud.google.com/go/iot v1.8.6/go.mod h1:MThnkiihNkMysWNeNje2Hp0GSOpEq2Wkb/DkBCVYa0U= cloud.google.com/go/language v1.14.3 h1:8hmFMiS3wjjj3TX/U1zZYTgzwZoUjDbo9PaqcYEmuB4= cloud.google.com/go/language v1.14.3/go.mod h1:hjamj+KH//QzF561ZuU2J+82DdMlFUjmiGVWpovGGSA= +cloud.google.com/go/language v1.14.5 h1:BVJ/POtlnJ55LElvnQY19UOxpMVtHoHHkFJW2uHJsVU= +cloud.google.com/go/language v1.14.5/go.mod h1:nl2cyAVjcBct1Hk73tzxuKebk0t2eULFCaruhetdZIA= cloud.google.com/go/lifesciences v0.10.3 h1:Z05C+Ui953f0EQx9hJ1la6+QQl8ADrIs3iNwP5Elkpg= cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAzDQQt5tWd7iIpNg= +cloud.google.com/go/lifesciences v0.10.6 h1:Vu7XF4s5KJ8+mSLIL4eaQM6JTyWXvSB54oqC+CUZH20= +cloud.google.com/go/lifesciences v0.10.6/go.mod h1:1nnZwaZcBThDujs9wXzECnd1S5d+UiDkPuJWAmhRi7Q= +cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= cloud.google.com/go/managedidentities v1.7.3 h1:b9xGs24BIjfyvLgCtJoClOZpPi8d8owPgWe5JEINgaY= cloud.google.com/go/managedidentities v1.7.3/go.mod h1:H9hO2aMkjlpY+CNnKWRh+WoQiUIDO8457wWzUGsdtLA= +cloud.google.com/go/managedidentities v1.7.6 h1:zrZVWXZJlmHnfpyCrTQIbDBGUBHrcOOvrsjMjoXRxrk= +cloud.google.com/go/managedidentities v1.7.6/go.mod h1:pYCWPaI1AvR8Q027Vtp+SFSM/VOVgbjBF4rxp1/z5p4= cloud.google.com/go/maps v1.19.0 h1:deVm1ZFyCrUwxG11CdvtBz350VG5JUQ/LHTLnQrBgrM= cloud.google.com/go/maps v1.19.0/go.mod h1:goHUXrmzoZvQjUVd0KGhH8t3AYRm17P8b+fsyR1UAmQ= +cloud.google.com/go/maps v1.21.0 h1:El61AfMxC1sU/RU8Wzs9dkZEgltyunKM86aKF9aDlaE= +cloud.google.com/go/maps v1.21.0/go.mod h1:cqzZ7+DWUKKbPTgqE+KuNQtiCRyg/o7WZF9zDQk+HQs= cloud.google.com/go/mediatranslation v0.9.3 h1:nRBjeaMLipw05Br+qDAlSCcCQAAlat4mvpafztbEVgc= cloud.google.com/go/mediatranslation v0.9.3/go.mod h1:KTrFV0dh7duYKDjmuzjM++2Wn6yw/I5sjZQVV5k3BAA= +cloud.google.com/go/mediatranslation v0.9.6 h1:SDGatA73TgZ8iCvILVXpk/1qhTK5DJyufUDEWgbmbV8= +cloud.google.com/go/mediatranslation v0.9.6/go.mod h1:WS3QmObhRtr2Xu5laJBQSsjnWFPPthsyetlOyT9fJvE= cloud.google.com/go/memcache v1.11.3 h1:XH/qT3GbbSH//R0JTqR77lRpBxaa0N9sHgAzfwbTrv0= cloud.google.com/go/memcache v1.11.3/go.mod h1:UeWI9cmY7hvjU1EU6dwJcQb6EFG4GaM3KNXOO2OFsbI= +cloud.google.com/go/memcache v1.11.6 h1:33IVqQEmFiITsBXwGHeTkUhWz0kLNKr90nV3e22uLPs= +cloud.google.com/go/memcache v1.11.6/go.mod h1:ZM6xr1mw3F8TWO+In7eq9rKlJc3jlX2MDt4+4H+/+cc= cloud.google.com/go/metastore v1.14.3 h1:jDqeCw6NGDRAPT9+2Y/EjnWAB0BfCcUfmPLOyhB0eHs= cloud.google.com/go/metastore v1.14.3/go.mod h1:HlbGVOvg0ubBLVFRk3Otj3gtuzInuzO/TImOBwsKlG4= +cloud.google.com/go/metastore v1.14.7 h1:dLm59AHHZCorveCylj7c2iWhkQsmMIeWTsV+tG/BXtY= +cloud.google.com/go/metastore v1.14.7/go.mod h1:0dka99KQofeUgdfu+K/Jk1KeT9veWZlxuZdJpZPtuYU= +cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +cloud.google.com/go/monitoring v1.22.1/go.mod h1:AuZZXAoN0WWWfsSvET1Cpc4/1D8LXq8KRDU87fMS6XY= cloud.google.com/go/networkconnectivity v1.16.1 h1:YsVhG71ZC4FkqCP2oCI55x/JeGFyd7738Lt8iNTrzJw= cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= +cloud.google.com/go/networkconnectivity v1.17.1 h1:RQcG1rZNCNV5Dn3tnINs4TYswDXk2hKH+85eh+JvoWU= +cloud.google.com/go/networkconnectivity v1.17.1/go.mod h1:DTZCq8POTkHgAlOAAEDQF3cMEr/B9k1ZbpklqvHEBtg= cloud.google.com/go/networkmanagement v1.18.0 h1:oEoFGPYxTBsY47h0zdoE2ojV5aU/541D83UmxfjHWaE= cloud.google.com/go/networkmanagement v1.18.0/go.mod h1:yTxpAFuvQOOKgL3W7+k2Rp1bSKTxyRcZ5xNHGdHUM6w= +cloud.google.com/go/networkmanagement v1.19.1 h1:ecukgArkYCVcK5w2h7WDDd+nHgmBAp9Bst7ClmVKz5A= +cloud.google.com/go/networkmanagement v1.19.1/go.mod h1:icgk265dNnilxQzpr6rO9WuAuuCmUOqq9H6WBeM2Af4= cloud.google.com/go/networksecurity v0.10.3 h1:JLJBFbxc8D7/OS81MyRoKhc2OvnVJxy5VMoQqqAhA7k= cloud.google.com/go/networksecurity v0.10.3/go.mod h1:G85ABVcPscEgpw+gcu+HUxNZJWjn3yhTqEU7+SsltFM= +cloud.google.com/go/networksecurity v0.10.6 h1:6b6fcCG9BFNcmtNO+VuPE04vkZb5TKNX9+7ZhYMgstE= +cloud.google.com/go/networksecurity v0.10.6/go.mod h1:FTZvabFPvK2kR/MRIH3l/OoQ/i53eSix2KA1vhBMJec= cloud.google.com/go/notebooks v1.12.3 h1:+9DrGJcZhCu6B2t0JJorekjIUBvg/KvBmXJYGmfvVvA= cloud.google.com/go/notebooks v1.12.3/go.mod h1:I0pMxZct+8Rega2LYrXL8jGAGZgLchSmh8Ksc+0xNyA= +cloud.google.com/go/notebooks v1.12.6 h1:nCfZwVihArMPP2atRoxRrXOXJ/aC9rAgpBQGCc2zpYw= +cloud.google.com/go/notebooks v1.12.6/go.mod h1:3Z4TMEqAKP3pu6DI/U+aEXrNJw9hGZIVbp+l3zw8EuA= cloud.google.com/go/optimization v1.7.3 h1:JwQjjoBZJpsoMQe/3mhVBMVZuSdagHg2pGOnwh2Jk+E= cloud.google.com/go/optimization v1.7.3/go.mod h1:GlYFp4Mju0ybK5FlOUtV6zvWC00TIScdbsPyF6Iv144= +cloud.google.com/go/optimization v1.7.6 h1:jDvIuSxDsXI2P7l2sYXm6CoX1YBIIT6Khm5m0hq0/KQ= +cloud.google.com/go/optimization v1.7.6/go.mod h1:4MeQslrSJGv+FY4rg0hnZBR/tBX2awJ1gXYp6jZpsYY= cloud.google.com/go/orchestration v1.11.4 h1:SFAsKyqvtS8VFcsq+JgXAeRkrksB9UH+AH7iFamkmlc= cloud.google.com/go/orchestration v1.11.4/go.mod h1:UKR2JwogaZmDGnAcBgAQgCPn89QMqhXFUCYVhHd31vs= +cloud.google.com/go/orchestration v1.11.9 h1:PnlZ/O4R/eiounpxUkhI9ZXRMWbG7vFqxc6L6sR+31k= +cloud.google.com/go/orchestration v1.11.9/go.mod h1:KKXK67ROQaPt7AxUS1V/iK0Gs8yabn3bzJ1cLHw4XBg= cloud.google.com/go/orgpolicy v1.14.2 h1:WFvgmjq/FO5GiXlhebltA9N14KdbLMcgG88ME+SWeBo= cloud.google.com/go/orgpolicy v1.14.2/go.mod h1:2fTDMT3X048iFKxc6DEgkG+a/gN+68qEgtPrHItKMzo= +cloud.google.com/go/orgpolicy v1.15.0 h1:uQziDu3UKYk9ZwUgneZAW5aWxZFKgOXXsuVKFKh0z7Y= +cloud.google.com/go/orgpolicy v1.15.0/go.mod h1:NTQLwgS8N5cJtdfK55tAnMGtvPSsy95JJhESwYHaJVs= cloud.google.com/go/osconfig v1.14.3 h1:cyf1PMK5c2/WOIr5r2lxjH/XBJMA9P4zC8Tm10i0z3M= cloud.google.com/go/osconfig v1.14.3/go.mod h1:9D2MS1Etne18r/mAeW5jtto3toc9H1qu9wLNDG3NvQg= +cloud.google.com/go/osconfig v1.14.6 h1:4uJrA1obzMBp1I+DF15y/MvsXKIODevuANpq3QhvX30= +cloud.google.com/go/osconfig v1.14.6/go.mod h1:LS39HDBH0IJDFgOUkhSZUHFQzmcWaCpYXLrc3A4CVzI= cloud.google.com/go/oslogin v1.14.3 h1:yomxnFPk+ye0zd0mJ15nn9fH4Ns7ex4xA3ll+u2q59A= cloud.google.com/go/oslogin v1.14.3/go.mod h1:fDEGODTG/W9ZGUTHTlMh8euXWC1fTcgjJ9Kcxxy14a8= +cloud.google.com/go/oslogin v1.14.6 h1:BDKVcxo1OO4ZT+PbuFchZjnbrlUGfChilt6+pITY1VI= +cloud.google.com/go/oslogin v1.14.6/go.mod h1:xEvcRZTkMXHfNSKdZ8adxD6wvRzeyAq3cQX3F3kbMRw= cloud.google.com/go/phishingprotection v0.9.3 h1:T5mGFV0ggBKg3qt9myFRiGJu+nIUucuHLAtVpAuQ08I= cloud.google.com/go/phishingprotection v0.9.3/go.mod h1:ylzN9HruB/X7dD50I4sk+FfYzuPx9fm5JWsYI0t7ncc= +cloud.google.com/go/phishingprotection v0.9.6 h1:yl572bBQbPjflX250SOflN6gwO2uYoddN2uRp36fDTo= +cloud.google.com/go/phishingprotection v0.9.6/go.mod h1:VmuGg03DCI0wRp/FLSvNyjFj+J8V7+uITgHjCD/x4RQ= cloud.google.com/go/policytroubleshooter v1.11.3 h1:ekIWI8JbKkpOfrgH/THGamQE/D16tcVBYJyrkseVcYI= cloud.google.com/go/policytroubleshooter v1.11.3/go.mod h1:AFHlORqh4AnMC0twc2yPKfzlozp3DO0yo9OfOd9aNOs= +cloud.google.com/go/policytroubleshooter v1.11.6 h1:Z8+tO2z21MY1arBBuJjwrOjbw8fbZb13AZTHXdzkl2U= +cloud.google.com/go/policytroubleshooter v1.11.6/go.mod h1:jdjYGIveoYolk38Dm2JjS5mPkn8IjVqPsDHccTMu3mY= cloud.google.com/go/privatecatalog v0.10.4 h1:fu2LABMi7CgZORQ2oNGbc0hoZ0FTqLkjGqIgAV/Kc7U= cloud.google.com/go/privatecatalog v0.10.4/go.mod h1:n/vXBT+Wq8B4nSRUJNDsmqla5BYjbVxOlHzS6PjiF+w= +cloud.google.com/go/privatecatalog v0.10.7 h1:R951ikhxIanXEijBCu0xnoUAOteS5m/Xplek0YvsNTE= +cloud.google.com/go/privatecatalog v0.10.7/go.mod h1:Fo/PF/B6m4A9vUYt0nEF1xd0U6Kk19/Je3eZGrQ6l60= cloud.google.com/go/pubsub v1.47.0 h1:Ou2Qu4INnf7ykrFjGv2ntFOjVo8Nloh/+OffF4mUu9w= cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8= +cloud.google.com/go/pubsub v1.49.0 h1:5054IkbslnrMCgA2MAEPcsN3Ky+AyMpEZcii/DoySPo= +cloud.google.com/go/pubsub v1.49.0/go.mod h1:K1FswTWP+C1tI/nfi3HQecoVeFvL4HUOB1tdaNXKhUY= cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+uF1Y= cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ= cloud.google.com/go/recaptchaenterprise/v2 v2.19.4 h1:T5YGzaXwTesHaPDNTAuU3neDwZEnfjce70zufPFUwno= cloud.google.com/go/recaptchaenterprise/v2 v2.19.4/go.mod h1:WaglfocMJGkqZVdXY/FVB7OhoVRONPS4uXqtNn6HfX0= +cloud.google.com/go/recaptchaenterprise/v2 v2.20.4 h1:P4QMryKcWdi4LIe1Sx0b2ZOAQv5gVfdzPt2peXcN32Y= +cloud.google.com/go/recaptchaenterprise/v2 v2.20.4/go.mod h1:3H8nb8j8N7Ss2eJ+zr+/H7gyorfzcxiDEtVBDvDjwDQ= cloud.google.com/go/recommendationengine v0.9.3 h1:kBpcYPx4ys4lrDGKp4OhP2uy8h7UjlmLW/qoO5Xb2bY= cloud.google.com/go/recommendationengine v0.9.3/go.mod h1:QRnX5aM7DCvtqtSs7I0zay5Zfq3fzxqnsPbZF7pa1G8= +cloud.google.com/go/recommendationengine v0.9.6 h1:slN7h23vswGccW8x3f+xUXCu9Yo18/GNkazH93LJbFk= +cloud.google.com/go/recommendationengine v0.9.6/go.mod h1:nZnjKJu1vvoxbmuRvLB5NwGuh6cDMMQdOLXTnkukUOE= cloud.google.com/go/recommender v1.13.3 h1:dVlOjxsbjuhlwu4MIcyPWe09qVcDqc419iOjdPl5RHk= cloud.google.com/go/recommender v1.13.3/go.mod h1:6yAmcfqJRKglZrVuTHsieTFEm4ai9JtY3nQzmX4TC0Q= +cloud.google.com/go/recommender v1.13.5 h1:cIsyRKGNw4LpCfY5c8CCQadhlp54jP4fHtP+d5Sy2xE= +cloud.google.com/go/recommender v1.13.5/go.mod h1:v7x/fzk38oC62TsN5Qkdpn0eoMBh610UgArJtDIgH/E= cloud.google.com/go/redis v1.18.0 h1:xcu35SCyHSp+nKV6QNIklgkBKTH1qb0aLUXjl0mSR8I= cloud.google.com/go/redis v1.18.0/go.mod h1:fJ8dEQJQ7DY+mJRMkSafxQCuc8nOyPUwo9tXJqjvNEY= +cloud.google.com/go/redis v1.18.2 h1:JlHLceAOILEmbn+NIS7l+vmUKkFuobLToCWTxL7NGcQ= +cloud.google.com/go/redis v1.18.2/go.mod h1:q6mPRhLiR2uLf584Lcl4tsiRn0xiFlu6fnJLwCORMtY= cloud.google.com/go/resourcemanager v1.10.3 h1:SHOMw0kX0xWratC5Vb5VULBeWiGlPYAs82kiZqNtWpM= cloud.google.com/go/resourcemanager v1.10.3/go.mod h1:JSQDy1JA3K7wtaFH23FBGld4dMtzqCoOpwY55XYR8gs= +cloud.google.com/go/resourcemanager v1.10.6 h1:LIa8kKE8HF71zm976oHMqpWFiaDHVw/H1YMO71lrGmo= +cloud.google.com/go/resourcemanager v1.10.6/go.mod h1:VqMoDQ03W4yZmxzLPrB+RuAoVkHDS5tFUUQUhOtnRTg= cloud.google.com/go/resourcesettings v1.8.3 h1:13HOFU7v4cEvIHXSAQbinF4wp2Baybbq7q9FMctg1Ek= cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= cloud.google.com/go/retail v1.19.2 h1:PT6CUlazIFIOLLJnV+bPBtiSH8iusKZ+FZRzZYFt2vk= cloud.google.com/go/retail v1.19.2/go.mod h1:71tRFYAcR4MhrZ1YZzaJxr030LvaZiIcupH7bXfFBcY= +cloud.google.com/go/retail v1.21.0 h1:8jgWgtAg1mk91WmaoWRTlL9CcvazPwqZ3YT9n6Gva9U= +cloud.google.com/go/retail v1.21.0/go.mod h1:LuG+QvBdLfKfO+7nnF3eA3l1j4TQw3Sg+UqlUorquRc= cloud.google.com/go/run v1.9.0 h1:9WeTqeEcriXqRViXMNwczjFJjixOSBlSlk/fW3lfKPg= cloud.google.com/go/run v1.9.0/go.mod h1:Dh0+mizUbtBOpPEzeXMM22t8qYQpyWpfmUiWQ0+94DU= +cloud.google.com/go/run v1.10.0 h1:CDhz0PPzI/cVpmNFyHe3Yp21jNpiAqtkfRxuoLi+JU0= +cloud.google.com/go/run v1.10.0/go.mod h1:z7/ZidaHOCjdn5dV0eojRbD+p8RczMk3A7Qi2L+koHg= cloud.google.com/go/scheduler v1.11.4 h1:ewVvigBnEnrr9Ih8CKnLVoB5IiULaWfYU5nEnnfVAto= cloud.google.com/go/scheduler v1.11.4/go.mod h1:0ylvH3syJnRi8EDVo9ETHW/vzpITR/b+XNnoF+GPSz4= +cloud.google.com/go/scheduler v1.11.7 h1:zkMEJ0UbEJ3O7NwEUlKLIp6eXYv1L7wHjbxyxznajKM= +cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s= cloud.google.com/go/secretmanager v1.14.5 h1:W++V0EL9iL6T2+ec24Dm++bIti0tI6Gx6sCosDBters= cloud.google.com/go/secretmanager v1.14.5/go.mod h1:GXznZF3qqPZDGZQqETZwZqHw4R6KCaYVvcGiRBA+aqY= +cloud.google.com/go/secretmanager v1.14.7 h1:VkscIRzj7GcmZyO4z9y1EH7Xf81PcoiAo7MtlD+0O80= +cloud.google.com/go/secretmanager v1.14.7/go.mod h1:uRuB4F6NTFbg0vLQ6HsT7PSsfbY7FqHbtJP1J94qxGc= cloud.google.com/go/security v1.18.3 h1:ya9gfY1ign6Yy25VMMMgZ9xy7D/TczDB0ElXcyWmEVE= cloud.google.com/go/security v1.18.3/go.mod h1:NmlSnEe7vzenMRoTLehUwa/ZTZHDQE59IPRevHcpCe4= +cloud.google.com/go/security v1.18.5 h1:6hqzvuwC8za9jyCTxygmEHnp4vZ8hfhwKVArxSCAVCo= +cloud.google.com/go/security v1.18.5/go.mod h1:D1wuUkDwGqTKD0Nv7d4Fn2Dc53POJSmO4tlg1K1iS7s= cloud.google.com/go/securitycenter v1.36.0 h1:IdDiAa7gYtL7Gdx+wEaNHimudk3ZkEGNhdz9FuEuxWM= cloud.google.com/go/securitycenter v1.36.0/go.mod h1:AErAQqIvrSrk8cpiItJG1+ATl7SD7vQ6lgTFy/Tcs4Q= +cloud.google.com/go/securitycenter v1.36.2 h1:hLA58IBYmWrNiXDIONvuCUQ4sHLVPy8JvDo2j1wSYCw= +cloud.google.com/go/securitycenter v1.36.2/go.mod h1:80ocoXS4SNWxmpqeEPhttYrmlQzCPVGaPzL3wVcoJvE= cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE= cloud.google.com/go/servicedirectory v1.12.3 h1:oFkCp6ti7fc7hzeROmOPQuPBHFqwyhcsv3Yrma28+uc= cloud.google.com/go/servicedirectory v1.12.3/go.mod h1:dwTKSCYRD6IZMrqoBCIvZek+aOYK/6+jBzOGw8ks5aY= +cloud.google.com/go/servicedirectory v1.12.6 h1:pl/KUNvFzlXpxgnPgzQjyTQQcv5WsQ97zCHaPrLQlYA= +cloud.google.com/go/servicedirectory v1.12.6/go.mod h1:OojC1KhOMDYC45oyTn3Mup08FY/S0Kj7I58dxUMMTpg= cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY= cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/shell v1.8.3 h1:mjYgUsOtV3jl9xvDmcvlRRmA64deEPf52zOfuc68b/g= cloud.google.com/go/shell v1.8.3/go.mod h1:OYcrgWF6JSp/uk76sNTtYFlMD0ho2+Cdzc7U3P/bF54= +cloud.google.com/go/shell v1.8.6 h1:jLWyztGlNWBx55QXBM4HbWvfv7aiRjPzRKTUkZA8dXk= +cloud.google.com/go/shell v1.8.6/go.mod h1:GNbTWf1QA/eEtYa+kWSr+ef/XTCDkUzRpV3JPw0LqSk= cloud.google.com/go/spanner v1.76.1 h1:vYbVZuXfnFwvNcvH3lhI2PeUA+kHyqKmLC7mJWaC4Ok= cloud.google.com/go/spanner v1.76.1/go.mod h1:YtwoE+zObKY7+ZeDCBtZ2ukM+1/iPaMfUM+KnTh/sx0= +cloud.google.com/go/spanner v1.82.0 h1:w9uO8RqEoBooBLX4nqV1RtgudyU2ZX780KTLRgeVg60= +cloud.google.com/go/spanner v1.82.0/go.mod h1:BzybQHFQ/NqGxvE/M+/iU29xgutJf7Q85/4U9RWMto0= cloud.google.com/go/speech v1.26.0 h1:qvURtJs7BQzQhbxWxwai0pT79S8KLVKJ/4W8igVkt1Y= cloud.google.com/go/speech v1.26.0/go.mod h1:78bqDV2SgwFlP/M4n3i3PwLthFq6ta7qmyG6lUV7UCA= +cloud.google.com/go/speech v1.27.1 h1:+OktATNlQc+4WH78OrQadIP4CzXb9mBucdDGCO1NrlI= +cloud.google.com/go/speech v1.27.1/go.mod h1:efCfklHFL4Flxcdt9gpEMEJh9MupaBzw3QiSOVeJ6ck= +cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= +cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= +cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= cloud.google.com/go/storagetransfer v1.12.1 h1:W3v9A7MGBN7H9sAFstyciwP/1XEQhUhZfrjclmDnpMs= cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= +cloud.google.com/go/storagetransfer v1.13.0 h1:uqKX3OgcYzR1W1YI943ZZ45id0RqA2eXXoCBSPstlbw= +cloud.google.com/go/storagetransfer v1.13.0/go.mod h1:+aov7guRxXBYgR3WCqedkyibbTICdQOiXOdpPcJCKl8= cloud.google.com/go/talent v1.8.0 h1:olv+s2g+LGXeJi+MYF1wI44/TwHaVnO0N7PiucVf5ZQ= cloud.google.com/go/talent v1.8.0/go.mod h1:/gvOzSrtMcfTL/9xWhdYaZATaxUNhQ+L+3ZaGOGs7bA= +cloud.google.com/go/talent v1.8.3 h1:wDP+++O/P1cTJBMkYlSY46k0a6atSoyO+UkBGuU9+Ao= +cloud.google.com/go/talent v1.8.3/go.mod h1:oD3/BilJpJX8/ad8ZUAxlXHCslTg2YBbafFH3ciZSLQ= cloud.google.com/go/texttospeech v1.11.0 h1:YF/RdNb+jUEp22cIZCvqiFjfA5OxGE+Dxss3mhXU7oQ= cloud.google.com/go/texttospeech v1.11.0/go.mod h1:7M2ro3I2QfIEvArFk1TJ+pqXJqhszDtxUpnIv/150As= +cloud.google.com/go/texttospeech v1.13.0 h1:oWWFQp0yFl4EJOr3opDkKH9304wUsZjgPjrTDS6S1a8= +cloud.google.com/go/texttospeech v1.13.0/go.mod h1:g/tW/m0VJnulGncDrAoad6WdELMTes8eb77Idz+4HCo= cloud.google.com/go/tpu v1.8.0 h1:BvMNijOb6Vd46Rr/SR5jWv1MPosOhVsi0UaeAGNjeds= cloud.google.com/go/tpu v1.8.0/go.mod h1:XyNzyK1xc55WvL5rZEML0Z9/TUHDfnq0uICkQw6rWMo= +cloud.google.com/go/tpu v1.8.3 h1:S4Ptq+yFIPNLEzQ/OQwiIYDNzk5I2vYmhf0SmFQOmWo= +cloud.google.com/go/tpu v1.8.3/go.mod h1:Do6Gq+/Jx6Xs3LcY2WhHyGwKDKVw++9jIJp+X+0rxRE= +cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= cloud.google.com/go/translate v1.12.3/go.mod h1:qINOVpgmgBnY4YTFHdfVO4nLrSBlpvlIyosqpGEgyEg= +cloud.google.com/go/translate v1.12.5 h1:QPMNi4WCtHwc2PPfxbyUMwdN/0+cyCGLaKi2tig41J8= +cloud.google.com/go/translate v1.12.5/go.mod h1:o/v+QG/bdtBV1d1edmtau0PwTfActvxPk/gtqdSDBi4= cloud.google.com/go/video v1.23.3 h1:C2FH+6yr6LCZC4fP0gm9FwJB/SRh5Ul88O5Sc/bL83I= cloud.google.com/go/video v1.23.3/go.mod h1:Kvh/BheubZxGZDXSb0iO6YX7ZNcaYHbLjnnaC8Qyy3g= +cloud.google.com/go/video v1.24.0 h1:KTB2BEXjGm2K/JcKxQXEgx3nSoMTByepnPZa4kln064= +cloud.google.com/go/video v1.24.0/go.mod h1:h6Bw4yUbGNEa9dH4qMtUMnj6cEf+OyOv/f2tb70G6Fk= cloud.google.com/go/videointelligence v1.12.3 h1:zNTOUQyatGQtnCJ2dR3faRtpWQOlC8wszJqwG5CtwVM= cloud.google.com/go/videointelligence v1.12.3/go.mod h1:dUA6V+NH7CVgX6TePq0IelVeBMGzvehxKPR4FGf1dtw= +cloud.google.com/go/videointelligence v1.12.6 h1:heq7jEO39sH5TycBh8TGFJ827XCxK0tIWatmBY/n0jI= +cloud.google.com/go/videointelligence v1.12.6/go.mod h1:/l34WMndN5/bt04lHodxiYchLVuWPQjCU6SaiTswrIw= cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= cloud.google.com/go/vision/v2 v2.9.3 h1:dPvfDuPqPH+Yscf0f2f1RprvKkoo+N/j0a+IbLYX7Cs= cloud.google.com/go/vision/v2 v2.9.3/go.mod h1:weAcT8aNYSgrWWVTC2PuJTc7fcXKvUeAyDq8B6HkLSg= +cloud.google.com/go/vision/v2 v2.9.5 h1:UJZ0H6UlOaYKgCn6lWG2iMAOJIsJZLnseEfzBR8yIqQ= +cloud.google.com/go/vision/v2 v2.9.5/go.mod h1:1SiNZPpypqZDbOzU052ZYRiyKjwOcyqgGgqQCI/nlx8= cloud.google.com/go/vmmigration v1.8.3 h1:dpCQq3pj2HnKdbvGTftdWymm3r4ovF7JW5z8xBcO2x4= cloud.google.com/go/vmmigration v1.8.3/go.mod h1:8CzUpK9eBzohgpL4RvBVtW4sY/sDliVyQonTFQfWcJ4= +cloud.google.com/go/vmmigration v1.8.6 h1:68hOQDhs1DOITrCrhritrwr8xy6s8QMdwDyMzMiFleU= +cloud.google.com/go/vmmigration v1.8.6/go.mod h1:uZ6/KXmekwK3JmC8PzBM/cKQmq404TTfWtThF6bbf0U= cloud.google.com/go/vmwareengine v1.3.3 h1:TfuQr5j7qriINulUMotaC/+27SQaW2thIkF3Gb6VJ38= cloud.google.com/go/vmwareengine v1.3.3/go.mod h1:G7vz05KGijha0c0dj1INRKyDAaQW8TRMZt/FrfOZVXc= +cloud.google.com/go/vmwareengine v1.3.5 h1:OsGd1SB91y9fDuzdzFngMv4UcT4cqmRxjsCsS4Xmcu8= +cloud.google.com/go/vmwareengine v1.3.5/go.mod h1:QuVu2/b/eo8zcIkxBYY5QSwiyEcAy6dInI7N+keI+Jg= cloud.google.com/go/vpcaccess v1.8.3 h1:vxVaoFM64M/ht619c4wZNF0iq0QPaMWElOh7Ns4r41A= cloud.google.com/go/vpcaccess v1.8.3/go.mod h1:bqOhyeSh/nEmLIsIUoCiQCBHeNPNjaK9M3bIvKxFdsY= +cloud.google.com/go/vpcaccess v1.8.6 h1:RYtUB9rQEijX9Tc6lQcGst58ZOzPgaYTkz6+2pyPQTM= +cloud.google.com/go/vpcaccess v1.8.6/go.mod h1:61yymNplV1hAbo8+kBOFO7Vs+4ZHYI244rSFgmsHC6E= cloud.google.com/go/webrisk v1.10.3 h1:yh0v/5n49VO4/i9pYfDm1gLJUj1Ph3Xzegn8WvK9YRA= cloud.google.com/go/webrisk v1.10.3/go.mod h1:rRAqCA5/EQOX8ZEEF4HMIrLHGTK/Y1hEQgWMnih+jAw= +cloud.google.com/go/webrisk v1.11.1 h1:yZKNB7zRxOMriLrhP5WDE+BjxXVl0wJHHZSdaYzbdVU= +cloud.google.com/go/webrisk v1.11.1/go.mod h1:+9SaepGg2lcp1p0pXuHyz3R2Yi2fHKKb4c1Q9y0qbtA= cloud.google.com/go/websecurityscanner v1.7.3 h1:/uxhVCWKXzPw5pVfnBOVjaSiQ6Bm0tDExDOCLV40thw= cloud.google.com/go/websecurityscanner v1.7.3/go.mod h1:gy0Kmct4GNLoCePWs9xkQym1D7D59ld5AjhXrjipxSs= +cloud.google.com/go/websecurityscanner v1.7.6 h1:cIPKJKZA3l7D8DfL4nxce8HGOWXBw3WAUBF0ymOW9GQ= +cloud.google.com/go/websecurityscanner v1.7.6/go.mod h1:ucaaTO5JESFn5f2pjdX01wGbQ8D6h79KHrmO2uGZeiY= cloud.google.com/go/workflows v1.13.3 h1:lNFDMranJymDEB7cTI7DI9czbc1WU0RWY9KCEv9zuDY= cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= +cloud.google.com/go/workflows v1.14.2 h1:phBz5TOAES0YGogxZ6Q7ISSudaf618lRhE3euzBpE9U= +cloud.google.com/go/workflows v1.14.2/go.mod h1:5nqKjMD+MsJs41sJhdVrETgvD5cOK3hUcAs8ygqYvXQ= codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= codeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3c= @@ -265,8 +510,12 @@ github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84J github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1 h1:o/Ws6bEqMeKZUfj1RRm3mQ51O8JGU5w+Qdg2AhHib6A= github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.7.1/go.mod h1:6QAMYBAbQeeKX+REFJMZ1nFWu9XLw/PPcjYpuc9RDFs= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0/go.mod h1:6vUKmzY17h6dpn9ZLAhM4R/rcrltBeq52qZIkUR7Oro= github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4= +github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= @@ -289,6 +538,15 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5 github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.6 h1:UucmvNRPE75F3KzT68GHhKzOPwttxiFkh1d5LTTywW8= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.6/go.mod h1:XGripOBEUAcge8IUWR/NMAB5qO9k82tkbpoewBpyjYQ= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 h1:DBjmt6/otSdULyJdVg2BlG0qGZO5tKL4VzOs0jpvw5Q= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns= github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8= @@ -310,6 +568,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= 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/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6 h1:5kUcJJAKWWI82Xnp/CaU0eu5hLlHkmm9acjowSkwCd0= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= @@ -345,10 +604,13 @@ github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fus github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4= github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 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= 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/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= 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= @@ -357,18 +619,30 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1 h1:nMp7diZObd4XEVUR0pEvn7/E13JI github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4/go.mod h1:TKKN7IQoM7uTnyuFm9bm9cw5P//ZYTl4m3htBWQ1G/c= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 h1:vlYXbindmagyVA3RS2SPd47eKZ00GZZQcr+etTviHtc= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2/go.mod h1:yGhDiLKguA3iFJYxbrQkQiNzuy+ddxesSZYWVeeEH5Q= github.com/aws/aws-sdk-go-v2/service/sns v1.31.3 h1:eSTEdxkfle2G98FE+Xl3db/XAXXVTJPNQo9K/Ar8oAI= github.com/aws/aws-sdk-go-v2/service/sns v1.31.3/go.mod h1:1dn0delSO3J69THuty5iwP0US2Glt0mx2qBBlI13pvw= +github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30= +github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY= github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3 h1:Vjqy5BZCOIsn4Pj8xzyqgGmsSqzz7y/WXbN3RgOoVrc= github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3/go.mod h1:L0enV3GCRd5iG9B64W35C4/hwsCB00Ib+DKVGTadKHI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= +github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0 h1:zQz6Q5uaC8s9734DV9UDAm2q1TEEfOvEejDBSulOapI= +github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0/go.mod h1:PUWUl5MDiYNQkUHN9Pyd9kgtA/YhbxnSnHP+yQqzrM8= 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/baidubce/bce-sdk-go v0.9.188 h1:8MA7ewe4VpX01uYl7Kic6ZvfIReUFdSKbY46ZqlQM7U= github.com/baidubce/bce-sdk-go v0.9.188/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= +github.com/bazelbuild/rules_go v0.49.0 h1:5vCbuvy8Q11g41lseGJDc5vxhDjJtfxr6nM/IC4VmqM= +github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC8+vGZA= github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -410,8 +684,13 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyY github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= 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-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 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 v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ= @@ -442,6 +721,8 @@ github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37g github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= @@ -516,6 +797,13 @@ github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLix github.com/elastic/lunes v0.1.0 h1:amRtLPjwkWtzDF/RKzcEPMvSsSseLDLW+bnhfNSLRe4= github.com/elastic/lunes v0.1.0/go.mod h1:xGphYIt3XdZRtyWosHQTErsQTd4OP1p9wsbVoHelrd4= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= +github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= +github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/expr-lang/expr v1.17.0/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 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.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= @@ -526,6 +814,7 @@ github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c h1:yKN46XJHYC github.com/fluent/fluent-bit-go v0.0.0-20230731091245-a7a013e2473c/go.mod h1:L92h+dgwElEyUuShEwjbiHjseW410WIcNz+Bjutc8YQ= 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/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= @@ -544,14 +833,22 @@ github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXW github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -577,16 +874,27 @@ github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/googleapis/cloud-bigtable-clients-test v0.0.3 h1:afMKTvA/jc6jSTMkeHBZGFDTt8Cc+kb1ATFzqMK85hw= +github.com/googleapis/cloud-bigtable-clients-test v0.0.3/go.mod h1:TWtDzrrAI70C3dNLDY+nZN3gxHtFdZIbpL9rCTFyxE0= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0= 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= @@ -598,17 +906,71 @@ 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.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= +github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3UqCIpoy9Z2wgKGrZ4A5vz+yQlP9ZXvCfEkiA= +github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05 h1:hMzOzI/S0nkZt0nUqpfAa4Rdb+YL8z8oG3pl4Jb31h8= +github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05/go.mod h1:K3YAJumchx5EEZItGv4D3pCv/Ux796hmoOibP/p/eYk= +github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309 h1:H2p3XKDHnTBGkMXLCgXiqb2dFnHbQ4zPDXOwKK4Ne3Y= +github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309/go.mod h1:pMfhRxL2LZ3Pm8iy7VcVsb9CLYuBtjFYbf1oxgx7yFA= +github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945 h1:3imTbxFpZSVI6IBIB9mn+Xc40lUweWjfMaBSgXR7rLs= +github.com/grafana/alerting v0.0.0-20250701210250-cea2d1683945/go.mod h1:gtR7agmxVfJOmNKV/n2ZULgOYTYNL+PDKYB5N48tQ7Q= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= +github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= +github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/authlib/types v0.0.0-20250314102521-a77865c746c0/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= +github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= +github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4= +github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= +github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0= +github.com/grafana/grafana-aws-sdk v0.38.2/go.mod h1:j3vi+cXYHEFqjhBGrI6/lw1TNM+dl0Y3f0cSnDOPy+s= +github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= +github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE= +github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= +github.com/grafana/grafana-plugin-sdk-go v0.269.1/go.mod h1:yv2KbO4mlr9WuDK2f+2gHAMTwwLmLuqaEnrPXTRU+OI= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= +github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagIr8sWr1RT9h7ufJl5NMcWJAU= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= +github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= +github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= +github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250416173722-ec17e0e4ce03/go.mod h1:oemrhKvFxxc5m32xKHPxInEHAObH0/hPPyHUiBUZ1Cc= +github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250506052906-7a2fc797fb4a/go.mod h1:VkX53kBiqIMHBoGgeEDJnzm5Nwcmv/726tuZuT5SvJY= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250616135341-59c2f154336b/go.mod h1:OIlvNnUufYDhBXa4xK4CyzPI2C69ZJkHy5+aFDyPtXw= +github.com/grafana/grafana/apps/dashboard v0.0.0-20250616145019-8d27f12428cb/go.mod h1:OIlvNnUufYDhBXa4xK4CyzPI2C69ZJkHy5+aFDyPtXw= +github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d/go.mod h1:HQprw3MmiYj5OUV9CZnkwA1FKDZBmYACuAB3oDvUOmI= +github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d/go.mod h1:DjJe5osrW/BKrzN9hAAOSElNWutj1bcriExa7iDP7kA= +github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= +github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d/go.mod h1:CXpwZ3Mkw6xVlGKc0SqUxqXCP3Uv182q6qAQnLaLxRg= +github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:xrKQcxQxz+IUF90ybtfENFeEXtlj9nAsX/3Fw0KEIeQ= +github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed h1:59JF1WhHLT+lNX89Tm1OzOEySMVMASAhaPbsRjtp8Kc= +github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG5fpuJQRNO1lUSj9nc18Xl3O7M8fjIlBO1cI= +github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU= +github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -620,7 +982,6 @@ github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6 github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= @@ -658,6 +1019,7 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw= github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a h1:sfe532Ipn7GX0V6mHdynBk393rDmqgI0QmjLK7ct7TU= github.com/joncrlsn/dque v0.0.0-20211108142734-c2ef48c5192a/go.mod h1:dNKs71rs2VJGBAmttu7fouEsRQlRjxy0p1Sx+T5wbpY= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= @@ -760,6 +1122,8 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I= github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ= github.com/ncw/swift v1.0.53 h1:luHjjTNtekIEvHg5KdAFIBaH7bWfNkefwFnpDffSIks= github.com/ncw/swift v1.0.53/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= @@ -768,6 +1132,9 @@ github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItc github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1 h1:+aiMrDR6xiaDM7xN4ByrBYI0Craqt68nZicmpYpt0co= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1/go.mod h1:H/TEWN4jgExt0McrtrBK2VFK6r9LRsWtqhEZrH690rs= github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I= @@ -872,8 +1239,13 @@ github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5Bdj github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6 h1:XqtxwYFCjS4L0o1QD4ipGHCuFG94U0f6BeldbilGQjU= github.com/substrait-io/substrait v0.66.1-0.20250205013839-a30b3e2d7ec6/go.mod h1:MPFNw6sToJgpD5Z2rj0rQrdP/Oq8HG7Z2t3CAEHtkHw= github.com/substrait-io/substrait-go/v3 v3.9.1 h1:2yfHDHpK6KMcvLd0bJVzUJoeXO+K98yS+ciBruxD9po= @@ -965,6 +1337,9 @@ github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ= +go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA= go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o= go.opentelemetry.io/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= @@ -1081,54 +1456,184 @@ go.opentelemetry.io/collector/service/hostcapabilities v0.124.0 h1:ArxbARF7+bnzK go.opentelemetry.io/collector/service/hostcapabilities v0.124.0/go.mod h1:vifQsB+lkeCsjBCRPVHca9lJ3pLpLPZKCGrG77nkxFQ= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= +go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/otelconf v0.15.0 h1:BLNiIUsrNcqhSKpsa6CnhE6LdrpY1A8X0szMVsu99eo= go.opentelemetry.io/contrib/otelconf v0.15.0/go.mod h1:OPH1seO5z9dp1P26gnLtoM9ht7JDvh3Ws6XRHuXqImY= go.opentelemetry.io/contrib/propagators/b3 v1.35.0 h1:DpwKW04LkdFRFCIgM3sqwTJA/QREHMeMHYPWP1WeaPQ= go.opentelemetry.io/contrib/propagators/b3 v1.35.0/go.mod h1:9+SNxwqvCWo1qQwUpACBY5YKNVxFJn5mlbXg/4+uKBg= go.opentelemetry.io/contrib/zpages v0.60.0 h1:wOM9ie1Hz4H88L9KE6GrGbKJhfm+8F1NfW/Y3q9Xt+8= go.opentelemetry.io/contrib/zpages v0.60.0/go.mod h1:xqfToSRGh2MYUsfyErNz8jnNDPlnpZqWM/y6Z2Cx7xw= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/bridge/opencensus v1.35.0 h1:4nJfffRbozhqnuukfRkiahA94mnpryCLJLiduMIDJKI= go.opentelemetry.io/otel/bridge/opencensus v1.35.0/go.mod h1:359S30saRYNsB4A46EDx91SpXsQFNgkma7ftg2/L5/M= go.opentelemetry.io/otel/bridge/opentracing v1.35.0 h1:qT4jl1fYl0hHuRopNcwS94QosLFhGYcS0HacPUeXmT4= go.opentelemetry.io/otel/bridge/opentracing v1.35.0/go.mod h1:p5CbIL4v7uQz7mnQD6T/AZc1pPUzwz+2wZ1zrGY9Kgs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.15.2 h1:Tlfh/jBk2tqjLZ4/P8ZIwGrLEWQSPDLRm/SNWKNXiGI= gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= +google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0= +google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE= +google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= +google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI= +google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M= +google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= +google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0= +google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= 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-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= +google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= +google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= +google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU= +google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= +google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= +google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= +google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2 h1:DbpkGFGRkd4GORg+IWQW2EhxUaa/My/PM8d1CGyTDMY= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9 h1:YI36gCL8AQMhzYN6+jH8PdV/iZ0On+Zd0rO/7lCH3k8= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= @@ -1147,7 +1652,10 @@ howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= k8s.io/code-generator v0.33.1 h1:ZLzIRdMsh3Myfnx9BaooX6iQry29UJjVfVG+BuS+UMw= k8s.io/code-generator v0.33.1/go.mod h1:HUKT7Ubp6bOgIbbaPIs9lpd2Q02uqkMCMx9/GjDrWpY= +k8s.io/code-generator v0.33.2 h1:PCJ0Y6viTCxxJHMOyGqYwWEteM4q6y1Hqo2rNpl6jF4= +k8s.io/code-generator v0.33.2/go.mod h1:hBjCA9kPMpjLWwxcr75ReaQfFXY8u+9bEJJ7kRw3J8c= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= +k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= @@ -1167,4 +1675,5 @@ rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= diff --git a/jest.config.js b/jest.config.js index a57ea5c2067..0bee3d4cfe7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,16 +5,22 @@ process.env.TZ = 'Pacific/Easter'; // UTC-06:00 or UTC-05:00 depending on daylig const esModules = [ '@glideapps/glide-data-grid', + '@wojtekmaj/date-utils', 'ol', 'd3', 'd3-color', 'd3-interpolate', 'delaunator', + 'get-user-locale', 'internmap', 'robust-predicates', 'leven', 'nanoid', + 'marked', + 'memoize', + 'mimic-function', 'monaco-promql', + 'react-calendar', '@kusto/monaco-kusto', 'monaco-editor', '@msagl', @@ -24,6 +30,11 @@ const esModules = [ 'react-data-grid', '@grafana/llm', 'pkce-challenge', + 'quickselect', + 'rbush', + 'earcut', + 'pbf', + 'geotiff', ].join('|'); module.exports = { diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index bbf3cb24321..1b325f7300b 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -215,6 +215,10 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Additional static options for query variable + staticOptions?: [...#VariableOption] + // Ordering of static options in relation to options returned from data source for query variable + staticOptionsOrder?: "before" | "after" | "sorted" ... } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) diff --git a/package.json b/package.json index 88066d57d79..231711d3d3b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", "e2e:playwright": "yarn playwright test", - "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", + "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e-playwright/start-and-run-suite", + "e2e:playwright:storybook": "yarn playwright test -c playwright.storybook.config.ts", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", "e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'", "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", @@ -111,14 +112,13 @@ "@testing-library/user-event": "14.6.1", "@types/babel__core": "^7", "@types/babel__preset-env": "^7", - "@types/chance": "^1.1.3", + "@types/chance": "^1.1.7", "@types/common-tags": "^1.8.0", "@types/confusing-browser-globals": "^1", "@types/d3": "7.4.3", "@types/d3-force": "^3.0.0", "@types/d3-scale-chromatic": "3.1.0", "@types/debounce-promise": "3.1.9", - "@types/diff": "^7.0.0", "@types/eslint": "9.6.1", "@types/eslint-scope": "^3.7.7", "@types/file-saver": "2.0.7", @@ -131,7 +131,7 @@ "@types/jquery": "3.5.32", "@types/js-yaml": "^4.0.5", "@types/jsurl": "^1.2.28", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", "@types/node": "22.15.0", @@ -148,7 +148,7 @@ "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.20", "@types/react-transition-group": "4.4.12", - "@types/react-virtualized-auto-sizer": "1.0.4", + "@types/react-virtualized-auto-sizer": "1.0.8", "@types/react-window": "1.8.8", "@types/react-window-infinite-loader": "^1", "@types/redux-mock-store": "1.5.0", @@ -157,31 +157,31 @@ "@types/slate-plain-serializer": "0.7.5", "@types/slate-react": "0.22.9", "@types/swagger-ui-react": "5.18.0", - "@types/systemjs": "6.15.1", + "@types/systemjs": "6.15.3", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", "@types/webpack-assets-manifest": "^5", "@types/webpack-env": "^1.18.4", "@types/yargs": "17.0.33", - "@typescript-eslint/eslint-plugin": "8.22.0", - "@typescript-eslint/parser": "8.22.0", - "autoprefixer": "10.4.20", + "@typescript-eslint/eslint-plugin": "8.35.1", + "@typescript-eslint/parser": "8.35.1", + "autoprefixer": "10.4.21", "babel-loader": "9.2.1", "blob-polyfill": "9.0.20240710", "browserslist": "^4.21.4", - "chance": "^1.0.10", - "chrome-remote-interface": "0.33.2", + "chance": "^1.1.13", + "chrome-remote-interface": "0.33.3", "codeowners": "^5.1.1", "confusing-browser-globals": "^1.0.11", "copy-webpack-plugin": "12.0.2", "core-js": "3.40.0", "crashme": "0.0.15", "css-loader": "7.1.2", - "css-minimizer-webpack-plugin": "7.0.0", + "css-minimizer-webpack-plugin": "7.0.2", "cypress": "14.3.2", "cypress-file-upload": "5.0.8", "cypress-recurse": "^1.35.3", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "esbuild-loader": "4.3.0", "esbuild-plugin-browserslist": "^1.0.0", "eslint": "9.19.0", @@ -193,16 +193,16 @@ "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-lodash": "8.0.0", "eslint-plugin-no-barrel-files": "^1.1.1", - "eslint-plugin-react": "7.37.4", + "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.1.0", "eslint-plugin-testing-library": "^7.0.0", "eslint-plugin-unicorn": "^56.0.0", "eslint-scope": "^8.1.0", "eslint-webpack-plugin": "4.2.0", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "fishery": "^2.2.2", "fork-ts-checker-webpack-plugin": "9.0.2", - "glob": "11.0.1", + "glob": "11.0.3", "html-loader": "5.1.0", "html-webpack-plugin": "5.6.3", "http-server": "14.1.1", @@ -218,9 +218,9 @@ "jest-watch-typeahead": "^2.2.2", "jimp": "^1.6.0", "jsdom-testing-mocks": "^1.13.1", - "lerna": "8.2.1", + "lerna": "8.2.3", "mini-css-extract-plugin": "2.9.2", - "msw": "2.10.2", + "msw": "2.10.4", "mutationobserver-shim": "0.3.7", "node-notifier": "10.0.1", "nx": "20.7.1", @@ -228,7 +228,7 @@ "pa11y-ci": "^3.1.0", "pdf-parse": "^1.1.1", "plop": "^4.0.1", - "postcss": "8.5.1", + "postcss": "8.5.6", "postcss-loader": "8.1.1", "postcss-reporter": "7.1.0", "postcss-scss": "4.0.9", @@ -249,7 +249,7 @@ "tracelib": "1.0.1", "ts-jest": "29.2.5", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1", "webpack-assets-manifest": "^5.1.0", "webpack-cli": "6.0.1", @@ -268,7 +268,7 @@ "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@fingerprintjs/fingerprintjs": "^3.4.2", - "@floating-ui/react": "0.27.12", + "@floating-ui/react": "0.27.13", "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", @@ -280,17 +280,17 @@ "@grafana/faro-web-sdk": "^1.13.2", "@grafana/faro-web-tracing": "^1.13.2", "@grafana/flamegraph": "workspace:*", - "@grafana/google-sdk": "0.3.2", + "@grafana/google-sdk": "0.3.4", "@grafana/i18n": "workspace:*", - "@grafana/lezer-logql": "0.2.7", + "@grafana/lezer-logql": "0.2.8", "@grafana/llm": "0.22.1", "@grafana/monaco-logql": "^0.0.8", "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/plugin-ui": "0.10.7", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "^6.26.1", - "@grafana/scenes-react": "^6.26.1", + "@grafana/scenes": "^6.27.2", + "@grafana/scenes-react": "^6.27.2", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -336,12 +336,12 @@ "dangerously-set-html-content": "1.1.0", "date-fns": "4.1.0", "debounce-promise": "3.1.2", - "diff": "^7.0.0", + "diff": "^8.0.0", "fast-deep-equal": "^3.1.3", "fast-json-patch": "3.1.1", "file-saver": "2.0.5", "history": "4.10.1", - "i18next": "^24.0.0", + "i18next": "^25.0.0", "i18next-browser-languagedetector": "^8.0.0", "i18next-pseudo": "^2.2.1", "immer": "10.1.1", @@ -356,10 +356,11 @@ "leven": "^4.0.0", "lodash": "4.17.21", "logfmt": "^1.3.2", + "lossless-json": "^4.1.1", "lru-cache": "11.1.0", "lru-memoize": "^1.1.0", "lucene": "^2.1.1", - "marked": "15.0.12", + "marked": "16.0.0", "memoize-one": "6.0.0", "micro-memoize": "^4.1.2", "ml-regression-polynomial": "^3.0.0", @@ -370,7 +371,7 @@ "moveable": "0.53.0", "nanoid": "^5.0.9", "node-forge": "^1.3.1", - "ol": "7.4.0", + "ol": "10.6.0", "ol-ext": "4.0.33", "pluralize": "^8.0.0", "prismjs": "1.30.0", @@ -380,7 +381,7 @@ "react": "18.3.1", "react-diff-viewer-continued": "^3.4.0", "react-dom": "18.3.1", - "react-draggable": "4.4.6", + "react-draggable": "4.5.0", "react-dropzone": "^14.2.3", "react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch", "react-highlight-words": "0.21.0", @@ -395,13 +396,13 @@ "react-router": "5.3.4", "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-split-pane": "0.1.92", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.6.0", "react-virtual": "2.10.4", - "react-virtualized-auto-sizer": "1.0.25", + "react-virtualized-auto-sizer": "1.0.26", "react-window": "1.8.11", "react-window-infinite-loader": "1.0.10", "react-zoom-pan-pinch": "^3.3.0", @@ -416,7 +417,7 @@ "slate": "0.47.9", "slate-plain-serializer": "0.7.13", "slate-react": "0.22.10", - "swagger-ui-react": "5.25.2", + "swagger-ui-react": "5.26.2", "symbol-observable": "4.0.0", "systemjs": "6.15.1", "tinycolor2": "1.6.0", @@ -425,7 +426,8 @@ "type-fest": "^4.18.2", "uplot": "1.6.32", "uuid": "11.1.0", - "visjs-network": "4.25.0", + "vis-data": "^7.1.10", + "vis-network": "9.1.13", "whatwg-fetch": "3.6.20" }, "resolutions": { @@ -444,13 +446,16 @@ "@grafana/scenes/@grafana/e2e-selectors": "workspace:*", "@grafana/scenes-react/@grafana/e2e-selectors": "workspace:*", "swagger-ui-react/dompurify": "3.2.6", - "refractor/prismjs": "^1.27.0" + "refractor/prismjs": "^1.27.0", + "@mapbox/jsonlint-lines-primitives": "github:mapbox/jsonlint#commit=e31b7289baedf3e1000d7ae7edd42268212c9954", + "get-document": "github:webmodules/get-document#commit=a04ccb499d6e0433a368c3bb150b4899b698461f", + "gitconfiglocal": "2.1.0" }, "workspaces": { "packages": [ "packages/*", "public/app/plugins/*/*", - "e2e/test-plugins/*" + "e2e-playwright/test-plugins/*" ] }, "engines": { diff --git a/packages/grafana-alerting/package.json b/packages/grafana-alerting/package.json index d02050db6ac..32ab506a757 100644 --- a/packages/grafana-alerting/package.json +++ b/packages/grafana-alerting/package.json @@ -78,7 +78,7 @@ "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "type-fest": "^4.40.0", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "@grafana/runtime": ">=11.6 <= 12.x", diff --git a/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts b/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts index cc4effa3d3e..acd8045d63e 100644 --- a/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts +++ b/packages/grafana-alerting/src/grafana/api/v0alpha1/types.ts @@ -51,7 +51,26 @@ type SlackIntegration = OverrideProperties< } >; -export type Integration = EmailIntegration | SlackIntegration | GenericIntegration; +// Based on https://github.com/grafana/alerting/blob/main/receivers/oncall/config.go#L14-L27 +type OnCallIntegration = OverrideProperties< + GenericIntegration, + { + type: 'OnCall'; + settings: { + url: string; + httpMethod?: 'POST' | 'PUT'; + maxAlerts?: number; + authorization_scheme?: string; + authorization_credentials?: string; + username?: string; + password?: string; + title?: string; + message?: string; + }; + } +>; + +export type Integration = EmailIntegration | SlackIntegration | OnCallIntegration | GenericIntegration; // Enhanced version of ContactPoint with typed integrations // ⚠️ MergeDeep does not check if the property you are overriding exists in the base type and there is no "DeepOverrideProperties" helper diff --git a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx index 3107319a9ea..56dacd8dca5 100644 --- a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx +++ b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.tsx @@ -3,7 +3,7 @@ import { chain } from 'lodash'; import { Combobox, ComboboxOption } from '@grafana/ui'; import type { ContactPoint } from '../../../api/v0alpha1/types'; -import { useListContactPointsv0alpha1 } from '../../hooks/useContactPoints'; +import { useListContactPoints } from '../../hooks/v0alpha1/useContactPoints'; import { getContactPointDescription } from '../../utils'; import { CustomComboBoxProps } from './ComboBox.types'; @@ -17,7 +17,7 @@ export type ContactPointSelectorProps = CustomComboBoxProps; * @TODO make ComboBox accept a ReactNode so we can use icons and such */ function ContactPointSelector(props: ContactPointSelectorProps) { - const { currentData: contactPoints, isLoading } = useListContactPointsv0alpha1(); + const { currentData: contactPoints, isLoading } = useListContactPoints(); // Create a mapping of options with their corresponding contact points const contactPointOptions = chain(contactPoints?.items) diff --git a/packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx b/packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx deleted file mode 100644 index ab0169f2891..00000000000 --- a/packages/grafana-alerting/src/grafana/contactPoints/hooks/useContactPoints.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { type TypedUseQueryHookResult, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; - -import { type ListReceiverApiArg, alertingAPI } from '../../api/v0alpha1/api.gen'; -import type { EnhancedListReceiverApiResponse } from '../../api/v0alpha1/types'; - -// this is a workaround for the fact that the generated types are not narrow enough -type EnhancedHookResult = TypedUseQueryHookResult< - EnhancedListReceiverApiResponse, - ListReceiverApiArg, - ReturnType ->; - -/** - * useListContactPoints is a hook that fetches a list of contact points - * - * This function wraps the alertingAPI.useListReceiverQuery with proper typing - * to ensure that the returned ContactPoints are correctly typed in the data.items array. - * - * It automatically uses the configured namespace for the query. - */ -function useListContactPointsv0alpha1() { - return alertingAPI.useListReceiverQuery({}); -} - -export { useListContactPointsv0alpha1 }; diff --git a/packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx b/packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx new file mode 100644 index 00000000000..25d1ab9a6bf --- /dev/null +++ b/packages/grafana-alerting/src/grafana/contactPoints/hooks/v0alpha1/useContactPoints.tsx @@ -0,0 +1,62 @@ +import { + type TypedUseMutationResult, + type TypedUseQueryHookResult, + fetchBaseQuery, +} from '@reduxjs/toolkit/query/react'; +import { OverrideProperties } from 'type-fest'; + +import { CreateReceiverApiArg, type ListReceiverApiArg, alertingAPI } from '../../../api/v0alpha1/api.gen'; +import type { ContactPoint, EnhancedListReceiverApiResponse } from '../../../api/v0alpha1/types'; + +// this is a workaround for the fact that the generated types are not narrow enough +type ListContactPointsHookResult = TypedUseQueryHookResult< + EnhancedListReceiverApiResponse, + ListReceiverApiArg, + ReturnType +>; + +/** + * useListContactPoints is a hook that fetches a list of contact points + * + * This function wraps the alertingAPI.useListReceiverQuery with proper typing + * to ensure that the returned ContactPoints are correctly typed in the data.items array. + * + * It automatically uses the configured namespace for the query. + */ +export function useListContactPoints() { + return alertingAPI.useListReceiverQuery({}); +} + +// type narrowing mutations requires us to define a few helper types +type CreateContactPointArgs = OverrideProperties< + CreateReceiverApiArg, + { receiver: Omit } +>; + +type CreateContactPointMutation = TypedUseMutationResult< + ContactPoint, + CreateContactPointArgs, + ReturnType +>; + +type UseCreateContactPointOptions = Parameters< + typeof alertingAPI.endpoints.createReceiver.useMutation +>[0]; + +/** + * useCreateContactPoint is a hook that creates a new contact point with one or more integrations + * + * This function wraps the alertingAPI.useCreateReceiverMutation with proper typing + * to ensure that the payload supports type narrowing. + */ +export function useCreateContactPoint(options?: UseCreateContactPointOptions) { + const [updateFn, result] = alertingAPI.endpoints.createReceiver.useMutation(options); + + const typedUpdateFn = (args: CreateContactPointArgs) => { + // @ts-expect-error this one is just impossible for me to figure out + const response = updateFn(args); + return response; + }; + + return [typedUpdateFn, result] as const; +} diff --git a/packages/grafana-alerting/src/unstable.ts b/packages/grafana-alerting/src/unstable.ts index f1e7ce62356..5c10dcbf3eb 100644 --- a/packages/grafana-alerting/src/unstable.ts +++ b/packages/grafana-alerting/src/unstable.ts @@ -4,14 +4,8 @@ // Contact Points export * from './grafana/api/v0alpha1/types'; -export { useListContactPointsv0alpha1 } from './grafana/contactPoints/hooks/useContactPoints'; +export { useListContactPoints } from './grafana/contactPoints/hooks/v0alpha1/useContactPoints'; export { ContactPointSelector } from './grafana/contactPoints/components/ContactPointSelector/ContactPointSelector'; // Low-level API hooks -export { alertingAPI as alertingAPIv0alpha1 } from './grafana/api/v0alpha1/api.gen'; - -// model factories / mocks -export * as mocksV0alpha1 from './grafana/api/v0alpha1/mocks/fakes/Receivers'; - -// MSW handlers -export * as handlersV0alpha1 from './grafana/api/v0alpha1/mocks/handlers'; +export { alertingAPI } from './grafana/api/v0alpha1/api.gen'; diff --git a/packages/grafana-alerting/tests/provider.tsx b/packages/grafana-alerting/tests/provider.tsx index 039015eb8bf..4331f7e6b8a 100644 --- a/packages/grafana-alerting/tests/provider.tsx +++ b/packages/grafana-alerting/tests/provider.tsx @@ -2,13 +2,13 @@ import { configureStore } from '@reduxjs/toolkit'; import { useEffect } from 'react'; import { Provider } from 'react-redux'; -import { alertingAPIv0alpha1 } from '../src/unstable'; +import { alertingAPI } from '../src/unstable'; // create an empty store export const store = configureStore({ - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPIv0alpha1.middleware), + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPI.middleware), reducer: { - [alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer, + [alertingAPI.reducerPath]: alertingAPI.reducer, }, }); @@ -35,7 +35,7 @@ export const getDefaultWrapper = () => { function useResetQueryCacheAfterUnmount() { useEffect(() => { return () => { - store.dispatch(alertingAPIv0alpha1.util.resetApiState()); + store.dispatch(alertingAPI.util.resetApiState()); }; }, []); } diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index cf964ed7fb7..64a027650c9 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -60,7 +60,7 @@ "@leeoniya/ufuzzy": "1.0.18", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", - "@types/systemjs": "6.15.1", + "@types/systemjs": "6.15.3", "d3-interpolate": "3.0.1", "date-fns": "4.1.0", "dompurify": "3.2.6", @@ -68,11 +68,11 @@ "fast_array_intersect": "1.1.0", "history": "4.10.1", "lodash": "4.17.21", - "marked": "15.0.12", - "marked-mangle": "1.1.10", + "marked": "16.0.0", + "marked-mangle": "1.1.11", "moment": "2.30.1", "moment-timezone": "0.5.47", - "ol": "7.4.0", + "ol": "10.6.0", "papaparse": "5.5.3", "react-use": "17.6.0", "rxjs": "7.8.2", @@ -84,22 +84,22 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "16.0.1", "@types/history": "4.7.11", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/papaparse": "5.3.16", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/tinycolor2": "1.4.6", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "react": "18.3.1", "react-dom": "18.3.1", "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx b/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx index 1464f1a6f6c..db5b5a5b610 100644 --- a/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx +++ b/packages/grafana-data/src/context/plugins/DataSourcePluginContextProvider.tsx @@ -2,7 +2,7 @@ import { PropsWithChildren, ReactElement, useMemo } from 'react'; import { DataSourceInstanceSettings } from '../../types/datasource'; -import { Context, DataSourcePluginContextType } from './PluginContext'; +import { PluginContext, DataSourcePluginContextType } from './PluginContext'; export type DataSourcePluginContextProviderProps = { instanceSettings: DataSourceInstanceSettings; @@ -16,5 +16,5 @@ export function DataSourcePluginContextProvider( return { instanceSettings, meta: instanceSettings.meta }; }, [instanceSettings]); - return {children}; + return {children}; } diff --git a/packages/grafana-data/src/context/plugins/PluginContext.tsx b/packages/grafana-data/src/context/plugins/PluginContext.tsx index f8748617541..7471109d93d 100644 --- a/packages/grafana-data/src/context/plugins/PluginContext.tsx +++ b/packages/grafana-data/src/context/plugins/PluginContext.tsx @@ -1,14 +1,15 @@ import { createContext } from 'react'; +import { KeyValue } from '../../types/data'; import { DataSourceInstanceSettings } from '../../types/datasource'; import { PluginMeta } from '../../types/plugin'; -export interface PluginContextType { - meta: PluginMeta; +export interface PluginContextType { + meta: PluginMeta; } -export interface DataSourcePluginContextType extends PluginContextType { +export interface DataSourcePluginContextType extends PluginContextType { instanceSettings: DataSourceInstanceSettings; } -export const Context = createContext(undefined); +export const PluginContext = createContext(undefined); diff --git a/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx b/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx index f60381c5aaa..c7698b18a34 100644 --- a/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx +++ b/packages/grafana-data/src/context/plugins/PluginContextProvider.tsx @@ -2,7 +2,7 @@ import { PropsWithChildren, ReactElement } from 'react'; import { PluginMeta } from '../../types/plugin'; -import { Context } from './PluginContext'; +import { PluginContext } from './PluginContext'; export type PluginContextProviderProps = { meta: PluginMeta; @@ -10,5 +10,5 @@ export type PluginContextProviderProps = { export function PluginContextProvider(props: PropsWithChildren): ReactElement { const { children, ...rest } = props; - return {children}; + return {children}; } diff --git a/packages/grafana-data/src/context/plugins/guards.ts b/packages/grafana-data/src/context/plugins/guards.ts index 3a93c862f09..3b89b24dfed 100644 --- a/packages/grafana-data/src/context/plugins/guards.ts +++ b/packages/grafana-data/src/context/plugins/guards.ts @@ -1,5 +1,9 @@ +import { KeyValue } from '../../types/data'; + import { type DataSourcePluginContextType, type PluginContextType } from './PluginContext'; -export function isDataSourcePluginContext(context: PluginContextType): context is DataSourcePluginContextType { +export function isDataSourcePluginContext( + context: PluginContextType +): context is DataSourcePluginContextType { return 'instanceSettings' in context && 'meta' in context; } diff --git a/packages/grafana-data/src/context/plugins/usePluginContext.tsx b/packages/grafana-data/src/context/plugins/usePluginContext.tsx index 58190a7e4e1..b00875909d7 100644 --- a/packages/grafana-data/src/context/plugins/usePluginContext.tsx +++ b/packages/grafana-data/src/context/plugins/usePluginContext.tsx @@ -1,9 +1,11 @@ import { useContext } from 'react'; -import { Context, PluginContextType } from './PluginContext'; +import { KeyValue } from '../../types/data'; -export function usePluginContext(): PluginContextType | null { - const context = useContext(Context); +import { PluginContext, PluginContextType } from './PluginContext'; + +export function usePluginContext(): PluginContextType | null { + const context = useContext(PluginContext); // The extensions hooks (e.g. `usePluginLinks()`) are using this hook to check // if they are inside a plugin or not (core Grafana), so we should be able to return an empty state as well (`null`). @@ -11,5 +13,6 @@ export function usePluginContext(): PluginContextType | null { return null; } - return context; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return context as PluginContextType; } diff --git a/packages/grafana-data/src/field/fieldColor.test.ts b/packages/grafana-data/src/field/fieldColor.test.ts index 9f293aab072..bcfa9648df4 100644 --- a/packages/grafana-data/src/field/fieldColor.test.ts +++ b/packages/grafana-data/src/field/fieldColor.test.ts @@ -47,7 +47,7 @@ describe('fieldColorModeRegistry', () => { it('Palette classic with series index 1', () => { const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 1, name: 'series2' }); - expect(calcFn(70, 0, undefined)).toEqual('#F2CC0C'); + expect(calcFn(70, 0, undefined)).toEqual('#FADE2A'); }); it('Palette uses name', () => { @@ -113,8 +113,8 @@ describe('fieldColorModeRegistry', () => { const calcFn1 = mode.getCalculator(frames[0].fields[1], createTheme()); const calcFn2 = mode.getCalculator(frames[1].fields[1], createTheme()); - expect(calcFn1(0, 0)).toEqual('#82B5D8'); - expect(calcFn2(0, 0)).toEqual('#FCE2DE'); + expect(calcFn1(0, 0)).toEqual('#5195CE'); + expect(calcFn2(0, 0)).toEqual('#37872D'); }); it('When color.seriesBy is set to last use that instead of v', () => { diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index 8c7702c2bb0..a7957f9f44e 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -218,7 +218,7 @@ export class FieldColorSchemeMode implements FieldColorMode { } } else if (this.useSeriesName) { return (_: number, _percent: number, _threshold?: Threshold) => { - return colors[Math.abs(stringHash(field.state?.displayName ?? field.name)) % colors.length]; + return getColorByStringHash(colors, field.state?.displayName ?? field.name); }; } else { return (_: number, _percent: number, _threshold?: Threshold) => { @@ -263,6 +263,10 @@ export function getFieldSeriesColor(field: Field, theme: GrafanaTheme2): ColorSc return scale(value); } +export function getColorByStringHash(colors: string[], string: string) { + return colors[Math.abs(stringHash(string)) % colors.length]; +} + function getFixedColor(field: Field, theme: GrafanaTheme2) { return () => { return theme.visualization.getColorByName(field.config.color?.fixedColor ?? FALLBACK_COLOR); diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index 718c0e54430..d4419310bad 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -388,7 +388,7 @@ describe('FieldDisplay', () => { const result = getFieldDisplayValues(options); expect(result[0].display.color).toEqual('#73BF69'); - expect(result[1].display.color).toEqual('#F2CC0C'); + expect(result[1].display.color).toEqual('#FADE2A'); }); it('When showing all values lookup color via an override', () => { @@ -435,7 +435,7 @@ describe('FieldDisplay', () => { const result = getFieldDisplayValues(options); expect(result[0].display.color).toEqual('#AAA'); - expect(result[1].display.color).toEqual('#F2CC0C'); + expect(result[1].display.color).toEqual('#FADE2A'); }); it('Multiple other string fields', () => { diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 0fd6e2d834e..4d62d1794df 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -1,4 +1,3 @@ -import { ArrayDataFrame } from '../dataframe/ArrayDataFrame'; import { createDataFrame, toDataFrame } from '../dataframe/processDataFrame'; import { relativeToTimeRange } from '../datetime/rangeutil'; import { createTheme } from '../themes/createTheme'; @@ -83,7 +82,7 @@ locationUtil.initialize({ describe('Global MinMax', () => { it('find global min max', () => { - const f0 = new ArrayDataFrame<{ title: string; value: number; value2: number | null }>([ + const f0 = toDataFrame([ { title: 'AAA', value: 100, value2: 1234 }, { title: 'BBB', value: -20, value2: null }, { title: 'CCC', value: 200, value2: 1000 }, @@ -95,7 +94,7 @@ describe('Global MinMax', () => { }); it('find global min max when all values are zero', () => { - const f0 = new ArrayDataFrame<{ title: string; value: number; value2: number | null }>([ + const f0 = toDataFrame([ { title: 'AAA', value: 0, value2: 0 }, { title: 'CCC', value: 0, value2: 0 }, ]); @@ -144,7 +143,7 @@ describe('Global MinMax', () => { }); describe('applyFieldOverrides', () => { - const f0 = new ArrayDataFrame<{ title: string; value: number; value2: number | null }>([ + const f0 = toDataFrame([ { title: 'AAA', value: 100, value2: 1234 }, { title: 'BBB', value: -20, value2: null }, { title: 'CCC', value: 200, value2: 1000 }, @@ -245,6 +244,48 @@ describe('applyFieldOverrides', () => { }); }); + describe('given a frame field', () => { + const f0Internal = createDataFrame({ + name: 'frame', + fields: [ + { name: 'time', type: FieldType.time, values: [1752170223000, 1752170224000] }, + { name: 'value', type: FieldType.number, values: [10, 20] }, + ], + }); + const f0 = createDataFrame({ + name: 'A', + fields: [ + { + name: 'message', + type: FieldType.string, + values: ['foo'], + }, + { + name: 'frame', + type: FieldType.frame, + values: [f0Internal], + }, + ], + }); + + it('will apply field overrides to the fields within the frame', () => { + const withOverrides = applyFieldOverrides({ + data: [f0], + fieldConfig: { + defaults: { + max: 30, + }, + overrides: [], + }, + replaceVariables: (value) => value, + theme: createTheme(), + fieldConfigRegistry: customFieldRegistry, + }); + + expect(withOverrides[0].fields[1].values[0].fields[1].state.range.max).toBe(30); + }); + }); + it('will merge FieldConfig with default values', () => { const field: FieldConfig = { min: 0, diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 6b29551e1c0..cbdf9ba49e8 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -199,6 +199,8 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra if (field.type === FieldType.nestedFrames) { for (const nestedFrames of field.values) { for (let nfIndex = 0; nfIndex < nestedFrames.length; nfIndex++) { + // TODO: should we apply fieldOverrides to nested frames? + for (const valueField of nestedFrames[nfIndex].fields) { // Get display processor for nested fields valueField.display = getDisplayProcessor({ @@ -232,6 +234,13 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra } } } + + if (field.type === FieldType.frame) { + field.values = applyFieldOverrides({ + ...options, + data: field.values, + }); + } } return newFrame; diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 18ed9b6a6df..ec522923e61 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -1,4 +1,4 @@ -import { Map as OpenLayersMap } from 'ol'; +import OpenLayersMap from 'ol/Map'; import BaseLayer from 'ol/layer/Base'; import { ReactNode } from 'react'; diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index dde87b84498..23414822473 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -137,6 +137,8 @@ export { fieldColorModeRegistry, type FieldColorMode, getFieldSeriesColor, + /** @internal */ + getColorByStringHash, } from './field/fieldColor'; export { FieldConfigOptionsRegistry } from './field/FieldConfigOptionsRegistry'; export { sortThresholds, getActiveThreshold } from './field/thresholds'; @@ -432,7 +434,11 @@ export { export { createFieldConfigRegistry } from './panel/registryFactories'; export { type QueryRunner, type QueryRunnerOptions } from './types/queryRunner'; export { type GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix'; -export { type PluginContextType, type DataSourcePluginContextType } from './context/plugins/PluginContext'; +export { + type PluginContextType, + type DataSourcePluginContextType, + PluginContext, +} from './context/plugins/PluginContext'; export { type PluginContextProviderProps, PluginContextProvider } from './context/plugins/PluginContextProvider'; export { type DataSourcePluginContextProviderProps, @@ -555,6 +561,7 @@ export { type ComponentTypeWithExtensionMeta, type PluginExtensionFunction, type PluginExtensionEventHelpers, + type DataSourceConfigErrorStatusContext, type PluginExtensionPanelContext, type PluginExtensionQueryEditorRowAdaptiveTelemetryV1Context, type PluginExtensionDataSourceConfigContext, diff --git a/packages/grafana-data/src/text/sanitize.test.ts b/packages/grafana-data/src/text/sanitize.test.ts index 218e4064046..a0c69838976 100644 --- a/packages/grafana-data/src/text/sanitize.test.ts +++ b/packages/grafana-data/src/text/sanitize.test.ts @@ -55,6 +55,26 @@ describe('sanitize', () => { const str = sanitize(html); expect(str).toBe(''); }); + + describe('should sanitize anchors with target="_blank"', () => { + it('should add rel="noopener noreferrer" to target="_blank" links', () => { + const html = 'Link'; + const str = sanitize(html); + expect(str).toBe('Link'); + }); + + it('should preserve existing rel attributes and add noopener noreferrer, if not already added', () => { + const html = 'Link'; + const str = sanitize(html); + expect(str).toBe('Link'); + }); + + it('should not modify links without target="_blank"', () => { + const html = 'Link'; + const str = sanitize(html); + expect(str).toBe('Link'); + }); + }); }); describe('validatePath', () => { @@ -78,6 +98,27 @@ describe('validatePath', () => { expect(() => validatePath('/api/%/admin')).toThrow(PathValidationError); expect(() => validatePath('/api/%2/admin')).toThrow(PathValidationError); }); + + it('should block paths with tab characters', () => { + expect(() => validatePath('/api/\tadmin')).toThrow(PathValidationError); + expect(() => validatePath('/api/users\t/123')).toThrow(PathValidationError); + }); + + it('should block paths with newline characters', () => { + expect(() => validatePath('/api/\nadmin')).toThrow(PathValidationError); + expect(() => validatePath('/api/users\n/123')).toThrow(PathValidationError); + }); + + it('should block paths with carriage return characters', () => { + expect(() => validatePath('/api/\radmin')).toThrow(PathValidationError); + expect(() => validatePath('/api/users\r/123')).toThrow(PathValidationError); + }); + + it('should block URL encoded tab and newline characters', () => { + expect(() => validatePath('/api/%09admin')).toThrow(PathValidationError); // tab + expect(() => validatePath('/api/%0Aadmin')).toThrow(PathValidationError); // newline + expect(() => validatePath('/api/%0Dadmin')).toThrow(PathValidationError); // carriage return + }); }); describe('safe paths', () => { diff --git a/packages/grafana-data/src/text/sanitize.ts b/packages/grafana-data/src/text/sanitize.ts index 8fed4311627..4d22046cb31 100644 --- a/packages/grafana-data/src/text/sanitize.ts +++ b/packages/grafana-data/src/text/sanitize.ts @@ -56,13 +56,22 @@ const sanitizeTextPanelWhitelist = new xss.FilterXSS({ */ export function sanitize(unsanitizedString: string): string { try { + DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName === 'A' && node.getAttribute('target') === '_blank') { + node.setAttribute('rel', 'noopener noreferrer'); + } + }); + return DOMPurify.sanitize(unsanitizedString, { USE_PROFILES: { html: true }, FORBID_TAGS: ['form', 'input'], + ADD_ATTR: ['target'], }); } catch (error) { console.error('String could not be sanitized', unsanitizedString); return escapeHtml(unsanitizedString); + } finally { + DOMPurify.removeHook('afterSanitizeAttributes'); } } @@ -151,7 +160,7 @@ export function validatePath(path: OriginalPath): O originalDecoded = cleaned; // If the original string contains traversal attempts, block it - if (originalDecoded.includes('..') || originalDecoded.includes('/\\')) { + if (/\.\.|\/\\|[\t\n\r]/.test(originalDecoded)) { throw new PathValidationError(); } diff --git a/packages/grafana-data/src/themes/createVisualizationColors.ts b/packages/grafana-data/src/themes/createVisualizationColors.ts index 7c53debb700..80238d2a655 100644 --- a/packages/grafana-data/src/themes/createVisualizationColors.ts +++ b/packages/grafana-data/src/themes/createVisualizationColors.ts @@ -260,16 +260,24 @@ function getClassicPalette() { // Todo replace these with named colors (as many as possible) return [ - 'green', // '#7EB26D', // 0: pale green - 'semi-dark-yellow', // '#EAB839', // 1: mustard - 'light-blue', // #6ED0E0', // 2: light blue - 'semi-dark-orange', // '#EF843C', // 3: orange - 'red', // '#E24D42', // 4: red - 'blue', // #1F78C1', // 5: ocean - 'purple', // '#BA43A9', // 6: purple - '#705DA0', // 7: violet - 'dark-green', // '#508642', // 8: dark green - 'yellow', //'#CCA300', // 9: dark sand + 'green', + 'yellow', + 'blue', + 'orange', + 'red', + 'purple', + 'dark-green', + 'dark-yellow', + 'dark-blue', + 'dark-orange', + 'dark-red', + 'dark-purple', + 'super-light-green', + 'super-light-yellow', + 'super-light-blue', + 'super-light-orange', + 'super-light-red', + 'super-light-purple', '#447EBC', '#C15C17', '#890F02', diff --git a/packages/grafana-data/src/transformations/standardTransformersRegistry.ts b/packages/grafana-data/src/transformations/standardTransformersRegistry.ts index 4e5a11d318a..01a528a4a9a 100644 --- a/packages/grafana-data/src/transformations/standardTransformersRegistry.ts +++ b/packages/grafana-data/src/transformations/standardTransformersRegistry.ts @@ -34,6 +34,21 @@ export interface TransformerRegistryItem extends RegistryItem { * Set of categories associated with the transformer */ categories?: Set; + + /** + * Set of tags associated with the transformer for improved transformation search + */ + tags?: Set; + + /** + * Image representing the transformer, for dark themes + */ + imageDark: string; + + /** + * Image representing the transformer, for light themes + */ + imageLight: string; } export enum TransformerCategory { diff --git a/packages/grafana-data/src/transformations/transformers/groupBy.ts b/packages/grafana-data/src/transformations/transformers/groupBy.ts index f00219a5657..f6eb212a138 100644 --- a/packages/grafana-data/src/transformations/transformers/groupBy.ts +++ b/packages/grafana-data/src/transformations/transformers/groupBy.ts @@ -10,7 +10,7 @@ import { FieldMatcherID } from '../matchers/ids'; import { DataTransformerID } from './ids'; import { findMaxFields } from './utils'; -const MINIMUM_FIELDS_REQUIRED = 2; +const MINIMUM_FIELDS_REQUIRED = 1; export enum GroupByOperationID { aggregate = 'aggregate', @@ -146,11 +146,7 @@ export const groupByTransformer: DataTransformerInfo const shouldCalculateField = (field: Field, options: GroupByTransformerOptions): boolean => { const fieldName = getFieldDisplayName(field); - return ( - options?.fields[fieldName]?.operation === GroupByOperationID.aggregate && - Array.isArray(options?.fields[fieldName].aggregations) && - options?.fields[fieldName].aggregations.length > 0 - ); + return Array.isArray(options?.fields[fieldName]?.aggregations) && options?.fields[fieldName].aggregations.length > 0; }; /** diff --git a/packages/grafana-data/src/transformations/transformers/limit.test.ts b/packages/grafana-data/src/transformations/transformers/limit.test.ts index 9fde97b0283..a042dc5f34e 100644 --- a/packages/grafana-data/src/transformations/transformers/limit.test.ts +++ b/packages/grafana-data/src/transformations/transformers/limit.test.ts @@ -35,18 +35,21 @@ describe('Limit transformer', () => { { name: 'time', type: FieldType.time, + state: { calcs: undefined }, values: [3000, 4000, 5000], config: {}, }, { name: 'message', type: FieldType.string, + state: { calcs: undefined }, values: ['one', 'two', 'two'], config: {}, }, { name: 'values', type: FieldType.number, + state: { calcs: undefined }, values: [1, 2, 2], config: {}, }, @@ -79,18 +82,21 @@ describe('Limit transformer', () => { { name: 'time', type: FieldType.time, + state: { calcs: undefined }, values: [6000, 7000, 8000], config: {}, }, { name: 'message', type: FieldType.string, + state: { calcs: undefined }, values: ['three', 'three', 'three'], config: {}, }, { name: 'values', type: FieldType.number, + state: { calcs: undefined }, values: [3, 3, 3], config: {}, }, diff --git a/packages/grafana-data/src/transformations/transformers/limit.ts b/packages/grafana-data/src/transformations/transformers/limit.ts index 230e66d9dd3..4361e93c385 100644 --- a/packages/grafana-data/src/transformations/transformers/limit.ts +++ b/packages/grafana-data/src/transformations/transformers/limit.ts @@ -37,6 +37,12 @@ export const limitTransformer: DataTransformerInfo = { fields: frame.fields.map((f) => { return { ...f, + // Clear cached field calculations since applying a limit changes the dataset + // and previously computed stats (min, max, mean, etc.) are no longer valid + state: { + ...f.state, + calcs: undefined, + }, values: limit >= 0 ? f.values.slice(0, limit) : f.values.slice(f.values.length + limit, f.values.length), }; diff --git a/packages/grafana-data/src/transformations/transformers/reduce.test.ts b/packages/grafana-data/src/transformations/transformers/reduce.test.ts index bae31b5b8f9..f50072e0f30 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.test.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.test.ts @@ -8,7 +8,7 @@ import { notTimeFieldMatcher } from '../matchers/predicates'; import { transformDataFrame } from '../transformDataFrame'; import { DataTransformerID } from './ids'; -import { reduceFields, reduceTransformer, ReduceTransformerOptions } from './reduce'; +import { reduceFields, reduceTransformer, ReduceTransformerMode, ReduceTransformerOptions } from './reduce'; const seriesAWithSingleField = toDataFrame({ name: 'A', @@ -582,4 +582,62 @@ describe('Reducer Transformer', () => { `); }); }); + + it('reduce fields mode keeps distinct label values with multiple reducers', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.reduce, + options: { + mode: ReduceTransformerMode.ReduceFields, + reducers: [ReducerID.max, ReducerID.count], + labelsToFields: false, + }, + }; + + const seriesA = toDataFrame({ + length: 3, + fields: [{ name: 'value', config: {}, labels: { category: 'apple' }, type: FieldType.number, values: [3, 4, 5] }], + }); + + const seriesB = toDataFrame({ + fields: [ + { name: 'value', config: {}, labels: { category: 'orange' }, type: FieldType.number, values: [6, 7, 8] }, + ], + }); + + await expect(transformDataFrame([cfg], [seriesA, seriesB])).toEmitValuesWith((received) => { + const processed = received[0]; + + expect(processed.length).toEqual(2); + expect(processed[0].fields).toMatchInlineSnapshot(` + [ + { + "config": {}, + "labels": { + "category": "apple", + "reducer": "Max", + }, + "name": "value", + "state": undefined, + "type": "number", + "values": [ + 5, + ], + }, + { + "config": {}, + "labels": { + "category": "apple", + "reducer": "Count", + }, + "name": "value", + "state": undefined, + "type": "number", + "values": [ + 3, + ], + }, + ] + `); + }); + }); }); diff --git a/packages/grafana-data/src/transformations/transformers/reduce.ts b/packages/grafana-data/src/transformations/transformers/reduce.ts index dba2bc286e0..f9d2cd01d4c 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.ts @@ -230,10 +230,11 @@ export function reduceFields(data: DataFrame[], matcher: FieldMatcher, reducerId const value = results[reducer]; const copy = { ...field, + labels: { ...field.labels }, type: getFieldType(reducer, field), values: [value], + state: undefined, }; - copy.state = undefined; if (reducers.length > 1) { if (!copy.labels) { copy.labels = {}; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ca54c10faa2..1832b95666f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -186,6 +186,7 @@ export interface FeatureToggles { logsExploreTableVisualisation?: boolean; /** * Support temporary security credentials in AWS plugins for Grafana Cloud customers + * @default true */ awsDatasourcesTempCredentials?: boolean; /** @@ -210,9 +211,9 @@ export interface FeatureToggles { */ provisioning?: boolean; /** - * Use experimental git library for provisioning + * Experimental feature to use the secrets service for provisioning instead of the legacy secrets */ - nanoGit?: boolean; + provisioningSecretsService?: boolean; /** * Start an additional https handler and write kubectl options */ @@ -738,11 +739,6 @@ export interface FeatureToggles { */ crashDetection?: boolean; /** - * Enables querying the Jaeger data source without the proxy - * @default true - */ - jaegerBackendMigration?: boolean; - /** * Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query * @default true */ @@ -947,6 +943,10 @@ export interface FeatureToggles { */ metricsFromProfiles?: boolean; /** + * Enables creating alerts from Tempo data source + */ + tempoAlerting?: boolean; + /** * Enables auto-updating of users installed plugins */ pluginsAutoUpdate?: boolean; @@ -979,7 +979,7 @@ export interface FeatureToggles { restoreDashboards?: boolean; /** * Skip token rotation if it was already rotated less than 5 seconds ago - * @default false + * @default true */ skipTokenRotationIfRecent?: boolean; /** @@ -993,13 +993,17 @@ export interface FeatureToggles { */ alertingImportAlertmanagerAPI?: boolean; /** + * Enables image sharing functionality for dashboards + */ + sharingDashboardImage?: boolean; + /** * Prefer library panel title over viz panel title. * @default false */ preferLibraryPanelTitle?: boolean; /** * Use fixed-width numbers globally in the UI - * @default true + * @default false */ tabularNumbers?: boolean; /** @@ -1017,4 +1021,9 @@ export interface FeatureToggles { * @default false */ foldersAppPlatformAPI?: boolean; + /** + * Set this to true to use the new PluginImporter functionality + * @default false + */ + enablePluginImporter?: boolean; } diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index a04b9b7470f..e64bf16cfdb 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -174,6 +174,11 @@ export type PluginExtensionEventHelpers = { * @param props The props to be passed to the component. */ openSidebar: (componentTitle: string, props?: Record) => void; + /** + * @internal + * Closes the extension sidebar. + */ + closeSidebar: () => void; }; // Extension Points & Contexts @@ -188,6 +193,7 @@ export enum PluginExtensionPoints { CommandPalette = 'grafana/commandpalette/action', DashboardPanelMenu = 'grafana/dashboard/panel/menu', DataSourceConfig = 'grafana/datasources/config', + DataSourceConfigErrorStatus = 'grafana/datasources/config/error-status', ExploreToolbarAction = 'grafana/explore/toolbar/action', UserProfileTab = 'grafana/user/profile/tab', TraceViewDetails = 'grafana/traceview/details', @@ -195,6 +201,7 @@ export enum PluginExtensionPoints { TraceViewResourceAttributes = 'grafana/traceview/resource-attributes', LogsViewResourceAttributes = 'grafana/logsview/resource-attributes', AppChrome = 'grafana/app/chrome/v1', + ExtensionSidebar = 'grafana/extension-sidebar/v0-alpha', } export type PluginExtensionPanelContext = { @@ -248,6 +255,19 @@ export type PluginExtensionResourceAttributesContext = { }; }; +export type DataSourceConfigErrorStatusContext = { + dataSource: { + type: string; + uid: string; + name: string; + }; + testingStatus: { + message?: string | null; + status?: string | null; + details?: Record; + }; +}; + type Dashboard = { uid: string; title: string; diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index 9265c5f9f2c..a4b125c1005 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -116,6 +116,8 @@ export interface QueryVariableModel extends VariableWithMultiSupport { query: any; regex: string; refresh: VariableRefresh; + staticOptions?: VariableOption[]; + staticOptionsOrder?: 'before' | 'after' | 'sorted'; } export interface TextBoxVariableModel extends VariableWithOptions { diff --git a/packages/grafana-data/src/utils/fuzzySearch.test.ts b/packages/grafana-data/src/utils/fuzzySearch.test.ts index 71d75b27558..b53c26e98aa 100644 --- a/packages/grafana-data/src/utils/fuzzySearch.test.ts +++ b/packages/grafana-data/src/utils/fuzzySearch.test.ts @@ -48,6 +48,14 @@ describe('fuzzySearch', () => { expect(result.map((idx) => haystack[idx])).toEqual(['A水']); }); + it('should do case-insensitive substring match when needle contains non-ascii characters', () => { + const haystack = ['Über']; + const needle = 'ü'; + const result = fuzzySearch(haystack, needle); + + expect(result.map((idx) => haystack[idx])).toEqual(['Über']); + }); + it('should handle multiple non-latin characters', () => { const haystack = ['台灣省', '台中市', '台北市', '台南市', '南投縣', '高雄市', '台中第一高級中學']; const needle = '南'; diff --git a/packages/grafana-data/src/utils/fuzzySearch.ts b/packages/grafana-data/src/utils/fuzzySearch.ts index 72d1540f7b9..09ca02b7bc8 100644 --- a/packages/grafana-data/src/utils/fuzzySearch.ts +++ b/packages/grafana-data/src/utils/fuzzySearch.ts @@ -1,5 +1,7 @@ import uFuzzy from '@leeoniya/ufuzzy'; +import { escapeRegex } from '../text/string'; + // https://catonmat.net/my-favorite-regex :) const REGEXP_NON_ASCII = /[^ -~]/m; // https://www.asciitable.com/ @@ -36,11 +38,13 @@ export function fuzzySearch(haystack: string[], needle: string): number[] { needle.length > maxNeedleLength || uf.split(needle).length > maxFuzzyTerms ) { + const needleRegex = new RegExp(escapeRegex(needle), 'i'); const indices: number[] = []; + for (let i = 0; i < haystack.length; i++) { let item = haystack[i]; - if (item.includes(needle)) { + if (needleRegex.test(item)) { indices.push(i); } } diff --git a/packages/grafana-data/src/utils/location.test.ts b/packages/grafana-data/src/utils/location.test.ts index b0fd82bbd98..b2b2ee4bcf2 100644 --- a/packages/grafana-data/src/utils/location.test.ts +++ b/packages/grafana-data/src/utils/location.test.ts @@ -5,13 +5,14 @@ import { GrafanaConfig } from '../types/config'; import { locationUtil } from './location'; describe('locationUtil', () => { - const { location } = window; + const win: typeof globalThis = window; + const { location } = win; beforeEach(() => { // @ts-ignore - delete window.location; + delete win.location; - window.location = { + win.location = { ...location, hash: '#hash', host: 'www.domain.com:9877', @@ -26,7 +27,7 @@ describe('locationUtil', () => { }); afterEach(() => { - window.location = location; + win.location = location; }); describe('stripBaseFromUrl', () => { @@ -106,7 +107,7 @@ describe('locationUtil', () => { describe('when origin does not have a port in it', () => { beforeEach(() => { - window.location = { + win.location = { ...location, hash: '#hash', host: 'www.domain.com', diff --git a/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts b/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts index ef1b6f78fce..e5ef149b5a3 100644 --- a/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts +++ b/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts @@ -11,6 +11,8 @@ export const mockTransformationsRegistry = (transformers: DataTransformerInfo[]) transformation: t, description: t.description, editor: () => null, + imageDark: `build/img/${t.id}-dark.abc123.svg`, + imageLight: `build/img/${t.id}-light.abc123.svg`, }; }); }); diff --git a/packages/grafana-data/src/utils/url.test.ts b/packages/grafana-data/src/utils/url.test.ts index d0be86cb209..7970529ab85 100644 --- a/packages/grafana-data/src/utils/url.test.ts +++ b/packages/grafana-data/src/utils/url.test.ts @@ -100,11 +100,12 @@ describe('parseKeyValue', () => { }); describe('getUrlSearchParams', () => { - const { location } = window; + const win: typeof globalThis = window; + const { location } = win; // @ts-ignore - delete window.location; + delete win.location; - window.location = { + win.location = { ...location, hash: '#hash', host: 'www.domain.com:9877', diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 6ebb0e74af1..265adc2ffdb 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -39,10 +39,10 @@ "postpack": "mv package.json.bak package.json" }, "devDependencies": { - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "16.0.1", "@types/node": "22.15.0", "@types/semver": "7.7.0", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.0", @@ -52,6 +52,6 @@ "@grafana/tsconfig": "^2.0.0", "semver": "^7.7.0", "tslib": "2.8.1", - "typescript": "5.7.3" + "typescript": "5.8.3" } } diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 0ff43b6807e..93e5a545d8c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -1179,11 +1179,6 @@ export const versionedComponents = { '12.1.0': 'data-testid Data links actions tooltip wrapper', }, }, - TablePanel: { - autoCell: { - '12.1.0': 'data-testid Table panel auto cell', - }, - }, CodeEditor: { container: { '10.2.3': 'data-testid Code editor container', @@ -1367,6 +1362,63 @@ export const versionedComponents = { '11.5.0': 'data-testid portal-container', }, }, + ExportImage: { + formatOptions: { + container: { + ['12.1.0']: 'data-testid export-image-format-options', + }, + png: { + ['12.1.0']: 'data-testid export-image-format-png', + }, + jpg: { + ['12.1.0']: 'data-testid export-image-format-jpg', + }, + }, + rendererAlert: { + container: { + ['12.1.0']: 'data-testid export-image-renderer-alert', + }, + title: { + ['12.1.0']: 'data-testid export-image-renderer-alert-title', + }, + description: { + ['12.1.0']: 'data-testid export-image-renderer-alert-description', + }, + }, + buttons: { + generate: { + ['12.1.0']: 'data-testid export-image-generate-button', + }, + download: { + ['12.1.0']: 'data-testid export-image-download-button', + }, + cancel: { + ['12.1.0']: 'data-testid export-image-cancel-button', + }, + }, + preview: { + container: { + ['12.1.0']: 'data-testid export-image-preview-container', + }, + loading: { + ['12.1.0']: 'data-testid export-image-preview-loading', + }, + image: { + ['12.1.0']: 'data-testid export-image-preview', + }, + error: { + container: { + ['12.1.0']: 'data-testid export-image-error', + }, + title: { + ['12.1.0']: 'data-testid export-image-error-title', + }, + message: { + ['12.1.0']: 'data-testid export-image-error-message', + }, + }, + }, + }, } satisfies VersionedSelectorGroup; export type VersionedComponents = typeof versionedComponents; diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 056f3209ccd..93b359e034e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -227,6 +227,9 @@ export const versionedPages = { exportAsJson: { '11.2.0': 'data-testid new export button export as json', }, + exportAsImage: { + '12.1.0': 'data-testid new export button export as image', + }, }, }, playlistControls: { @@ -489,6 +492,27 @@ export const versionedPages = { queryOptionsQueryInput: { '10.4.0': 'data-testid Variable editor Form Default Variable Query Editor textarea', }, + queryOptionsStaticOptionsRow: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options row', + }, + queryOptionsStaticOptionsToggle: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options toggle', + }, + queryOptionsStaticOptionsLabelInput: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Label input', + }, + queryOptionsStaticOptionsValueInput: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Value input', + }, + queryOptionsStaticOptionsDeleteButton: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Delete button', + }, + queryOptionsStaticOptionsAddButton: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Add button', + }, + queryOptionsStaticOptionsOrderDropdown: { + [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Order dropdown', + }, valueGroupsTagsEnabledSwitch: { [MIN_GRAFANA_VERSION]: 'Variable editor Form Query UseTags switch', }, diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index 1fb043a4ec0..be49122b758 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -18,6 +18,8 @@ Check if border-radius theme tokens are used. To improve the consistency across Grafana we encourage devs to use tokens instead of custom values. In this case, we want the `borderRadius` to use the appropriate token such as `theme.shape.radius.default`, `theme.shape.radius.pill` or `theme.shape.radius.circle`. +Instead of using `0` to remove a previously set border-radius, use `unset`. + ### `no-unreduced-motion` Avoid direct use of `animation*` or `transition*` properties. @@ -111,3 +113,30 @@ const getStyles = (theme: GrafanaTheme2) => ({ ### `theme-token-usage` Used to find all instances of `theme` tokens being used in the codebase and emit the counts as metrics. Should **not** be used as an actual lint rule! + +### `consistent-story-titles` + +Enforce consistent Storybook titles in `.story.tsx` files. + +Storybook titles should not contain more than one `/` for sections (resulting in maximum 2 parts), unless one of the sections is 'Deprecated'. This helps maintain a clean and organized Storybook structure. + +#### Examples + +```tsx +// Bad ❌ +export default { title: 'Components/Forms/Button' }; + +// Good ✅ +export default { title: 'Components/Button' }; + +// Good ✅ - Deprecated allows any number of sections +export default { title: 'Components/Deprecated/Forms/Button/Extra' }; + +// Good ✅ - Variable assignment pattern +const storyConfig = { title: 'Components/Button' }; +export default storyConfig; + +// Bad ❌ - Variable assignment with too many sections +const storyConfig = { title: 'Components/Forms/Button' }; +export default storyConfig; +``` diff --git a/packages/grafana-eslint-rules/index.cjs b/packages/grafana-eslint-rules/index.cjs index 2babd165372..438d88332bc 100644 --- a/packages/grafana-eslint-rules/index.cjs +++ b/packages/grafana-eslint-rules/index.cjs @@ -3,6 +3,7 @@ const noBorderRadiusLiteral = require('./rules/no-border-radius-literal.cjs'); const noUnreducedMotion = require('./rules/no-unreduced-motion.cjs'); const themeTokenUsage = require('./rules/theme-token-usage.cjs'); const noRestrictedImgSrcs = require('./rules/no-restricted-img-srcs.cjs'); +const consistentStoryTitles = require('./rules/consistent-story-titles.cjs'); module.exports = { rules: { @@ -11,5 +12,6 @@ module.exports = { 'no-border-radius-literal': noBorderRadiusLiteral, 'theme-token-usage': themeTokenUsage, 'no-restricted-img-srcs': noRestrictedImgSrcs, + 'consistent-story-titles': consistentStoryTitles, }, }; diff --git a/packages/grafana-eslint-rules/rules/consistent-story-titles.cjs b/packages/grafana-eslint-rules/rules/consistent-story-titles.cjs new file mode 100644 index 00000000000..9fe077d699b --- /dev/null +++ b/packages/grafana-eslint-rules/rules/consistent-story-titles.cjs @@ -0,0 +1,106 @@ +// @ts-check +const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); + +/** + * @param {string} title + * @returns {boolean} + */ +const isValidStorybookTitle = (title) => { + if (typeof title !== 'string') { + return true; // Skip non-string titles + } + + const sections = title.split('/'); + + // Allow up to 3 sections if one of them is 'Deprecated' + if (sections.some((section) => section.trim() === 'Deprecated')) { + return sections.length <= 3; + } + + // Otherwise, limit to maximum 2 sections (1 slash) + return sections.length <= 2; +}; + +/** + * @param {import('@typescript-eslint/utils').TSESTree.ObjectExpression} objectNode + * @param {import('@typescript-eslint/utils/ts-eslint').RuleContext<'invalidTitle', []>} context + */ +const checkObjectForTitle = (objectNode, context) => { + const titleProperty = objectNode.properties.find( + (prop) => + prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === 'title' + ); + + if ( + titleProperty && + titleProperty.type === AST_NODE_TYPES.Property && + titleProperty.value.type === AST_NODE_TYPES.Literal + ) { + const titleValue = titleProperty.value.value; + + if (typeof titleValue === 'string' && !isValidStorybookTitle(titleValue)) { + context.report({ + node: titleProperty.value, + messageId: 'invalidTitle', + data: { + title: titleValue, + }, + }); + } + } +}; + +const consistentStoryTitlesRule = createRule({ + create(context) { + return { + ExportDefaultDeclaration(node) { + // Only check .story.tsx files + const filename = context.filename; + if (!filename || !filename.endsWith('.story.tsx')) { + return; + } + + if (node.declaration.type === AST_NODE_TYPES.ObjectExpression) { + // Handle direct object export: export default { title: '...' } + checkObjectForTitle(node.declaration, context); + } else if (node.declaration.type === AST_NODE_TYPES.Identifier) { + // Handle variable reference export: export default storyConfig + const variableName = node.declaration.name; + const scope = context.sourceCode.getScope(node); + const variable = scope.set.get(variableName); + + if (variable) { + // Find the variable declaration + const declaration = variable.defs.find((def) => def.type === 'Variable'); + if ( + declaration && + declaration.node.init && + declaration.node.init.type === AST_NODE_TYPES.ObjectExpression + ) { + checkObjectForTitle(declaration.node.init, context); + } + } + } + }, + }; + }, + name: 'consistent-story-titles', + meta: { + type: 'problem', + docs: { + description: 'Enforce consistent Storybook titles with maximum two sections (1 slash) unless one is "Deprecated"', + }, + messages: { + invalidTitle: + 'Storybook title "{{ title }}" has too many sections. Use maximum 2 sections (1 slash) unless one section is "Deprecated".', + }, + schema: [], + }, + defaultOptions: [], +}); + +module.exports = consistentStoryTitlesRule; diff --git a/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs b/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs index 27535a5e76c..8e9eeba6c58 100644 --- a/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs +++ b/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs @@ -4,6 +4,16 @@ const createRule = ESLintUtils.RuleCreator( (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` ); +const BORDER_RADIUS_PROPERTIES = [ + 'borderRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', +]; + +const RE_ZERO_VALUE = /^0([a-zA-Z%]*)$/; + const borderRadiusRule = createRule({ create(context) { return { @@ -11,13 +21,30 @@ const borderRadiusRule = createRule({ if ( node.type === AST_NODE_TYPES.Property && node.key.type === AST_NODE_TYPES.Identifier && - node.key.name === 'borderRadius' && + BORDER_RADIUS_PROPERTIES.includes(node.key.name) && node.value.type === AST_NODE_TYPES.Literal ) { - context.report({ - node, - messageId: 'borderRadiusId', - }); + const value = node.value.value; + + if (value === 'unset' || value === 'initial') { + // Allow 'unset' or 'initial' to remove border radius + return; + } else if (value === 0 || RE_ZERO_VALUE.test(value)) { + // Require 'unset' or 'initial' to remove border radius instead of `0` or `0px` + context.report({ + node, + messageId: 'borderRadiusNoZeroValue', + fix(fixer) { + return fixer.replaceText(node.value, "'unset'"); + }, + }); + } else { + // Otherwise, require theme tokens are used + context.report({ + node, + messageId: 'borderRadiusUseTokens', + }); + } } }, }; @@ -25,11 +52,13 @@ const borderRadiusRule = createRule({ name: 'no-border-radius-literal', meta: { type: 'problem', + fixable: 'code', docs: { description: 'Check if border-radius theme tokens are used', }, messages: { - borderRadiusId: 'Prefer using theme.shape.radius tokens instead of literal values.', + borderRadiusUseTokens: 'Prefer using theme.shape.radius tokens instead of literal values.', + borderRadiusNoZeroValue: 'Use unset or initial to remove a border radius.', }, schema: [], }, diff --git a/packages/grafana-eslint-rules/tests/consistent-stories.test.js b/packages/grafana-eslint-rules/tests/consistent-stories.test.js new file mode 100644 index 00000000000..004cd0aeeb3 --- /dev/null +++ b/packages/grafana-eslint-rules/tests/consistent-stories.test.js @@ -0,0 +1,117 @@ +import { RuleTester } from 'eslint'; + +import consistentStories from '../rules/consistent-story-titles.cjs'; + +RuleTester.setDefaultConfig({ + languageOptions: { + ecmaVersion: 2018, + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}); + +const ruleTester = new RuleTester(); + +ruleTester.run('eslint consistent-stories', consistentStories, { + valid: [ + { + name: 'simple title', + code: `export default { title: 'Button' };`, + filename: 'Button.story.tsx', + }, + { + name: 'one section', + code: `export default { title: 'Components/Button' };`, + filename: 'Button.story.tsx', + }, + { + name: 'deprecated can have 3 sections', + code: `export default { title: 'Components/Deprecated/Button' };`, + filename: 'Button.story.tsx', + }, + { + name: 'not a story file', + code: `export default { title: 'Components/Forms/Button/Extra/Section' };`, + filename: 'Button.tsx', + }, + { + name: 'non-string title', + code: `export default { title: 123 };`, + filename: 'Button.story.tsx', + }, + { + name: 'no title property', + code: `export default { component: Button };`, + filename: 'Button.story.tsx', + }, + { + name: 'variable assignment - simple title', + code: ` +const storyConfig = { title: 'Button' }; +export default storyConfig;`, + filename: 'Button.story.tsx', + }, + { + name: 'variable assignment - one section', + code: ` +const storyConfig = { title: 'Components/Button' }; +export default storyConfig;`, + filename: 'Button.story.tsx', + }, + { + name: 'variable assignment - with Deprecated', + code: ` +const storyConfig = { title: 'Components/Deprecated/Button' }; +export default storyConfig;`, + filename: 'Button.story.tsx', + }, + ], + invalid: [ + { + name: 'too many sections without Deprecated', + code: `export default { title: 'Components/Forms/Button' };`, + filename: 'Button.story.tsx', + errors: [ + { + messageId: 'invalidTitle', + }, + ], + }, + { + name: 'too many sections without Deprecated', + code: `export default { title: 'Components/Forms/Button/Extra' };`, + filename: 'Button.story.tsx', + errors: [ + { + messageId: 'invalidTitle', + }, + ], + }, + { + name: 'with spaces around sections', + code: `export default { title: 'Components / Forms / Button' };`, + filename: 'Button.story.tsx', + errors: [ + { + messageId: 'invalidTitle', + }, + ], + }, + { + name: 'variable assignment - too many sections', + code: ` +const storyConfig = { title: 'Components/Forms/Button' }; +export default storyConfig;`, + filename: 'Button.story.tsx', + errors: [ + { + messageId: 'invalidTitle', + }, + ], + }, + ], +}); diff --git a/packages/grafana-eslint-rules/tests/no-border-radius-literal.test.js b/packages/grafana-eslint-rules/tests/no-border-radius-literal.test.js index 7410059db50..c2bf3f94de2 100644 --- a/packages/grafana-eslint-rules/tests/no-border-radius-literal.test.js +++ b/packages/grafana-eslint-rules/tests/no-border-radius-literal.test.js @@ -14,8 +14,12 @@ RuleTester.setDefaultConfig({ }, }); -const expectedError = { - messageId: 'borderRadiusId', +const useTokenError = { + messageId: 'borderRadiusUseTokens', +}; + +const noZeroValueError = { + messageId: 'borderRadiusNoZeroValue', }; const ruleTester = new RuleTester(); @@ -31,20 +35,44 @@ ruleTester.run('eslint no-border-radius-literal', noBorderRadiusLiteral, { { code: `css({ borderRadius: theme.shape.radius.pill })`, }, + { + code: `css({ borderTopLeftRadius: theme.shape.radius.pill })`, + }, + { + code: `css({ borderTopRightRadius: theme.shape.radius.pill })`, + }, + { + code: `css({ borderBottomLeftRadius: theme.shape.radius.pill })`, + }, + { + code: `css({ borderBottomRightRadius: theme.shape.radius.pill })`, + }, + + // allow values to remove border radius + { + code: `css({ borderRadius: 'initial' })`, + }, + { + code: `css({ borderRadius: 'unset' })`, + }, ], invalid: [ { code: `css({ borderRadius: '2px' })`, - errors: [expectedError], + errors: [useTokenError], + }, + { + code: `css({ borderRadius: 2 })`, // should error on px shorthand + errors: [useTokenError], }, { code: `css({ lineHeight: 1 }, { borderRadius: '2px' })`, - errors: [expectedError], + errors: [useTokenError], }, { code: `css([{ lineHeight: 1 }, { borderRadius: '2px' }])`, - errors: [expectedError], + errors: [useTokenError], }, { name: 'nested classes', @@ -56,7 +84,40 @@ css({ }, }, })`, - errors: [expectedError], + errors: [useTokenError], + }, + { + code: `css({ borderTopLeftRadius: 1 })`, + errors: [useTokenError], + }, + { + code: `css({ borderTopRightRadius: "2px" })`, + errors: [useTokenError], + }, + { + code: `css({ borderBottomLeftRadius: 3 })`, + errors: [useTokenError], + }, + { + code: `css({ borderBottomRightRadius: "4px" })`, + errors: [useTokenError], + }, + + // should use unset or initial to remove border radius + { + code: `css({ borderRadius: 0 })`, + output: `css({ borderRadius: 'unset' })`, + errors: [noZeroValueError], + }, + { + code: `css({ borderRadius: '0px' })`, + output: `css({ borderRadius: 'unset' })`, + errors: [noZeroValueError], + }, + { + code: `css({ borderRadius: "0%" })`, + output: `css({ borderRadius: 'unset' })`, + errors: [noZeroValueError], }, ], }); diff --git a/packages/grafana-flamegraph/README.md b/packages/grafana-flamegraph/README.md index 9bd09dfc6c6..5022f4b2199 100644 --- a/packages/grafana-flamegraph/README.md +++ b/packages/grafana-flamegraph/README.md @@ -9,7 +9,7 @@ This is a Flamegraph component that is used in Grafana and Pyroscope web app to Currently this library exposes single component `Flamegraph` that renders whole visualization used for profiling which contains a header, a table representation of the data and a flamegraph. ```tsx -import { Flamegraph } from '@grafana/flamegraph'; +import { FlameGraph } from '@grafana/flamegraph'; createTheme({ colors: { mode: 'dark' } })} diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 7adc38a06c9..892771c1082 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -51,7 +51,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-use": "17.6.0", - "react-virtualized-auto-sizer": "1.0.25", + "react-virtualized-auto-sizer": "1.0.26", "tinycolor2": "1.6.0", "tslib": "2.8.1" }, @@ -60,20 +60,20 @@ "@babel/preset-env": "7.26.9", "@babel/preset-react": "7.26.3", "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "16.0.1", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/d3": "^7", "@types/jest": "^29.5.4", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", - "@types/react-virtualized-auto-sizer": "1.0.4", + "@types/react-virtualized-auto-sizer": "1.0.8", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "^4.22.4", @@ -81,7 +81,7 @@ "rollup-plugin-node-externals": "^8.0.0", "ts-jest": "29.2.5", "ts-node": "10.9.2", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx index 94fcfa71a1c..5be3bb3c3db 100644 --- a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx @@ -19,9 +19,13 @@ jest.mock('react-use', () => ({ describe('FlameGraphContainer', () => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'clientWidth', { value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + left: 0, + })), + }); const FlameGraphContainerWithProps = () => { const flameGraphData = createDataFrame(data); diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx index 0396e536535..22f50cb36b8 100644 --- a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx @@ -31,8 +31,12 @@ describe('FlameGraphTopTableContainer', () => { it('should render correctly', async () => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + })), + }); setup(); const rows = screen.getAllByRole('row'); @@ -56,8 +60,13 @@ describe('FlameGraphTopTableContainer', () => { it('should render search and sandwich buttons', async () => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + left: 0, + })), + }); const { mocks } = setup(); diff --git a/packages/grafana-i18n/package.json b/packages/grafana-i18n/package.json index b06109d02db..9f2fd298de1 100644 --- a/packages/grafana-i18n/package.json +++ b/packages/grafana-i18n/package.json @@ -56,7 +56,7 @@ "@formatjs/intl-durationformat": "^0.7.0", "@typescript-eslint/utils": "^8.33.1", "fast-deep-equal": "^3.1.3", - "i18next": "^24.0.0", + "i18next": "^25.0.0", "i18next-browser-languagedetector": "^8.0.0", "i18next-pseudo": "^2.2.1", "micro-memoize": "^4.1.2", @@ -67,7 +67,7 @@ "@types/react": "18.3.18", "rollup": "^4.22.4", "rollup-plugin-copy": "3.5.0", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "react": ">=18" diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs index 91de4620060..64ca4c5ff4a 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs @@ -24,7 +24,21 @@ const createRule = ESLintUtils.RuleCreator( /** * JSX props to check for untranslated strings */ -const propsToCheck = ['content', 'label', 'description', 'placeholder', 'aria-label', 'title', 'text', 'tooltip']; +const propsToCheck = [ + 'content', + 'label', + 'description', + 'placeholder', + 'aria-label', + 'ariaLabel', + 'title', + 'text', + 'tooltip', + 'confirmText', + 'body', + 'noOptionsMessage', + 'loadingMessage', +]; /** * Object properties to check for untranslated strings @@ -34,11 +48,15 @@ const propertiesToCheck = [ 'description', 'placeholder', 'aria-label', + 'ariaLabel', 'title', 'subTitle', 'text', 'tooltip', 'message', + 'confirmText', + 'placeholderText', + 'noFieldsMessage', ]; /** @type {RuleDefinition} */ diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 301762fdfd7..18faf879904 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -24,7 +24,7 @@ "@grafana/runtime": "12.1.0-pre", "@grafana/schema": "12.1.0-pre", "@grafana/ui": "12.1.0-pre", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" @@ -38,12 +38,12 @@ "@types/jest": "^29.5.4", "@types/node": "22.15.0", "@types/react": "18.3.18", - "@types/systemjs": "6.15.1", + "@types/systemjs": "6.15.3", "jest": "^29.6.4", "react": "18.3.1", "ts-jest": "29.2.5", "ts-node": "10.9.2", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx index b5bca15f317..50870a2d5bc 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx @@ -80,6 +80,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { 'grafana-opensearch-datasource', // external 'grafana-falconlogscale-datasource', // external 'googlecloud-logging-datasource', // external + 'victoriametrics-logs-datasource', // external ]; const traceToLogs = useMemo( diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index f46b55fe6a4..cf0cc967eaa 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -6,6 +6,7 @@ "dependencies": { "tslib": "2.8.1" }, + "type": "module", "devDependencies": { "@grafana/tsconfig": "^2.0.0", "@swc/core": "1.10.12", @@ -15,11 +16,11 @@ "eslint": "9.19.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", - "glob": "11.0.1", + "glob": "11.0.3", "imports-loader": "^5.0.0", "replace-in-file-webpack-plugin": "1.0.6", "swc-loader": "0.2.6", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1", "webpack-bundle-analyzer": "^4.10.2", "webpack-virtual-modules": "^0.6.2" diff --git a/packages/grafana-plugin-configs/tsconfig.json b/packages/grafana-plugin-configs/tsconfig.json index 1790c28c284..b7a0dd239e8 100644 --- a/packages/grafana-plugin-configs/tsconfig.json +++ b/packages/grafana-plugin-configs/tsconfig.json @@ -4,19 +4,11 @@ "alwaysStrict": true, "declaration": false, "resolveJsonModule": true, - "moduleResolution": "bundler" - }, - "ts-node": { - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "moduleResolution": "Node", - "esModuleInterop": true - }, - "transpileOnly": true + "moduleResolution": "bundler", + "noEmit": true, + "allowImportingTsExtensions": true }, "extends": "@grafana/tsconfig", - "exclude": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"], "include": ["./types", "."] } diff --git a/packages/grafana-plugin-configs/types/replace-in-webpack-plugin.d.ts b/packages/grafana-plugin-configs/types/replace-in-webpack-plugin.d.ts new file mode 100644 index 00000000000..746b3e0d303 --- /dev/null +++ b/packages/grafana-plugin-configs/types/replace-in-webpack-plugin.d.ts @@ -0,0 +1,23 @@ +declare module 'replace-in-file-webpack-plugin' { + import { Compiler, Plugin } from 'webpack'; + + interface ReplaceRule { + search: string | RegExp; + replace: string | ((match: string) => string); + } + + interface ReplaceOption { + dir?: string; + files?: string[]; + test?: RegExp | RegExp[]; + rules: ReplaceRule[]; + } + + class ReplaceInFilePlugin extends Plugin { + constructor(options?: ReplaceOption[]); + options: ReplaceOption[]; + apply(compiler: Compiler): void; + } + + export = ReplaceInFilePlugin; +} diff --git a/packages/grafana-plugin-configs/utils.ts b/packages/grafana-plugin-configs/utils.ts index 3a74dd90af6..7c517dc2d18 100644 --- a/packages/grafana-plugin-configs/utils.ts +++ b/packages/grafana-plugin-configs/utils.ts @@ -3,12 +3,19 @@ import { glob } from 'glob'; import path from 'path'; import process from 'process'; +// Support for node 22 and 24. Due to the many changes in importing json files +// across recent node versions we load the json files using fs for simplicity. +function loadJson(path: string) { + const rawJson = fs.readFileSync(path, 'utf8'); + return JSON.parse(rawJson); +} + export function getPackageJson() { - return require(path.resolve(process.cwd(), 'package.json')); + return loadJson(path.resolve(process.cwd(), 'package.json')); } export function getPluginJson() { - return require(path.resolve(process.cwd(), 'plugin.json')); + return loadJson(path.resolve(process.cwd(), 'plugin.json')); } export async function getEntries(): Promise> { diff --git a/packages/grafana-plugin-configs/webpack.config.ts b/packages/grafana-plugin-configs/webpack.config.ts index 7714093000a..c1bfc469d7b 100644 --- a/packages/grafana-plugin-configs/webpack.config.ts +++ b/packages/grafana-plugin-configs/webpack.config.ts @@ -2,15 +2,14 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; import ESLintPlugin from 'eslint-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import path from 'path'; -// @ts-expect-error - there are no types for this package import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; -import { type Configuration, BannerPlugin } from 'webpack'; +import webpack, { type Configuration } from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import VirtualModulesPlugin from 'webpack-virtual-modules'; -import { DIST_DIR } from './constants'; -import { getPackageJson, getPluginJson, getEntries, hasLicense } from './utils'; +import { DIST_DIR } from './constants.ts'; +import { getPackageJson, getPluginJson, getEntries, hasLicense } from './utils.ts'; function skipFiles(f: string): boolean { if (f.includes('/dist/')) { @@ -32,7 +31,7 @@ function skipFiles(f: string): boolean { return true; } -type Env = { +export type Env = { [key: string]: true | string | Env; }; @@ -54,9 +53,13 @@ const config = async (env: Env): Promise => { cache: { type: 'filesystem', buildDependencies: { - config: [__filename], + config: [import.meta.filename], }, - cacheDirectory: path.resolve(__dirname, '../../node_modules/.cache/webpack', path.basename(process.cwd())), + cacheDirectory: path.resolve( + import.meta.dirname, + '../../node_modules/.cache/webpack', + path.basename(process.cwd()) + ), }, context: process.cwd(), @@ -114,7 +117,7 @@ const config = async (env: Env): Promise => { test: /module\.tsx?$/, use: [ { - loader: require.resolve('imports-loader'), + loader: 'imports-loader', options: { imports: `side-effects grafana-public-path`, }, @@ -125,10 +128,10 @@ const config = async (env: Env): Promise => { exclude: /(node_modules)/, test: /\.[tj]sx?$/, use: { - loader: require.resolve('swc-loader'), + loader: 'swc-loader', options: { jsc: { - baseUrl: path.resolve(__dirname), + baseUrl: path.resolve(import.meta.dirname), target: 'es2015', loose: false, parser: { @@ -209,7 +212,7 @@ const config = async (env: Env): Promise => { plugins: [ virtualPublicPath, // Insert create plugin version information into the bundle so Grafana will load from cdn with script tags. - new BannerPlugin({ + new webpack.BannerPlugin({ banner: '/* [create-plugin] version: 5.22.0 */', raw: true, entryOnly: true, @@ -264,7 +267,7 @@ const config = async (env: Env): Promise => { extensions: ['.ts', '.tsx'], lintDirtyModulesOnly: true, // don't lint on start, only lint changed files cacheLocation: path.resolve( - __dirname, + import.meta.dirname, '../../node_modules/.cache/eslint-webpack-plugin', path.basename(process.cwd()), '.eslintcache' diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index b1d1ba33825..d6a5ef7fd3e 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@floating-ui/react": "0.27.12", + "@floating-ui/react": "0.27.13", "@grafana/data": "12.1.0-pre", "@grafana/e2e-selectors": "12.1.0-pre", "@grafana/i18n": "12.1.0-pre", @@ -53,10 +53,10 @@ "@lezer/common": "1.2.3", "@lezer/highlight": "1.2.1", "@lezer/lr": "1.4.2", - "@prometheus-io/lezer-promql": "0.304.1", + "@prometheus-io/lezer-promql": "0.304.2", "@reduxjs/toolkit": "2.5.1", "@types/debounce-promise": "3.1.9", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-highlight-words": "0.20.0", @@ -82,7 +82,7 @@ "@rollup/plugin-dynamic-import-vars": "2.1.5", "@rollup/plugin-image": "3.0.3", "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "16.0.1", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", @@ -90,7 +90,7 @@ "@types/node": "22.15.0", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "i18next-parser": "9.3.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", @@ -102,7 +102,7 @@ "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "testing-library-selector": "0.3.1", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx index c0e64fa2356..d8f99a7bc48 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx @@ -25,7 +25,7 @@ import { StandardPromVariableQuery, } from '../types'; -export const variableOptions = [ +const variableOptions = [ { label: 'Label names', value: QueryType.LabelNames }, { label: 'Label values', value: QueryType.LabelValues }, { label: 'Metrics', value: QueryType.MetricNames }, diff --git a/packages/grafana-prometheus/src/components/cancelable-promise.ts b/packages/grafana-prometheus/src/components/cancelable-promise.ts index 0256922cbbb..673d619b253 100644 --- a/packages/grafana-prometheus/src/components/cancelable-promise.ts +++ b/packages/grafana-prometheus/src/components/cancelable-promise.ts @@ -6,7 +6,7 @@ export interface CancelablePromise { cancel: () => void; } -export interface CancelablePromiseRejection { +interface CancelablePromiseRejection { isCanceled: boolean; } diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx index 1c24fadd96b..0200645d392 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx @@ -257,23 +257,30 @@ const MonacoQueryField = (props: Props) => { return; } const query = model.getValue(); - const errors = - validateQuery( - query, - datasource.interpolateString(query, placeHolderScopedVars), - model.getLinesContent(), - parser - ) || []; + const { errors, warnings } = validateQuery( + query, + datasource.interpolateString(query, placeHolderScopedVars), + model.getLinesContent(), + parser + ); - const markers = errors.map(({ error, ...boundary }) => ({ - message: `${ - error ? `Error parsing "${error}"` : 'Parse error' - }. The query appears to be incorrect and could fail to be executed.`, - severity: monaco.MarkerSeverity.Error, - ...boundary, - })); + const errorMarkers = errors.map(({ issue, ...boundary }) => { + return { + message: `${issue ? `Error parsing "${issue}"` : 'Parse error'}. The query appears to be incorrect and could fail to be executed.`, + severity: monaco.MarkerSeverity.Error, + ...boundary, + }; + }); - monaco.editor.setModelMarkers(model, 'owner', markers); + const warningMarkers = warnings.map(({ issue, ...boundary }) => { + return { + message: `Warning: ${issue}`, + severity: monaco.MarkerSeverity.Warning, + ...boundary, + }; + }); + + monaco.editor.setModelMarkers(model, 'owner', [...errorMarkers, ...warningMarkers]); }); } }} diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/data_provider.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/data_provider.ts index a651f761292..b5772fa45cd 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/data_provider.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/data_provider.ts @@ -7,7 +7,7 @@ import { isValidLegacyName } from '../../../utf8_support'; export const CODE_MODE_SUGGESTIONS_INCOMPLETE_EVENT = 'codeModeSuggestionsIncomplete'; -export type SuggestionsIncompleteEvent = CustomEvent<{ +type SuggestionsIncompleteEvent = CustomEvent<{ limit: number; datasourceUid: string; }>; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.test.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.test.ts index 3768457dfce..14acf601422 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.test.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.test.ts @@ -1,11 +1,11 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/validation.test.ts import { parser } from '@prometheus-io/lezer-promql'; -import { validateQuery } from './validation'; +import { validateQuery, warningTypes } from './validation'; describe('Monaco Query Validation', () => { test('Identifies empty queries as valid', () => { - expect(validateQuery('', '', [], parser)).toBeFalsy(); + expect(validateQuery('', '', [], parser)).toEqual({ errors: [], warnings: [] }); }); test.each([ @@ -17,45 +17,30 @@ describe('Monaco Query Validation', () => { 'rate(http_requests_total[5m])', 'topk(3, sum by (app, proc) (rate(instance_cpu_time_ns[5m])))', ])('Identifies valid queries', (query: string) => { - expect(validateQuery(query, query, [], parser)).toBeFalsy(); + expect(validateQuery(query, query, [], parser).errors).toEqual([]); }); test('Identifies invalid queries', () => { // Missing } at the end let query = 'access_evaluation_duration_sum{job="grafana"'; - expect(validateQuery(query, query, [query], parser)).toEqual([ - { - endColumn: 45, - endLineNumber: 1, - error: '{job="grafana"', - startColumn: 31, - startLineNumber: 1, - }, - ]); + expect(validateQuery(query, query, [query], parser)).toEqual({ + errors: [{ endColumn: 45, endLineNumber: 1, issue: '{job="grafana"', startColumn: 31, startLineNumber: 1 }], + warnings: [], + }); // Missing handler="value" query = 'http_requests_total{job="apiserver", handler}[5m]'; - expect(validateQuery(query, query, [query], parser)).toEqual([ - { - endColumn: 45, - endLineNumber: 1, - error: 'handler', - startColumn: 38, - startLineNumber: 1, - }, - ]); + expect(validateQuery(query, query, [query], parser)).toEqual({ + errors: [{ endColumn: 45, endLineNumber: 1, issue: 'handler', startColumn: 38, startLineNumber: 1 }], + warnings: [], + }); // Missing : in [30s:5s] query = 'max_over_time(deriv(rate(distance_covered_total[5s])[30s5s])[10m:])'; - expect(validateQuery(query, query, [query], parser)).toEqual([ - { - endColumn: 59, - endLineNumber: 1, - error: '5s', - startColumn: 57, - startLineNumber: 1, - }, - ]); + expect(validateQuery(query, query, [query], parser)).toEqual({ + errors: [{ endColumn: 59, endLineNumber: 1, issue: '5s', startColumn: 57, startLineNumber: 1 }], + warnings: [], + }); }); test('Identifies valid multi-line queries', () => { @@ -64,7 +49,7 @@ sum by (job) ( rate(http_requests_total[5m]) )`; const queryLines = query.split('\n'); - expect(validateQuery(query, query, queryLines, parser)).toBeFalsy(); + expect(validateQuery(query, query, queryLines, parser)).toEqual({ errors: [], warnings: [] }); }); test('Identifies invalid multi-line queries', () => { @@ -73,14 +58,32 @@ sum by (job) ( rate(http_requests_total[]) )`; const queryLines = query.split('\n'); - expect(validateQuery(query, query, queryLines, parser)).toEqual([ - { - endColumn: 30, - endLineNumber: 3, - error: '', - startColumn: 30, - startLineNumber: 3, - }, - ]); + expect(validateQuery(query, query, queryLines, parser)).toEqual({ + errors: [{ endColumn: 30, endLineNumber: 3, issue: '', startColumn: 30, startLineNumber: 3 }], + warnings: [], + }); + }); + + test('Warns agains subqueries with same duration and step', () => { + const query = 'rate(http_requests_total[5m:5m])'; + const queryLines = query.split('\n'); + expect(validateQuery(query, query, queryLines, parser)).toEqual({ + errors: [], + warnings: [ + { endColumn: 32, endLineNumber: 1, issue: warningTypes.SubqueryExpr, startColumn: 6, startLineNumber: 1 }, + ], + }); + }); + + test('Warns agains queries with multiple subqueries', () => { + const query = 'quantile_over_time(0.5, rate(http_requests_total[1m:1m]) [1m:1m])'; + const queryLines = query.split('\n'); + expect(validateQuery(query, query, queryLines, parser)).toEqual({ + errors: [], + warnings: [ + { issue: warningTypes.SubqueryExpr, startColumn: 25, endColumn: 65, startLineNumber: 1, endLineNumber: 1 }, + { issue: warningTypes.SubqueryExpr, startColumn: 30, endColumn: 56, startLineNumber: 1, endLineNumber: 1 }, + ], + }); }); }); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.ts index 34ce38c060b..6bdd7172f63 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/validation.ts @@ -3,17 +3,27 @@ import { SyntaxNode } from '@lezer/common'; import { LRParser } from '@lezer/lr'; // Although 0 isn't explicitly provided in the @grafana/lezer-logql library as the error node ID, it does appear to be the ID of error nodes within lezer. -export const ErrorId = 0; +const ErrorId = 0; -interface ParserErrorBoundary { +export const warningTypes: Record = { + SubqueryExpr: + 'This subquery may return only one data point, preventing rate/increase/delta calculations. Use a range at least twice the step size (e.g., [2x:x]).', +}; + +enum NodeType { + SubqueryExpr = 'SubqueryExpr', + Duration = 'NumberDurationLiteralInDurationContext', +} + +interface ParserIssueBoundary { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; - error: string; + issue: string; } -interface ParseError { +interface ParseIssue { text: string; node: SyntaxNode; } @@ -30,9 +40,9 @@ export function validateQuery( interpolatedQuery: string, queryLines: string[], parser: LRParser -): ParserErrorBoundary[] | false { +): { errors: ParserIssueBoundary[]; warnings: ParserIssueBoundary[] } { if (!query) { - return false; + return { errors: [], warnings: [] }; } /** @@ -42,51 +52,93 @@ export function validateQuery( * have different lengths. With this, we also exclude irrelevant parser errors that are produced by * lezer not understanding $variables and $__variables, which usually generate 2 or 3 error SyntaxNode. */ - const interpolatedErrors: ParseError[] = parseQuery(interpolatedQuery, parser); - if (!interpolatedErrors.length) { - return false; + const { errors: interpolatedErrors, warnings: interpolatedWarnings } = parseQuery(interpolatedQuery, parser); + if (!interpolatedErrors.length && !interpolatedWarnings.length) { + return { errors: [], warnings: [] }; } - let parseErrors: ParseError[] = interpolatedErrors; + let parseErrors: ParseIssue[] = interpolatedErrors; + let parseWarnings: ParseIssue[] = interpolatedWarnings; if (query !== interpolatedQuery) { - const queryErrors: ParseError[] = parseQuery(query, parser); + const { errors: queryErrors, warnings: queryWarnings } = parseQuery(query, parser); parseErrors = interpolatedErrors.flatMap( (interpolatedError) => queryErrors.filter((queryError) => interpolatedError.text === queryError.text) || interpolatedError ); + parseWarnings = interpolatedWarnings.flatMap( + (interpolatedWarning) => + queryWarnings.filter((queryWarning) => interpolatedWarning.node.from === queryWarning.node.from) || + interpolatedWarning + ); } - return parseErrors.map((parseError) => findErrorBoundary(query, queryLines, parseError)).filter(isErrorBoundary); + const errorBoundaries = parseErrors + .map((parseError) => findIssueBoundary(query, queryLines, parseError, 'error')) + .filter(isValidIssueBoundary); + const warningBoundaries = parseWarnings + .map((parseWarning) => findIssueBoundary(query, queryLines, parseWarning, 'warning')) + .filter(isValidIssueBoundary); + + return { + errors: errorBoundaries, + warnings: warningBoundaries, + }; } function parseQuery(query: string, parser: LRParser) { - const parseErrors: ParseError[] = []; + const parseErrors: ParseIssue[] = []; + const parseWarnings: ParseIssue[] = []; + const tree = parser.parse(query); tree.iterate({ enter: (nodeRef): false | void => { if (nodeRef.type.id === ErrorId) { const node = nodeRef.node; - parseErrors.push({ - node: node, - text: query.substring(node.from, node.to), - }); + parseErrors.push({ node: node, text: query.substring(node.from, node.to) }); + } + + if (nodeRef.type.name === NodeType.SubqueryExpr) { + const node = nodeRef.node; + const durations: string[] = []; + + const children = node.getChildren(NodeType.Duration); + for (const child of children) { + durations.push(query.substring(child.from, child.to)); + } + + if (durations.length === 2 && durations[0] === durations[1]) { + parseWarnings.push({ node: node, text: query.substring(node.from, node.to) }); + } } }, }); - return parseErrors; + + return { errors: parseErrors, warnings: parseWarnings }; } -function findErrorBoundary(query: string, queryLines: string[], parseError: ParseError): ParserErrorBoundary | null { +function findIssueBoundary( + query: string, + queryLines: string[], + parseError: ParseIssue, + issueType: 'error' | 'warning' +): ParserIssueBoundary | null { if (queryLines.length === 1) { const isEmptyString = parseError.node.from === parseError.node.to; const errorNode = isEmptyString && parseError.node.parent ? parseError.node.parent : parseError.node; - const error = isEmptyString ? query.substring(errorNode.from, errorNode.to) : parseError.text; + let issue: string; + + if (issueType === 'error') { + issue = isEmptyString ? query.substring(errorNode.from, errorNode.to) : parseError.text; + } else { + issue = warningTypes[parseError.node.type.name]; + } + return { startLineNumber: 1, startColumn: errorNode.from + 1, endLineNumber: 1, endColumn: errorNode.to + 1, - error, + issue, }; } @@ -105,14 +157,14 @@ function findErrorBoundary(query: string, queryLines: string[], parseError: Pars startColumn: parseError.node.from - startPos + 1, endLineNumber: line + 1, endColumn: parseError.node.to - startPos + 1, - error: parseError.text, + issue: issueType === 'error' ? parseError.text : warningTypes[parseError.node.type.name], }; } return null; } -function isErrorBoundary(boundary: ParserErrorBoundary | null): boundary is ParserErrorBoundary { +function isValidIssueBoundary(boundary: ParserIssueBoundary | null): boundary is ParserIssueBoundary { return boundary !== null; } diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/promql.ts b/packages/grafana-prometheus/src/components/monaco-query-field/promql.ts index d1ca8752aea..4a850789c72 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/promql.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/promql.ts @@ -162,7 +162,7 @@ export const language = { digits: /\d+(_+\d+)*/, octaldigits: /[0-7]+(_+[0-7]+)*/, binarydigits: /[0-1]+(_+[0-1]+)*/, - hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/, + hexdigits: /[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/, integersuffix: /(ll|LL|u|U|l|L)?(ll|LL|u|U|l|L)?/, floatsuffix: /[fFlL]?/, // The main tokenizer for our languages diff --git a/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx b/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx index 0dad0be5447..48c136895a3 100644 --- a/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx +++ b/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx @@ -12,7 +12,7 @@ import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul'; import { DataSourceHttpSettingsOverhaul } from './DataSourceHttpSettingsOverhaul'; import { PromSettings } from './PromSettings'; import { overhaulStyles } from './shared/utils'; -export type PrometheusConfigProps = DataSourcePluginOptionsEditorProps; +type PrometheusConfigProps = DataSourcePluginOptionsEditorProps; export const ConfigEditor = (props: PrometheusConfigProps) => { const { options, onOptionsChange } = props; diff --git a/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx b/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx index ff9a8ee7208..7905dd1e530 100644 --- a/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx +++ b/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx @@ -8,7 +8,7 @@ import { PromOptions } from '../types'; import { docsTip, overhaulStyles } from './shared/utils'; -export type DataSourceHttpSettingsProps = { +type DataSourceHttpSettingsProps = { options: DataSourceSettings; onOptionsChange: (options: DataSourceSettings) => void; secureSocksDSProxyEnabled: boolean; diff --git a/packages/grafana-prometheus/src/configuration/PromSettings.tsx b/packages/grafana-prometheus/src/configuration/PromSettings.tsx index 63642bdb592..789275e0814 100644 --- a/packages/grafana-prometheus/src/configuration/PromSettings.tsx +++ b/packages/grafana-prometheus/src/configuration/PromSettings.tsx @@ -40,11 +40,6 @@ const httpOptions = [ { value: 'GET', label: 'GET' }, ]; -const editorOptions = [ - { value: QueryEditorMode.Builder, label: 'Builder' }, - { value: QueryEditorMode.Code, label: 'Code' }, -]; - const cacheValueOptions = [ { value: PrometheusCacheLevel.Low, label: 'Low' }, { value: PrometheusCacheLevel.Medium, label: 'Medium' }, @@ -86,6 +81,17 @@ export const PromSettings = (props: Props) => { const styles = overhaulStyles(theme); const { onOptionsChange } = props; + const editorOptions = [ + { + value: QueryEditorMode.Builder, + label: t('grafana-prometheus.configuration.prom-settings.editor-options.label-builder', 'Builder'), + }, + { + value: QueryEditorMode.Code, + label: t('grafana-prometheus.configuration.prom-settings.editor-options.label-code', 'Code'), + }, + ]; + const optionsWithDefaults = getOptionsWithDefaults(props.options); const [validDuration, updateValidDuration] = useState({ timeInterval: '', diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 0b1495aa288..9a64613cf3f 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -27,7 +27,7 @@ import { processLabels, toPromLikeQuery, } from './language_utils'; -import PromqlSyntax from './promql'; +import { promqlGrammar } from './promql'; import { buildVisualQueryFromString } from './querybuilder/parsing'; import { LabelsApiClient, ResourceApiClient, SeriesApiClient } from './resource_clients'; import { PromMetricsMetadata, PromQuery } from './types'; @@ -45,7 +45,7 @@ const API_V1 = { LABELS_VALUES: (labelKey: string) => `/api/v1/label/${labelKey}/values`, }; -export interface PrometheusBaseLanguageProvider { +interface PrometheusBaseLanguageProvider { datasource: PrometheusDatasource; /** @@ -70,7 +70,7 @@ export interface PrometheusBaseLanguageProvider { /** * @deprecated This interface is deprecated and will be removed. */ -export interface PrometheusLegacyLanguageProvider { +interface PrometheusLegacyLanguageProvider { /** * @deprecated Use retrieveHistogramMetrics() method instead */ @@ -736,7 +736,7 @@ export const exportToAbstractQuery = (query: PromQuery): AbstractQuery => { if (!promQuery || promQuery.length === 0) { return { refId: query.refId, labelMatchers: [] }; } - const tokens = Prism.tokenize(promQuery, PromqlSyntax); + const tokens = Prism.tokenize(promQuery, promqlGrammar); const labelMatchers: AbstractLabelMatcher[] = extractLabelMatchers(tokens); const nameLabelValue = getNameLabelValue(promQuery, tokens); if (nameLabelValue && nameLabelValue.length > 0) { diff --git a/packages/grafana-prometheus/src/language_utils.test.ts b/packages/grafana-prometheus/src/language_utils.test.ts index e71a505aad8..e30360b1b69 100644 --- a/packages/grafana-prometheus/src/language_utils.test.ts +++ b/packages/grafana-prometheus/src/language_utils.test.ts @@ -9,84 +9,12 @@ import { fixSummariesMetadata, getPrometheusTime, getRangeSnapInterval, - parseSelector, processLabels, toPromLikeQuery, truncateResult, } from './language_utils'; import { PrometheusCacheLevel } from './types'; -describe('parseSelector()', () => { - let parsed; - - it('returns a clean selector from an empty selector', () => { - parsed = parseSelector('{}', 1); - expect(parsed.selector).toBe('{}'); - expect(parsed.labelKeys).toEqual([]); - }); - - it('returns a clean selector from an unclosed selector', () => { - const parsed = parseSelector('{foo'); - expect(parsed.selector).toBe('{}'); - }); - - it('returns the selector sorted by label key', () => { - parsed = parseSelector('{foo="bar"}'); - expect(parsed.selector).toBe('{foo="bar"}'); - expect(parsed.labelKeys).toEqual(['foo']); - - parsed = parseSelector('{foo="bar",baz="xx"}'); - expect(parsed.selector).toBe('{baz="xx",foo="bar"}'); - }); - - it('returns a clean selector from an incomplete one', () => { - parsed = parseSelector('{foo}'); - expect(parsed.selector).toBe('{}'); - - parsed = parseSelector('{foo="bar",baz}'); - expect(parsed.selector).toBe('{foo="bar"}'); - - parsed = parseSelector('{foo="bar",baz="}'); - expect(parsed.selector).toBe('{foo="bar"}'); - - // Cursor in value area counts as incomplete - parsed = parseSelector('{foo="bar",baz=""}', 16); - expect(parsed.selector).toBe('{foo="bar"}'); - - parsed = parseSelector('{foo="bar",baz="4"}', 17); - expect(parsed.selector).toBe('{foo="bar"}'); - }); - - it('throws if not inside a selector', () => { - expect(() => parseSelector('foo{}', 0)).toThrow(); - expect(() => parseSelector('foo{} + bar{}', 5)).toThrow(); - }); - - it('returns the selector nearest to the cursor offset', () => { - expect(() => parseSelector('{foo="bar"} + {foo="bar"}', 0)).toThrow(); - - parsed = parseSelector('{foo="bar"} + {foo="bar"}', 1); - expect(parsed.selector).toBe('{foo="bar"}'); - - parsed = parseSelector('{foo="bar"} + {baz="xx"}', 1); - expect(parsed.selector).toBe('{foo="bar"}'); - - parsed = parseSelector('{baz="xx"} + {foo="bar"}', 16); - expect(parsed.selector).toBe('{foo="bar"}'); - }); - - it('returns a selector with metric if metric is given', () => { - parsed = parseSelector('bar{foo}', 4); - expect(parsed.selector).toBe('{__name__="bar"}'); - - parsed = parseSelector('baz{foo="bar"}', 13); - expect(parsed.selector).toBe('{__name__="baz",foo="bar"}'); - - parsed = parseSelector('bar:metric:1m{}', 14); - expect(parsed.selector).toBe('{__name__="bar:metric:1m"}'); - }); -}); - describe('fixSummariesMetadata', () => { const synthetics = { ALERTS: { diff --git a/packages/grafana-prometheus/src/language_utils.ts b/packages/grafana-prometheus/src/language_utils.ts index ebe92f3d08c..62d7fb2dfa3 100644 --- a/packages/grafana-prometheus/src/language_utils.ts +++ b/packages/grafana-prometheus/src/language_utils.ts @@ -63,9 +63,6 @@ export function processLabels(labels: Array<{ [key: string]: string }>, withName return { values: valueArray, keys: Object.keys(valueArray) }; } -// const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; -export const selectorRegexp = /\{[^}]*?(\}|$)/; - // This will capture 4 groups. Example label filter => {instance="10.4.11.4:9003"} // 1. label: instance // 2. operator: = @@ -73,71 +70,7 @@ export const selectorRegexp = /\{[^}]*?(\}|$)/; // 4. comma: if there is a comma it will give , // 5. space: if there is a space after comma it will give the whole space // comma and space is useful for addLabelsToExpression function -export const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")(,)?(\s*)?/g; - -export function parseSelector(query: string, cursorOffset = 1): { labelKeys: string[]; selector: string } { - if (!query.match(selectorRegexp)) { - // Special matcher for metrics - if (query.match(/^[A-Za-z:][\w:]*$/)) { - return { - selector: `{__name__="${query}"}`, - labelKeys: ['__name__'], - }; - } - throw new Error('Query must contain a selector: ' + query); - } - - // Check if inside a selector - const prefix = query.slice(0, cursorOffset); - const prefixOpen = prefix.lastIndexOf('{'); - const prefixClose = prefix.lastIndexOf('}'); - if (prefixOpen === -1) { - throw new Error('Not inside selector, missing open brace: ' + prefix); - } - if (prefixClose > -1 && prefixClose > prefixOpen) { - throw new Error('Not inside selector, previous selector already closed: ' + prefix); - } - const suffix = query.slice(cursorOffset); - const suffixCloseIndex = suffix.indexOf('}'); - const suffixClose = suffixCloseIndex + cursorOffset; - const suffixOpenIndex = suffix.indexOf('{'); - const suffixOpen = suffixOpenIndex + cursorOffset; - if (suffixClose === -1) { - throw new Error('Not inside selector, missing closing brace in suffix: ' + suffix); - } - if (suffixOpenIndex > -1 && suffixOpen < suffixClose) { - throw new Error('Not inside selector, next selector opens before this one closed: ' + suffix); - } - - // Extract clean labels to form clean selector, incomplete labels are dropped - const selector = query.slice(prefixOpen, suffixClose); - const labels: { [key: string]: { value: string; operator: string } } = {}; - selector.replace(labelRegexp, (label, key, operator, value) => { - const labelOffset = query.indexOf(label); - const valueStart = labelOffset + key.length + operator.length + 1; - const valueEnd = labelOffset + key.length + operator.length + value.length - 1; - // Skip label if cursor is in value - if (cursorOffset < valueStart || cursorOffset > valueEnd) { - labels[key] = { value, operator }; - } - return ''; - }); - - // Add metric if there is one before the selector - const metricPrefix = query.slice(0, prefixOpen); - const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/); - if (metricMatch) { - labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' }; - } - - // Build sorted selector - const labelKeys = Object.keys(labels).sort(); - const cleanSelector = labelKeys.map((key) => `${key}${labels[key].operator}${labels[key].value}`).join(','); - - const selectorString = ['{', cleanSelector, '}'].join(''); - - return { labelKeys, selector: selectorString }; -} +const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")(,)?(\s*)?/g; export function expandRecordingRules(query: string, mapping: { [name: string]: RecordingRuleIdentifier }): string { const getRuleRegex = (ruleName: string) => new RegExp(`(\\s|\\(|^)(${ruleName})(\\s|$|\\(|\\[|\\{)`, 'ig'); @@ -334,23 +267,19 @@ export function roundMsToMin(milliseconds: number): number { return roundSecToMin(milliseconds / 1000); } -export function roundSecToMin(seconds: number): number { +function roundSecToMin(seconds: number): number { return Math.floor(seconds / 60); } // Returns number of minutes rounded up to the nearest nth minute -export function roundSecToNextMin(seconds: number, secondsToRound = 1): number { +function roundSecToNextMin(seconds: number, secondsToRound = 1): number { return Math.ceil(seconds / 60) - (Math.ceil(seconds / 60) % secondsToRound); } -export function limitSuggestions(items: string[]) { +function limitSuggestions(items: string[]) { return items.slice(0, SUGGESTIONS_LIMIT); } -export function addLimitInfo(items: unknown[] | undefined): string { - return items && items.length >= SUGGESTIONS_LIMIT ? `, limited to the first ${SUGGESTIONS_LIMIT} received items` : ''; -} - const FromPromLikeMap: Record = { '=': AbstractLabelOperator.Equal, '!=': AbstractLabelOperator.NotEqual, @@ -363,7 +292,7 @@ const ToPromLikeMap: Record = invert(FromPromLike string >; -export function toPromLikeExpr(labelBasedQuery: AbstractQuery): string { +function toPromLikeExpr(labelBasedQuery: AbstractQuery): string { const expr = labelBasedQuery.labelMatchers .map((selector: AbstractLabelMatcher) => { const operator = ToPromLikeMap[selector.operator]; @@ -387,7 +316,7 @@ export function toPromLikeQuery(labelBasedQuery: AbstractQuery): PromLikeQuery { }; } -export interface PromLikeQuery extends DataQuery { +interface PromLikeQuery extends DataQuery { expr: string; range: boolean; } diff --git a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json index 97bd700ee53..52a2547c672 100644 --- a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Poskytnout zpětnou vazbu", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -276,6 +324,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formát", "label-min-step": "", @@ -290,6 +343,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Spustit dotaz", "label-explain": "", "run-queries": "", @@ -306,6 +361,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -332,6 +393,9 @@ "results-table": { "content-descriptive-type": "", "description": "Popis", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Název", "select": "Vybrat", "type": "Typ" diff --git a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json index b2a62f0ced1..6d5b75460fb 100644 --- a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Feedback geben", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Format", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Starten Sie Ihre Abfrage", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Beschreibung", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Name", "select": "Auswählen", "type": "Typ" diff --git a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json index 5de8bc8d245..7885c3d457c 100644 --- a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "{{promType}} type", "aria-label-prometheus-type": "Prometheus type", "aria-label-select-http-method": "Select HTTP method", + "editor-options": { + "label-builder": "Builder", + "label-code": "Code" + }, "label-cache-level": "Cache level", "label-custom-query-parameters": "Custom query parameters", "label-default-editor": "Default editor", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "Checking this option will favor the series endpoint with {{exampleParameter}} parameter over the label values endpoint with {{exampleParameter}} parameter. While the label values endpoint is considered more performant, some users may prefer the series because it has a POST method while the label values endpoint only has a GET method." } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "Only includes unique labels", + "description-custom": "Provide a naming template", + "description-verbose": "All label names and values", + "label-auto": "Auto", + "label-custom": "Custom", + "label-verbose": "Verbose" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Filter metric names by regex search, using an additional call on the Prometheus API.", @@ -204,6 +218,36 @@ "give-feedback": "Give feedback", "title-give-feedback": "The metrics explorer is new, please let us know how we can improve it" }, + "get-collapsed-info": { + "exemplars": "Exemplars: {{value}}", + "format": "Format: {{value}}", + "legend": "Legend: {{value}}", + "step": "Step: {{value}}", + "type": "Type: {{value}}" + }, + "get-placeholders": { + "browse": "Search metrics by name", + "include-null-metadata": "Include results with no metadata", + "metadata-search-switch": "Include description in search", + "set-use-backend": "Enable regex search", + "type": "Filter by type" + }, + "get-prom-types": { + "description-counter": "A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.", + "description-gauge": "A metric that represents a single numerical value that can arbitrarily go up and down.", + "description-histogram": "A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.", + "description-native-histogram": "Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.", + "description-no-type": "These metrics have no defined type in the metadata.", + "description-summary": "A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.", + "description-unknown": "These metrics have been given the type unknown in the metadata.", + "label-counter": "Counter", + "label-gauge": "Gauge", + "label-histogram": "Histogram", + "label-native-histogram": "Native histogram", + "label-no-type": "No type", + "label-summary": "Summary", + "label-unknown": "Unknown" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "Query parsing is ambiguous." @@ -219,6 +263,10 @@ "label-label-filters": "Label filters", "tooltip-label-filters": "Optional: used to filter the metric select for this query type." }, + "label-param-editor": { + "loadingMessage-loading-labels": "Loading labels", + "noOptionsMessage-no-labels-found": "No labels found" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "Open metrics explorer", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Set lower limit for the step parameter", "aria-label-select-resolution": "Select resolution", + "format-options": { + "label-heatmap": "Heatmap", + "label-table": "Table", + "label-time-series": "Time series" + }, "label-exemplars": "Exemplars", "label-format": "Format", "label-min-step": "Min step", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "The number of metric names exceeds the autocomplete limit. Only the {{autocompleteLimit}}-most relevant metrics are displayed. You can adjust the threshold in the data source settings." }, "prom-query-editor-selector": { + "body-syntax-error": "There is a syntax error, or the query structure cannot be visualized when switching to the builder mode. Parts of the query may be lost.", + "confirmText-continue": "Continue", "kick-start-your-query": "Kick start your query", "label-explain": "Explain", "run-queries": "Run queries", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "hint: {{hintDetails}}" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "Builder", + "label-code": "Code" + } + }, "query-pattern": { "apply-query": "Apply query", "aria-label-apply-query-starter-button": "apply query starter button", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "When creating a {{descriptiveType}}, Prometheus exposes multiple series with the type counter. ", "description": "Description", + "message-expand-label-filters": "There are no metrics found. Try to expand your label filters.", + "message-expand-search": "There are no metrics found. Try to expand your search and filters.", + "message-no-metrics-found": "There are no metrics found in the data source.", "name": "Name", "select": "Select", "type": "Type" diff --git a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json index 2cfb484899d..3637b63be9f 100644 --- a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Enviar comentarios", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formato", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Inicie su consulta", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Descripción", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Nombre", "select": "Seleccionar", "type": "Tipo" diff --git a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json index 3d8d947716d..98411174ec0 100644 --- a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Publiez votre commentaire", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Format", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Lancer votre requête", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Description", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Nom", "select": "Sélectionner", "type": "Type" diff --git a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json index fb93de301a0..530cb9dd643 100644 --- a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Visszajelzés küldése", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formátum", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Előbeállításos lekérdezés", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Leírás", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Név", "select": "Kijelölés", "type": "Típus" diff --git a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json index efb21274cbd..f3695a08dc1 100644 --- a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Berikan umpan balik", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -273,6 +321,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Format", "label-min-step": "", @@ -287,6 +340,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Mulai kueri Anda", "label-explain": "", "run-queries": "", @@ -303,6 +358,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -329,6 +390,9 @@ "results-table": { "content-descriptive-type": "", "description": "Deskripsi", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Nama", "select": "Pilih", "type": "Jenis" diff --git a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json index 43fb6ff60a7..08a0c9261c6 100644 --- a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Lascia un feedback", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formato", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Avvia la query", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Descrizione", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Nome", "select": "Seleziona", "type": "Tipo" diff --git a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json index f9560e7c3a3..9eb35073868 100644 --- a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "フィードバックを送信", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -273,6 +321,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "形式", "label-min-step": "", @@ -287,6 +340,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "クエリを開始", "label-explain": "", "run-queries": "", @@ -303,6 +358,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -329,6 +390,9 @@ "results-table": { "content-descriptive-type": "", "description": "説明", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "名前", "select": "選択", "type": "種類" diff --git a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json index c67caa63f50..0d2f8634ffb 100644 --- a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "피드백 제출하기", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -273,6 +321,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "형식", "label-min-step": "", @@ -287,6 +340,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "쿼리 시작하기", "label-explain": "", "run-queries": "", @@ -303,6 +358,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -329,6 +390,9 @@ "results-table": { "content-descriptive-type": "", "description": "설명", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "이름", "select": "선택", "type": "유형" diff --git a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json index e25eaefff64..287182071c8 100644 --- a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Feedback geven", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formaat", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Start je query", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Beschrijving", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Naam", "select": "Selecteren", "type": "Type" diff --git a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json index ad21fccdb5a..ba03c50dc97 100644 --- a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Przekaż opinię", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -276,6 +324,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Format", "label-min-step": "", @@ -290,6 +343,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Uruchom zapytanie", "label-explain": "", "run-queries": "", @@ -306,6 +361,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -332,6 +393,9 @@ "results-table": { "content-descriptive-type": "", "description": "Opis", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Imię", "select": "Wybierz", "type": "Typ" diff --git a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json index 81801a8c07d..8dfc0746a76 100644 --- a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Dar feedback", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formato", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Iniciar sua consulta", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Descrição", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Nome", "select": "Selecionar", "type": "Tipo" diff --git a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json index fcfd4a81bc0..adef904aa1a 100644 --- a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Dar feedback", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Formato", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Dê início à sua consulta", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Descrição", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Nome", "select": "Selecionar", "type": "Tipo" diff --git a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json index 6524e3464eb..0c618e57e2a 100644 --- a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Отправить отзыв", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -276,6 +324,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Формат", "label-min-step": "", @@ -290,6 +343,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Запустить запрос", "label-explain": "", "run-queries": "", @@ -306,6 +361,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -332,6 +393,9 @@ "results-table": { "content-descriptive-type": "", "description": "Описание", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Имя", "select": "Выбрать", "type": "Тип" diff --git a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json index eff0092cdb4..a7da455bdad 100644 --- a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Ge feedback", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Format", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Kickstarta din fråga", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Beskrivning", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Namn", "select": "Välj", "type": "Typ" diff --git a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json index f1cc39bb7ef..995dcb57cea 100644 --- a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json @@ -157,6 +157,10 @@ "aria-label-prom-type-type": "", "aria-label-prometheus-type": "", "aria-label-select-http-method": "", + "editor-options": { + "label-builder": "", + "label-code": "" + }, "label-cache-level": "", "label-custom-query-parameters": "", "label-default-editor": "", @@ -195,6 +199,16 @@ "tooltip-use-series-endpoint": "" } }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -204,6 +218,36 @@ "give-feedback": "Geri bildirim gönder", "title-give-feedback": "" }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "" @@ -219,6 +263,10 @@ "label-label-filters": "", "tooltip-label-filters": "" }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" + }, "metric-combobox": { "async-select": { "aria-label-open-metrics-explorer": "", @@ -274,6 +322,11 @@ "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, "label-exemplars": "", "label-format": "Biçim", "label-min-step": "", @@ -288,6 +341,8 @@ "tooltip-autocomplete-suggestions-limited": "" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "Sorgunuzu hızlı başlatın", "label-explain": "", "run-queries": "", @@ -304,6 +359,12 @@ "query-editor-hints": { "hint-details": "" }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } + }, "query-pattern": { "apply-query": "", "aria-label-apply-query-starter-button": "", @@ -330,6 +391,9 @@ "results-table": { "content-descriptive-type": "", "description": "Açıklama", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "Ad", "select": "Seçin", "type": "Tür" diff --git a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json index d5194ff8a10..7edad556e3e 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json @@ -2,340 +2,404 @@ "grafana-prometheus": { "components": { "annotation-query-editor": { - "annotation-data-load-error": "", - "aria-label-lower-limit-parameter": "", - "label-min-step": "", - "label-series-value-as-timestamp": "", + "annotation-data-load-error": "注释数据加载错误!", + "aria-label-lower-limit-parameter": "设置步长参数的下限", + "label-min-step": "最小步长", + "label-series-value-as-timestamp": "作为时间戳的序列值", "label-tags": "标签", - "label-text": "", + "label-text": "文本", "label-title": "标题", - "placeholder-auto": "", - "tooltip-either-pattern-example-instance-replaced-label": "", - "tooltip-min-step": "", - "tooltip-timestamp-milliseconds-series-value-seconds-multiply": "" + "placeholder-auto": "自动", + "tooltip-either-pattern-example-instance-replaced-label": "使用名称或模式。例如,{{labelTemplate}}将替换为标签{{labelName}}的标签值。", + "tooltip-min-step": "Prometheus 查询的步长参数以及 <2>{{intervalVar}} 和 <4>{{rateIntervalVar}} 变量的附加下限。", + "tooltip-timestamp-milliseconds-series-value-seconds-multiply": "时间戳的单位是毫秒。如果序列值的单位为秒,则将其范围向量乘以 1000。" }, "get-query-type-options": { "description": { - "instant-query-range": "" + "instant-query-range": "运行即时查询和范围查询" }, "label": { - "both": "" + "both": "两个都" }, "range-options": { "description": { - "query-range": "" + "query-range": "针对时间范围运行查询" }, "label": { - "instant": "", - "range": "" + "instant": "即时", + "range": "范围" } } }, "label-selector": { - "aria-label-filter-expression-for-label": "", - "description-select-labels": "", - "select-labels-to-search-in": "" + "aria-label-filter-expression-for-label": "筛选标签表达式", + "description-select-labels": "选择标签值后,仅显示可能的标签组合。", + "select-labels-to-search-in": "2. 选择要搜索的标签" }, "metric-selector": { - "aria-label-filter-expression-for-metric": "", - "aria-label-limit-results-from-series-endpoint": "", - "description-series-limit": "", - "label-select-metric": "", - "select-a-metric": "", - "series-limit": "" + "aria-label-filter-expression-for-metric": "筛选指标表达式", + "aria-label-limit-results-from-series-endpoint": "限制来自序列端点的结果", + "description-series-limit": "该限制适用于所有指标、标签和值。若将字段留空,则使用默认限制。设置为 0 以禁用限制并获取所有内容,但这可能会导致性能问题。", + "label-select-metric": "选择指标后,仅显示可能的标签。标签受以下序列限制的约束。", + "select-a-metric": "1. 选择指标", + "series-limit": "序列限制" }, "prom-cheat-sheet": { - "prom-ql-cheat-sheet": "" + "prom-ql-cheat-sheet": "PromQL 速查表" }, "prom-exemplar-field": { - "exemplars": "", - "tooltip-disable-query": "", - "tooltip-enable-query": "" + "exemplars": "范例", + "tooltip-disable-query": "禁用带有范例的查询", + "tooltip-enable-query": "启用带有范例的查询" }, "prom-explore-extra-field": { - "aria-label-prometheus-extra-field": "", - "aria-label-query-type-field": "", - "aria-label-step-field": "", - "min-step": "", - "query-type": "", - "tooltip-units-builtin-variables-example-interval-rateinterval": "" + "aria-label-prometheus-extra-field": "Prometheus 额外字段", + "aria-label-query-type-field": "查询类型字段", + "aria-label-step-field": "步长字段", + "min-step": "最小步长", + "query-type": "查询类型", + "tooltip-units-builtin-variables-example-interval-rateinterval": "此处可以使用时间单位和内置变量,例如:{{example1}}、{{example2}}、{{example3}}、{{example4}}{{example5}}、{{example6}}、{{example7}}(如果未指定单位,则为默认值:{{default}})" }, "prom-query-field": { - "placeholder-enter-a-prom-ql-query": "" + "placeholder-enter-a-prom-ql-query": "输入 PromQL 查询……" }, "prom-variable-query-editor": { - "aria-label-classic-query": "", - "aria-label-metric-regex": "", - "aria-label-metric-selector": "", - "aria-label-prometheus-query": "", - "aria-label-query-type": "", - "aria-label-series-query": "", - "label-classic-query": "", + "aria-label-classic-query": "经典查询", + "aria-label-metric-regex": "指标正则表达式", + "aria-label-metric-selector": "指标选择器", + "aria-label-prometheus-query": "Prometheus 查询", + "aria-label-query-type": "查询类型", + "aria-label-series-query": "序列查询", + "label-classic-query": "经典查询", "label-label": "标签", - "label-metric-regex": "", + "label-metric-regex": "指标正则表达式", "label-query": "查询", - "label-query-type": "", - "label-series-query": "", - "placeholder-classic-query": "", - "placeholder-metric-regex": "", - "placeholder-prometheus-query": "", + "label-query-type": "查询类型", + "label-series-query": "序列查询", + "placeholder-classic-query": "经典查询", + "placeholder-metric-regex": "指标正则表达式", + "placeholder-prometheus-query": "Prometheus 查询", "placeholder-select-query-type": "选择查询类型", - "placeholder-series-query": "", - "returns-metrics-matching-specified-metric-regex": "", - "tooltip-classic-query": "", - "tooltip-label": "", - "tooltip-metric-regex": "", - "tooltip-query": "", - "tooltip-query-type": "", - "tooltip-series-query": "" + "placeholder-series-query": "序列查询", + "returns-metrics-matching-specified-metric-regex": "返回与指定指标正则表达式匹配的指标列表。", + "tooltip-classic-query": "Prometheus 变量查询编辑器的原始执行操作。按照这些文档中所述,输入包含正确查询类型和参数的字符串。例如,{{exampleQuery}}。", + "tooltip-label": "返回所有指标中标签名称的标签值列表,除非指定了指标。", + "tooltip-metric-regex": "返回标签名称列表,可选择按指定的指标正则表达式进行筛选。", + "tooltip-query": "返回查询的 Prometheus 查询结果列表。这可以包含 Prometheus 函数,即 {{exampleQuery}}。", + "tooltip-query-type": "Prometheus 数据源插件为模板变量提供以下查询类型。", + "tooltip-series-query": "输入带有标签的指标、仅指标或仅标签,即 {{example1}}、{{example2}} 或 {{example3}}。返回与输入数据关联的时间序列列表。" }, "selector-actions": { - "aria-label-selector": "", - "aria-label-selector-clear-button": "", - "aria-label-use-selector-as-metrics-button": "", - "aria-label-use-selector-for-query-button": "", - "aria-label-validate-submit-button": "", + "aria-label-selector": "选择器", + "aria-label-selector-clear-button": "选择器清除按钮", + "aria-label-use-selector-as-metrics-button": "使用选择器作为指标按钮", + "aria-label-use-selector-for-query-button": "使用选择器作为查询按钮", + "aria-label-validate-submit-button": "验证提交按钮", "clear": "清除", - "resulting-selector": "", - "use-as-rate-query": "", - "use-query": "", - "validate-selector": "" + "resulting-selector": "4. 结果选择器", + "use-as-rate-query": "用作速率查询", + "use-query": "使用查询", + "validate-selector": "验证选择器" }, "value-selector": { - "aria-label-filter-expression-for-label-values": "", - "aria-label-values-for": "", - "description-search-field-values-across-selected-labels": "", - "select-multiple-values-for-your-labels": "" + "aria-label-filter-expression-for-label-values": "筛选标签值表达式", + "aria-label-values-for": "{{labelKey}}的值", + "description-search-field-values-across-selected-labels": "使用搜索字段在所选标签中查找值。", + "select-multiple-values-for-your-labels": "3. 为您的标签选择(多个)值" } }, "configuration": { "alerting-settings-overhaul": { - "label-allow-as-recording-rules-target": "", - "label-manage-alerts-via-alerting-ui": "", + "label-allow-as-recording-rules-target": "允许作为录制规则目标", + "label-manage-alerts-via-alerting-ui": "通过警报用户界面管理警报", "title-alerting": "警报", - "tooltip-allow-as-recording-rules-target": "", + "tooltip-allow-as-recording-rules-target": "允许此数据源被选为撰写录制规则的目标。", "tooltip-manage-alerts-via-alerting-ui": "管理此数据源的提醒规则。要管理其他 Alerting 资源,请添加 Alertmanager 数据源。" }, "config-editor": { - "browser-access-mode-error": "", - "description-advanced-settings": "", + "browser-access-mode-error": "Prometheus 数据源中的浏览器访问模式不再可用。切换到服务器访问模式。", + "description-advanced-settings": "附加设置是可选设置,通过配置,可对数据源进行更精细的控制。", "title-advanced-settings": "高级设置", "title-error": "错误" }, "data-source-http-settings-overhaul": { "tooltip-browser-access-mode": "您的访问方式是<1>浏览器,这意味着需要从浏览器访问 URL。", - "tooltip-http-url": "", + "tooltip-http-url": "指定完整的 HTTP 网址(例如 {{exampleURL}})", "tooltip-server-access-mode": "您的访问方式是<1>服务器,这意味着需要从 grafana 后端/服务器访问 URL。" }, "docs-tip": { - "visit-docs-for-more-details-here": "" + "visit-docs-for-more-details-here": "请访问此处的文档了解更多信息。" }, "exemplar-setting": { "label-data-source": "数据源", - "label-internal-link": "", + "label-internal-link": "内部链接", "label-label-name": "标签名称", - "label-remove-exemplar-link": "", + "label-remove-exemplar-link": "移除范例链接", "label-url": "URL", - "label-url-label": "", - "placeholder-go-to-examplecom": "", - "placeholder-httpsexamplecomvalueraw": "", - "placeholder-trace-id": "", - "title-remove-exemplar-link": "", - "tooltip-data-source": "", - "tooltip-internal-link": "", - "tooltip-label-name": "", - "tooltip-url": "", - "tooltip-url-label": "" + "label-url-label": "网址标签", + "placeholder-go-to-examplecom": "前往 example.com", + "placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}", + "placeholder-trace-id": "traceID", + "title-remove-exemplar-link": "移除范例链接", + "tooltip-data-source": "范例将要导航到的数据源。", + "tooltip-internal-link": "如果您有内部链接,请启用此选项。启用后,页面将出现数据源选择器。为您的范例数据选择后端跟踪数据存储。", + "tooltip-label-name": "应用于获取 traceID 的标签对象中的字段名称。", + "tooltip-url": "用户将查看其跟踪的跟踪后端的网址", + "tooltip-url-label": "用于覆盖示例 traceID 字段上的按钮标签。" }, "exemplars-settings": { "add": "添加", - "no-exemplars-configurations": "", - "title-exemplars": "" + "no-exemplars-configurations": "没有范例配置", + "title-exemplars": "范例" }, "prom-settings": { - "aria-label-default-editor": "", - "aria-label-prom-type-type": "", - "aria-label-prometheus-type": "", - "aria-label-select-http-method": "", - "label-cache-level": "", - "label-custom-query-parameters": "", - "label-default-editor": "", - "label-disable-metrics-lookup": "", - "label-disable-recording-rules-beta": "", - "label-http-method": "", - "label-incremental-querying-beta": "", - "label-metric-names-suggestion-limit": "", - "label-prom-type-version": "", - "label-prometheus-type": "", - "label-query-overlap-window": "", - "label-query-timeout": "", - "label-scrape-interval": "", - "label-series-limit": "", - "label-use-series-endpoint": "", - "more-info": "", - "placeholder-example-maxsourceresolutionmtimeout": "", - "title-interval-behaviour": "", - "title-other": "", + "aria-label-default-editor": "默认编辑器(代码或构建器)", + "aria-label-prom-type-type": "{{promType}} 类型", + "aria-label-prometheus-type": "Prometheus 类型", + "aria-label-select-http-method": "选择 HTTP 方法", + "editor-options": { + "label-builder": "", + "label-code": "" + }, + "label-cache-level": "缓存级别", + "label-custom-query-parameters": "自定义查询参数", + "label-default-editor": "默认编辑器", + "label-disable-metrics-lookup": "禁用指标查找", + "label-disable-recording-rules-beta": "禁用录制规则(测试版)", + "label-http-method": "HTTP 方法", + "label-incremental-querying-beta": "增量查询(测试版)", + "label-metric-names-suggestion-limit": "指标名称建议限制", + "label-prom-type-version": "{{promType}} 版本", + "label-prometheus-type": "Prometheus 类型", + "label-query-overlap-window": "查询重叠窗口", + "label-query-timeout": "查询超时", + "label-scrape-interval": "抓取间隔", + "label-series-limit": "序列限制", + "label-use-series-endpoint": "使用序列端点", + "more-info": "有关在数据源中配置 prometheus 类型和版本的更多信息,请参阅<2>预配文档。", + "placeholder-example-maxsourceresolutionmtimeout": "示例:{{example}}", + "title-interval-behaviour": "间隔行为", + "title-other": "其他", "title-performance": "性能", - "title-query-editor": "", - "tooltip-cache-level": "", - "tooltip-custom-query-parameters": "", - "tooltip-default-editor": "", - "tooltip-disable-metrics-lookup": "", - "tooltip-disable-recording-rules-beta": "", - "tooltip-http-method": "", - "tooltip-incremental-querying-beta": "", - "tooltip-metric-names-suggestion-limit": "", - "tooltip-prom-type-version": "", - "tooltip-prometheus-type": "", - "tooltip-query-overlap-window": "", - "tooltip-query-timeout": "", - "tooltip-scrape-interval": "", - "tooltip-series-limit": "", - "tooltip-use-series-endpoint": "" + "title-query-editor": "查询编辑器", + "tooltip-cache-level": "设置编辑器查询的浏览器缓存级别。对于高基数数据源,建议使用更高的缓存设置。", + "tooltip-custom-query-parameters": "将自定义参数添加到 Prometheus 查询网址。例如 {{example1}}、{{example2}}、{{example3}} 或 {{example4}}。多个参数应使用 {{concatenationChar}} 连接起来。", + "tooltip-default-editor": "为此数据源的所有用户设置默认编辑器选项。", + "tooltip-disable-metrics-lookup": "选中此选项将禁用查询字段自动完成中的指标选择器和指标/标签支持。如果您在使用较大的 Prometheus 实例时遇到性能问题,这样做会有所帮助。", + "tooltip-disable-recording-rules-beta": "此功能将禁用录制规则。开启此选项可提高数据面板性能", + "tooltip-http-method": "您可以使用 POST 或 GET HTTP 方法来查询 Prometheus 数据源。POST 是推荐的方法,因为它允许更大的查询。如果您的 Prometheus 版本低于 2.1,或者您的网络中限制了 POST 请求,请将其更改为 GET。", + "tooltip-incremental-querying-beta": "此功能将改变相对查询的默认行为,使其总是从 Prometheus 实例请求新数据,而不会缓存查询结果,且只请求新数据。开启此选项可减少数据库和网络负载。", + "tooltip-metric-names-suggestion-limit": "在查询编辑器的“代码”模式下,可能显示为自动完成建议的指标名称的最大数量。", + "tooltip-prom-type-version": "如果 {{promType}} 实例未自动配置版本,请使用此项来设置它的版本。", + "tooltip-prometheus-type": "将此设置为 Prometheus 数据库的类型,例如 Prometheus、Cortex、Mimir 或 Thanos。更改此字段将保存您的当前设置。某些类型的 Prometheus 对 API 的支持不一样。例如,某些类型支持标签查询的正则表达式匹配,以提高性能。某些类型具有用于元数据的 API。如果设置不正确,则在查询指标和标签时可能会遇到奇怪的行为。请检查您的 Prometheus 文档,以确保您输入了正确的类型。", + "tooltip-query-overlap-window": "设置持续时间,如 {{example1}} 或 {{example2}} 或 {{example3}}。默认值为 {{default}}。此持续时间将添加到每个增量请求的持续时间中。", + "tooltip-query-timeout": "设置 Prometheus 查询超时。", + "tooltip-scrape-interval": "此间隔是 Prometheus 抓取目标的频率。将此设置为 Prometheus 配置文件中配置的典型抓取和评估间隔。如果您将此值设置为大于 Prometheus 配置文件的间隔,Grafana 将根据此间隔评估数据,您将看到较少的数据点。默认值为 {{default}}。", + "tooltip-series-limit": "限制适用于两个端点(系列和标签)的所有资源(指标、标签和值)。将字段留空,以使用默认限制 (40000)。设置为 0 以禁用限制并获取所有内容,但这可能会导致性能问题。默认限制为 40000。", + "tooltip-use-series-endpoint": "选中此选项将优先使用带有 {{exampleParameter}} 参数的序列端点,而不是带有 {{exampleParameter}} 参数的标签值端点。虽然标签值端点在性能方面表现更好,但有些用户可能更喜欢序列,因为它具有 POST 方法,而标签值端点仅具有 GET 方法。" + } + }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" } }, "querybuilder": { "additional-settings": { - "content-filter-metric-names-regex-search-using": "", - "disable-text-wrap": "" + "content-filter-metric-names-regex-search-using": "使用 Prometheus API 上的其他调用,通过正则表达式搜索筛选指标名称。", + "disable-text-wrap": "禁用文本换行" }, "feedback-link": { "give-feedback": "提供反馈", - "title-give-feedback": "" + "title-give-feedback": "指标浏览器是新功能,请告诉我们如何改进" + }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" }, "handle-function": { "text": { - "query-parsing-is-ambiguous": "" + "query-parsing-is-ambiguous": "查询解析不明确。" } }, "label-filter-item": { - "aria-label-remove": "", - "placeholder-select-label": "", + "aria-label-remove": "移除{{name}}", + "placeholder-select-label": "选择标签", "placeholder-select-value": "选择值" }, "label-filters": { - "label-filters": "", - "label-label-filters": "", - "tooltip-label-filters": "" + "label-filters": "标签筛选", + "label-label-filters": "标签筛选", + "tooltip-label-filters": "可选:用于筛选为此查询类型选择的指标。" + }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" }, "metric-combobox": { "async-select": { - "aria-label-open-metrics-explorer": "", - "placeholder-select-metric": "", - "tooltip-open-metrics-explorer": "" + "aria-label-open-metrics-explorer": "打开指标浏览器", + "placeholder-select-metric": "选择指标", + "tooltip-open-metrics-explorer": "打开指标浏览器" }, "label-metric": "指标", - "tooltip-metric": "" + "tooltip-metric": "可选:返回指定指标中标签名称的标签值列表。" }, "metrics-modal": { - "additional-settings": "", + "additional-settings": "其他设置", "aria-label-additional-settings": "附加设置", - "aria-label-browse-metrics": "", - "currently-selected": "", - "metrics-pre-filtered": "", - "placeholder-results-per-page": "", - "results-per-page": "", - "title-metrics-explorer": "", - "results-amount_other": "" + "aria-label-browse-metrics": "浏览指标", + "currently-selected": "当前选择:{{selected}}", + "metrics-pre-filtered": "这些指标已通过标签筛选器中选择的标签进行预筛选。", + "placeholder-results-per-page": "每页显示结果数", + "results-per-page": "每页结果数", + "title-metrics-explorer": "指标浏览器", + "results-amount_other": "显示 {{num}} 个结果(共 {{count}} 个)" }, "nested-query": { "label": { - "ignoring": "", + "ignoring": "忽略", "on": "方式" }, "operator": "运算符", - "tooltip-remove-match": "", - "vector-matches": "" + "tooltip-remove-match": "移除匹配", + "vector-matches": "矢量匹配" }, "operation-editor": { - "not-found": "", - "title-remove": "" + "not-found": "找不到操作 {{id}}", + "title-remove": "移除{{name}}" }, "operation-header": { "placeholder-replace-with": "替换为", - "title-click-to-view-alternative-operations": "", - "title-remove-operation": "" + "title-click-to-view-alternative-operations": "点击以查看替代操作", + "title-remove-operation": "移除操作" }, "operation-info-button": { - "title-click-to-show-description": "", - "title-remove-operation": "" + "title-click-to-show-description": "点击以显示描述", + "title-remove-operation": "移除操作" }, "operation-list": { - "operations": "", + "operations": "操作", "placeholder-search": "搜索", - "title-add-operation": "" + "title-add-operation": "添加操作" }, "operation-param-editor": { - "title-add": "", - "title-remove": "" + "title-add": "添加{{name}}", + "title-remove": "移除{{name}}" }, "prom-query-builder-options": { - "aria-label-lower-limit-parameter": "", - "aria-label-select-resolution": "", - "label-exemplars": "", + "aria-label-lower-limit-parameter": "设置步长参数的下限", + "aria-label-select-resolution": "选择分辨率", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, + "label-exemplars": "范例", "label-format": "格式", - "label-min-step": "", - "label-resolution": "", + "label-min-step": "最小步长", + "label-resolution": "分辨率", "label-type": "类型", - "placeholder-auto": "", + "placeholder-auto": "自动", "title-options": "选项", - "tooltip-min-step": "" + "tooltip-min-step": "Prometheus 查询的步长参数以及 <2>{{interval}} 和 <4>{{rateInterval}} 变量的附加下限。" }, "prom-query-code-editor-autocomplete-info": { - "autocomplete-suggestions-limited": "", - "tooltip-autocomplete-suggestions-limited": "" + "autocomplete-suggestions-limited": "自动完成建议受限", + "tooltip-autocomplete-suggestions-limited": "指标名称的数量超过了自动完成限制。仅显示 {{autocompleteLimit}} 个最相关的指标。您可以在数据源设置中调整阈值。" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "启动您的查询", - "label-explain": "", - "run-queries": "", - "title-parsing-error-switch-builder": "" + "label-explain": "解释", + "run-queries": "运行查询", + "title-parsing-error-switch-builder": "解析错误:切换到构建器模式?" }, "prom-query-legend-editor": { - "label-legend": "", - "placeholder-select-legend-mode": "", - "tooltip-legend": "" + "label-legend": "图例", + "placeholder-select-legend-mode": "选择图例模式", + "tooltip-legend": "序列名称覆盖或模板。例如,{{templateExample}} 将替换为 {{labelName}} 的标签值。" }, "query-builder-hints": { - "hint-details": "" + "hint-details": "提示:{{hintDetails}}" }, "query-editor-hints": { - "hint-details": "" + "hint-details": "提示:{{hintDetails}}" + }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } }, "query-pattern": { - "apply-query": "", - "aria-label-apply-query-starter-button": "", - "aria-label-back-button": "", - "aria-label-create-new-query-button": "", - "aria-label-raw-query": "", - "aria-label-use-this-query-button": "", + "apply-query": "应用查询", + "aria-label-apply-query-starter-button": "应用查询启动按钮", + "aria-label-back-button": "后退按钮", + "aria-label-create-new-query-button": "创建新查询按钮", + "aria-label-raw-query": "{{patternName}} 原始查询", + "aria-label-use-this-query-button": "使用此查询按钮", "back": "上一步", - "create-new-query": "", + "create-new-query": "创建新查询", "use-this-query": "使用此查询" }, "query-patterns-modal": { - "aria-label-close-kick-start-your-query-modal": "", + "aria-label-close-kick-start-your-query-modal": "关闭“快速启动查询”弹窗", "aria-label-kick-start-your-query-modal": "启动您的查询模式", - "aria-label-toggle-query-starter": "", + "aria-label-toggle-query-starter": "打开和关闭 {{patternType}} 查询启动卡", "close": "关闭", - "description-kick-start-your-query": "", - "label-toggle-query-starter": "", + "description-kick-start-your-query": "通过选择其中一个查询来快速启动您的查询。然后,您可以继续完成查询。", + "label-toggle-query-starter": "{{patternType}} 查询启动器", "title-kick-start-your-query": "启动您的查询" }, "raw-query": { - "aria-label-selector": "" + "aria-label-selector": "选择器" }, "results-table": { - "content-descriptive-type": "", + "content-descriptive-type": "创建 {{descriptiveType}} 时,Prometheus 会使用类型计数器公开多个序列。", "description": "描述", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "名称", "select": "选择", "type": "类型" }, "update-function-args": { "text": { - "query-parsing-is-ambiguous": "" + "query-parsing-is-ambiguous": "查询解析不明确。" } } } diff --git a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json index 88d1bf2d1e5..8017f7dca29 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json @@ -2,340 +2,404 @@ "grafana-prometheus": { "components": { "annotation-query-editor": { - "annotation-data-load-error": "", - "aria-label-lower-limit-parameter": "", - "label-min-step": "", - "label-series-value-as-timestamp": "", + "annotation-data-load-error": "註解資料載入錯誤!", + "aria-label-lower-limit-parameter": "設定步驟參數的下限", + "label-min-step": "最少步驟數", + "label-series-value-as-timestamp": "作為時間戳記的序列值", "label-tags": "標籤", - "label-text": "", + "label-text": "文字", "label-title": "標題", - "placeholder-auto": "", - "tooltip-either-pattern-example-instance-replaced-label": "", - "tooltip-min-step": "", - "tooltip-timestamp-milliseconds-series-value-seconds-multiply": "" + "placeholder-auto": "自動", + "tooltip-either-pattern-example-instance-replaced-label": "使用名稱或模式。例如,{{labelTemplate}} 會替換為標籤{{labelName}}的標籤值。", + "tooltip-min-step": "Prometheus 查詢的步驟參數以及 <2>{{intervalVar}} 和 <4>{{rateIntervalVar}} 變數的其他下限。", + "tooltip-timestamp-milliseconds-series-value-seconds-multiply": "時間戳記的單位為毫秒。如果序列值的單位為秒,請將其範圍向量乘以 1000。" }, "get-query-type-options": { "description": { - "instant-query-range": "" + "instant-query-range": "執行即時查詢和範圍查詢" }, "label": { - "both": "" + "both": "兩者" }, "range-options": { "description": { - "query-range": "" + "query-range": "針對一段時間執行查詢" }, "label": { - "instant": "", - "range": "" + "instant": "立即", + "range": "範圍" } } }, "label-selector": { - "aria-label-filter-expression-for-label": "", - "description-select-labels": "", - "select-labels-to-search-in": "" + "aria-label-filter-expression-for-label": "篩選標籤的表達式", + "description-select-labels": "一旦選取標籤值,只會顯示可能的標籤組合。", + "select-labels-to-search-in": "2. 選取您要搜尋的標籤" }, "metric-selector": { - "aria-label-filter-expression-for-metric": "", - "aria-label-limit-results-from-series-endpoint": "", - "description-series-limit": "", - "label-select-metric": "", - "select-a-metric": "", - "series-limit": "" + "aria-label-filter-expression-for-metric": "篩選指標的表達式", + "aria-label-limit-results-from-series-endpoint": "限制序列端點的結果", + "description-series-limit": "限制適用於所有指標、標籤和數值。將欄位留空以使用預設的限制。設定為 0 以停用限制並擷取所有內容,不過這可能會導致效能問題。", + "label-select-metric": "一旦選取指標,只會顯示可能的標籤。標籤受以下序列限制。", + "select-a-metric": "1. 選擇指標", + "series-limit": "序列限制" }, "prom-cheat-sheet": { - "prom-ql-cheat-sheet": "" + "prom-ql-cheat-sheet": "PromQL 速查表" }, "prom-exemplar-field": { - "exemplars": "", - "tooltip-disable-query": "", - "tooltip-enable-query": "" + "exemplars": "範例", + "tooltip-disable-query": "停用範例查詢", + "tooltip-enable-query": "啟用範例查詢" }, "prom-explore-extra-field": { - "aria-label-prometheus-extra-field": "", - "aria-label-query-type-field": "", - "aria-label-step-field": "", - "min-step": "", - "query-type": "", - "tooltip-units-builtin-variables-example-interval-rateinterval": "" + "aria-label-prometheus-extra-field": "Prometheus 額外欄位", + "aria-label-query-type-field": "查詢類型欄位", + "aria-label-step-field": "步驟欄位", + "min-step": "最小步長", + "query-type": "查詢類型", + "tooltip-units-builtin-variables-example-interval-rateinterval": "此處可使用時間單位和內建變數,例如:{{example1}}、{{example2}}、{{example3}}、{{example4}}、{{example5}}、{{example6}}、{{example7}}(如未指定單位,則為預設值:{{default}})" }, "prom-query-field": { - "placeholder-enter-a-prom-ql-query": "" + "placeholder-enter-a-prom-ql-query": "輸入 PromQL 查詢…" }, "prom-variable-query-editor": { - "aria-label-classic-query": "", - "aria-label-metric-regex": "", - "aria-label-metric-selector": "", - "aria-label-prometheus-query": "", - "aria-label-query-type": "", - "aria-label-series-query": "", - "label-classic-query": "", + "aria-label-classic-query": "經典查詢", + "aria-label-metric-regex": "指標正規表達式", + "aria-label-metric-selector": "指標選擇器", + "aria-label-prometheus-query": "Prometheus 查詢", + "aria-label-query-type": "查詢類型", + "aria-label-series-query": "序列查詢", + "label-classic-query": "經典查詢", "label-label": "標籤", - "label-metric-regex": "", + "label-metric-regex": "指標正規表達式", "label-query": "查詢", - "label-query-type": "", - "label-series-query": "", - "placeholder-classic-query": "", - "placeholder-metric-regex": "", - "placeholder-prometheus-query": "", + "label-query-type": "查詢類型", + "label-series-query": "序列查詢", + "placeholder-classic-query": "經典查詢", + "placeholder-metric-regex": "指標正規表達式", + "placeholder-prometheus-query": "Prometheus 查詢", "placeholder-select-query-type": "選取查詢類型", - "placeholder-series-query": "", - "returns-metrics-matching-specified-metric-regex": "", - "tooltip-classic-query": "", - "tooltip-label": "", - "tooltip-metric-regex": "", - "tooltip-query": "", - "tooltip-query-type": "", - "tooltip-series-query": "" + "placeholder-series-query": "序列查詢", + "returns-metrics-matching-specified-metric-regex": "回傳與指定指標正規表達式相符的指標清單。", + "tooltip-classic-query": "Prometheus 變數查詢編輯器的原始實作。輸入一個字串,其中包含如這些文件中所述正確的查詢類型和參數。例如:{{exampleQuery}}。", + "tooltip-label": "除非有指定指標,否則會回傳所有指標中該標籤名稱的標籤值清單。", + "tooltip-metric-regex": "回傳標籤名稱清單,選擇性地依據指定指標正規表達式篩選。", + "tooltip-query": "回傳此查詢的 Prometheus 查詢結果清單。這可能包括 Prometheus 功能,即{{exampleQuery}}。", + "tooltip-query-type": "Prometheus 資料來源外掛程式為範本變數提供以下查詢類型。", + "tooltip-series-query": "輸入帶有標籤的指標、僅有指標或僅標籤,即 {{example1}}、{{example2}} 或 {{example3}}。回傳與輸入資料相關的時間序列清單。" }, "selector-actions": { - "aria-label-selector": "", - "aria-label-selector-clear-button": "", - "aria-label-use-selector-as-metrics-button": "", - "aria-label-use-selector-for-query-button": "", - "aria-label-validate-submit-button": "", + "aria-label-selector": "選擇器", + "aria-label-selector-clear-button": "選擇器清除按鈕", + "aria-label-use-selector-as-metrics-button": "使用選擇器作為指標按鈕", + "aria-label-use-selector-for-query-button": "使用選擇器作為查詢按鈕", + "aria-label-validate-submit-button": "驗證提交按鈕", "clear": "清除", - "resulting-selector": "", - "use-as-rate-query": "", - "use-query": "", - "validate-selector": "" + "resulting-selector": "4. 結果選擇器", + "use-as-rate-query": "作為速率查詢使用", + "use-query": "使用此查詢", + "validate-selector": "驗證選擇器" }, "value-selector": { - "aria-label-filter-expression-for-label-values": "", - "aria-label-values-for": "", - "description-search-field-values-across-selected-labels": "", - "select-multiple-values-for-your-labels": "" + "aria-label-filter-expression-for-label-values": "篩選標籤數值的表達式", + "aria-label-values-for": "以下項目的值:{{labelKey}}", + "description-search-field-values-across-selected-labels": "使用搜尋欄位在所選標籤中尋找數值。", + "select-multiple-values-for-your-labels": "3. 為您的標籤選取(多個)值" } }, "configuration": { "alerting-settings-overhaul": { - "label-allow-as-recording-rules-target": "", - "label-manage-alerts-via-alerting-ui": "", + "label-allow-as-recording-rules-target": "允許作為錄製規則目標", + "label-manage-alerts-via-alerting-ui": "透過警報 UI 管理警報", "title-alerting": "警報", - "tooltip-allow-as-recording-rules-target": "", + "tooltip-allow-as-recording-rules-target": "允許將此資料來源選為撰寫紀錄規則的目標。", "tooltip-manage-alerts-via-alerting-ui": "管理此資料來源的警報規則。若要管理其他警報資源,請新增 Alertmanager 資料來源。" }, "config-editor": { - "browser-access-mode-error": "", - "description-advanced-settings": "", + "browser-access-mode-error": "Prometheus 資料來源中的瀏覽器存取模式無法再使用。切換至伺服器存取模式。", + "description-advanced-settings": "附加設定為選擇性設定,可進行設定以便更妥善掌控您的資料來源。", "title-advanced-settings": "進階設定", "title-error": "錯誤" }, "data-source-http-settings-overhaul": { "tooltip-browser-access-mode": "您的存取方式是<1>瀏覽器,這表示需要從瀏覽器存取網址。", - "tooltip-http-url": "", + "tooltip-http-url": "指定完整的 HTTP URL(例如 {{exampleURL}})", "tooltip-server-access-mode": "您的存取方式是<1>伺服器,這表示需要從 Grafana 後端/伺服器存取網址。" }, "docs-tip": { - "visit-docs-for-more-details-here": "" + "visit-docs-for-more-details-here": "在此處瀏覽文件以了解更多詳細資訊。" }, "exemplar-setting": { "label-data-source": "資料來源", - "label-internal-link": "", + "label-internal-link": "內部連結", "label-label-name": "標籤名稱", - "label-remove-exemplar-link": "", + "label-remove-exemplar-link": "移除範例連結", "label-url": "網址", - "label-url-label": "", - "placeholder-go-to-examplecom": "", - "placeholder-httpsexamplecomvalueraw": "", - "placeholder-trace-id": "", - "title-remove-exemplar-link": "", - "tooltip-data-source": "", - "tooltip-internal-link": "", - "tooltip-label-name": "", - "tooltip-url": "", - "tooltip-url-label": "" + "label-url-label": "網址標籤", + "placeholder-go-to-examplecom": "前往 example.com", + "placeholder-httpsexamplecomvalueraw": "https://example.com/${__value.raw}", + "placeholder-trace-id": "traceID", + "title-remove-exemplar-link": "移除範例點連結", + "tooltip-data-source": "範例所導向的資料來源。", + "tooltip-internal-link": "若您有內部連結,請啟用此選項。啟用後將顯示資料來源選擇器。為您的範例資料選擇後端追蹤資料存放區。", + "tooltip-label-name": "標籤物件中應用來取得 traceID 的欄位名稱。", + "tooltip-url": "使用者將前往查看其追蹤的追蹤後端網址", + "tooltip-url-label": "用來覆寫範例 traceID 欄位上的按鈕標籤。" }, "exemplars-settings": { "add": "新增", - "no-exemplars-configurations": "", - "title-exemplars": "" + "no-exemplars-configurations": "沒有範例設定", + "title-exemplars": "範例點" }, "prom-settings": { - "aria-label-default-editor": "", - "aria-label-prom-type-type": "", - "aria-label-prometheus-type": "", - "aria-label-select-http-method": "", - "label-cache-level": "", - "label-custom-query-parameters": "", - "label-default-editor": "", - "label-disable-metrics-lookup": "", - "label-disable-recording-rules-beta": "", - "label-http-method": "", - "label-incremental-querying-beta": "", - "label-metric-names-suggestion-limit": "", - "label-prom-type-version": "", - "label-prometheus-type": "", - "label-query-overlap-window": "", - "label-query-timeout": "", - "label-scrape-interval": "", - "label-series-limit": "", - "label-use-series-endpoint": "", - "more-info": "", - "placeholder-example-maxsourceresolutionmtimeout": "", - "title-interval-behaviour": "", - "title-other": "", + "aria-label-default-editor": "預設編輯器(程式碼或編輯器)", + "aria-label-prom-type-type": "{{promType}} 類型", + "aria-label-prometheus-type": "Prometheus 類型", + "aria-label-select-http-method": "選取 HTTP 方法", + "editor-options": { + "label-builder": "", + "label-code": "" + }, + "label-cache-level": "快取層級", + "label-custom-query-parameters": "自訂查詢參數", + "label-default-editor": "預設編輯器", + "label-disable-metrics-lookup": "停用指標檢索", + "label-disable-recording-rules-beta": "停用錄製規則(測試版)", + "label-http-method": "HTTP 方法", + "label-incremental-querying-beta": "增量查詢(測試版)", + "label-metric-names-suggestion-limit": "指標名稱建議限制", + "label-prom-type-version": "{{promType}} 版本", + "label-prometheus-type": "Prometheus 類型", + "label-query-overlap-window": "查詢重疊視窗", + "label-query-timeout": "查詢逾時", + "label-scrape-interval": "抓取間隔", + "label-series-limit": "序列限制", + "label-use-series-endpoint": "使用序列端點", + "more-info": "如需更多在資料來源中設定 Prometheus 類型與版本的相關資訊,請參閱<2>佈建文件。", + "placeholder-example-maxsourceresolutionmtimeout": "範例:{{example}}", + "title-interval-behaviour": "間隔行為", + "title-other": "其他", "title-performance": "成效", - "title-query-editor": "", - "tooltip-cache-level": "", - "tooltip-custom-query-parameters": "", - "tooltip-default-editor": "", - "tooltip-disable-metrics-lookup": "", - "tooltip-disable-recording-rules-beta": "", - "tooltip-http-method": "", - "tooltip-incremental-querying-beta": "", - "tooltip-metric-names-suggestion-limit": "", - "tooltip-prom-type-version": "", - "tooltip-prometheus-type": "", - "tooltip-query-overlap-window": "", - "tooltip-query-timeout": "", - "tooltip-scrape-interval": "", - "tooltip-series-limit": "", - "tooltip-use-series-endpoint": "" + "title-query-editor": "查詢編輯器", + "tooltip-cache-level": "設定編輯器查詢的瀏覽器快取層級。針對高基數資料來源,建議使用較高層級的快取設定。", + "tooltip-custom-query-parameters": "將自訂參數新增至 Prometheus 查詢網址。例如 {{example1}}、{{example2}}、{{example3}} 或 {{example4}}。多個參數應與{{concatenationChar}}串接在一起。", + "tooltip-default-editor": "為此資料來源的所有使用者設定預設編輯器選項。", + "tooltip-disable-metrics-lookup": "勾選此選項將停用查詢欄位自動完成中的指標選擇器和指標/標籤支援。如果您在較大的 Prometheus 實例上遇到效能問題,這會有所幫助。", + "tooltip-disable-recording-rules-beta": "此功能將停用錄製規則。開啟此功能以改善儀表板效能", + "tooltip-http-method": "您可以使用 POST 或 GET HTTP 方法,來查詢您的 Prometheus 資料來源。POST 是建議的方法,因為它可以處理更大規模的查詢。如果您的 Prometheus 版本早於 2.1,或您的網路中限制了 POST 請求,請將此更改為 GET。", + "tooltip-incremental-querying-beta": "此功能將變更相對時間查詢的預設行為,以便隨時從 Prometheus 實例要求新資料,而不會快取查詢結果,並且只會請求新紀錄。開啟此功能以減少資料庫和網路負載。", + "tooltip-metric-names-suggestion-limit": "在查詢編輯器的程式碼模式中,可能會顯示為自動完成建議的指標名稱之最大數量。", + "tooltip-prom-type-version": "如果您的{{promType}}實例未自動設定版本,請使用此功能進行設定。", + "tooltip-prometheus-type": "將此設定為 Prometheus 資料庫的類型,例如 Prometheus、Cortex、Mimir 或 Thanos。變更此欄位將儲存您目前的設定。特定類型的 Prometheus 支援或無法支援各種 API。例如,某些類型支援對標籤查詢使用正規表達式匹配,以提高效能。部分類型具有元資料的 API。若您設定錯誤,在查詢指標和標籤時可能會遇到異常行為。請檢查您的 Prometheus 文件,以確保您輸入正確的類型。", + "tooltip-query-overlap-window": "設定持續時間,例如 {{example1}} 或 {{example2}} 或 {{example3}}。預設為{{default}}。將新增此持續時間至每個增量請求的持續時間。", + "tooltip-query-timeout": "設定 Prometheus 查詢逾時。", + "tooltip-scrape-interval": "此間隔是 Prometheus 抓取目標的頻率。將此設定為 Prometheus 設定檔案中設定的典型抓取和評估間隔。若您將此設定為大於 Prometheus 設定檔案間隔的數值,Grafana 將根據此間隔評估資料,您將看到較少的資料點。預設為{{default}}。", + "tooltip-series-limit": "限制適用於兩個端點(系列和標籤)的所有資源(指標、標籤和值)。將欄位留空以使用預設的限制 (40000)。設定為 0 以停用限制並擷取所有內容,不過這可能會導致效能問題。預設限制為 40000。", + "tooltip-use-series-endpoint": "勾選此選項將優先使用具有 {{exampleParameter}} 參數的序列端點,而非具有 {{exampleParameter}} 參數的標籤值端點。雖然標籤值端點被視為擁有更佳效能,但有些使用者可能會更喜歡該序列,因為它具有 POST 方法,而標籤值端點僅具有 GET 方法。" + } + }, + "prom-query-legend-editor": { + "get-legend-mode-options": { + "description-auto": "", + "description-custom": "", + "description-verbose": "", + "label-auto": "", + "label-custom": "", + "label-verbose": "" } }, "querybuilder": { "additional-settings": { - "content-filter-metric-names-regex-search-using": "", - "disable-text-wrap": "" + "content-filter-metric-names-regex-search-using": "透過正規表達式搜尋篩選指標名稱,並使用額外的 Prometheus API 呼叫。", + "disable-text-wrap": "停用文字換行" }, "feedback-link": { "give-feedback": "提供意見回饋", - "title-give-feedback": "" + "title-give-feedback": "指標瀏覽器為新功能,請告訴我們如何改善" + }, + "get-collapsed-info": { + "exemplars": "", + "format": "", + "legend": "", + "step": "", + "type": "" + }, + "get-placeholders": { + "browse": "", + "include-null-metadata": "", + "metadata-search-switch": "", + "set-use-backend": "", + "type": "" + }, + "get-prom-types": { + "description-counter": "", + "description-gauge": "", + "description-histogram": "", + "description-native-histogram": "", + "description-no-type": "", + "description-summary": "", + "description-unknown": "", + "label-counter": "", + "label-gauge": "", + "label-histogram": "", + "label-native-histogram": "", + "label-no-type": "", + "label-summary": "", + "label-unknown": "" }, "handle-function": { "text": { - "query-parsing-is-ambiguous": "" + "query-parsing-is-ambiguous": "查詢剖析不明確。" } }, "label-filter-item": { - "aria-label-remove": "", - "placeholder-select-label": "", + "aria-label-remove": "移除{{name}}", + "placeholder-select-label": "選擇標籤", "placeholder-select-value": "選擇值" }, "label-filters": { - "label-filters": "", - "label-label-filters": "", - "tooltip-label-filters": "" + "label-filters": "標籤篩選條件", + "label-label-filters": "標籤篩選條件", + "tooltip-label-filters": "選擇性設定:用於篩選為此查詢類型的指標選擇。" + }, + "label-param-editor": { + "loadingMessage-loading-labels": "", + "noOptionsMessage-no-labels-found": "" }, "metric-combobox": { "async-select": { - "aria-label-open-metrics-explorer": "", - "placeholder-select-metric": "", - "tooltip-open-metrics-explorer": "" + "aria-label-open-metrics-explorer": "開啟指標瀏覽器", + "placeholder-select-metric": "選擇指標", + "tooltip-open-metrics-explorer": "開啟計量瀏覽器" }, "label-metric": "指標", - "tooltip-metric": "" + "tooltip-metric": "選擇性設定:回傳指定指標中該標籤名稱的標籤值清單。" }, "metrics-modal": { - "additional-settings": "", + "additional-settings": "附加設定", "aria-label-additional-settings": "附加設定", - "aria-label-browse-metrics": "", - "currently-selected": "", - "metrics-pre-filtered": "", - "placeholder-results-per-page": "", - "results-per-page": "", - "title-metrics-explorer": "", - "results-amount_other": "" + "aria-label-browse-metrics": "瀏覽指標", + "currently-selected": "目前已選取:{{selected}}", + "metrics-pre-filtered": "這些指標已透過標籤篩選條件中選擇的標籤進行預先篩選。", + "placeholder-results-per-page": "每頁顯示結果", + "results-per-page": "每頁顯示的結果", + "title-metrics-explorer": "指標瀏覽器", + "results-amount_other": "結果共 {{count}} 項,顯示 {{num}} 項" }, "nested-query": { "label": { - "ignoring": "", + "ignoring": "忽略", "on": "開啟" }, "operator": "運算子", - "tooltip-remove-match": "", - "vector-matches": "" + "tooltip-remove-match": "移除比對", + "vector-matches": "向量比對" }, "operation-editor": { - "not-found": "", - "title-remove": "" + "not-found": "找不到{{id}}操作", + "title-remove": "移除{{name}}" }, "operation-header": { "placeholder-replace-with": "取代為", - "title-click-to-view-alternative-operations": "", - "title-remove-operation": "" + "title-click-to-view-alternative-operations": "按一下以檢視替代操作", + "title-remove-operation": "移除操作" }, "operation-info-button": { - "title-click-to-show-description": "", - "title-remove-operation": "" + "title-click-to-show-description": "按一下以顯示說明", + "title-remove-operation": "移除操作" }, "operation-list": { - "operations": "", + "operations": "操作", "placeholder-search": "搜尋", - "title-add-operation": "" + "title-add-operation": "新增操作" }, "operation-param-editor": { - "title-add": "", - "title-remove": "" + "title-add": "新增{{name}}", + "title-remove": "移除{{name}}" }, "prom-query-builder-options": { - "aria-label-lower-limit-parameter": "", - "aria-label-select-resolution": "", - "label-exemplars": "", + "aria-label-lower-limit-parameter": "設定步驟參數的下限", + "aria-label-select-resolution": "選擇解決方案", + "format-options": { + "label-heatmap": "", + "label-table": "", + "label-time-series": "" + }, + "label-exemplars": "範例點", "label-format": "格式", - "label-min-step": "", - "label-resolution": "", + "label-min-step": "最小步長", + "label-resolution": "解析度", "label-type": "類型", - "placeholder-auto": "", + "placeholder-auto": "自動", "title-options": "選項", - "tooltip-min-step": "" + "tooltip-min-step": "Prometheus 查詢的步驟參數以及 <2>{{interval}} 和 <4>{{rateInterval}} 變數的其他下限。" }, "prom-query-code-editor-autocomplete-info": { - "autocomplete-suggestions-limited": "", - "tooltip-autocomplete-suggestions-limited": "" + "autocomplete-suggestions-limited": "自動完成建議有所限制", + "tooltip-autocomplete-suggestions-limited": "指標名稱的數量超過自動完成限制。僅顯示與{{autocompleteLimit}}最相關的指標。您可以在資料來源設定中調整臨界值。" }, "prom-query-editor-selector": { + "body-syntax-error": "", + "confirmText-continue": "", "kick-start-your-query": "啟動您的查詢", - "label-explain": "", - "run-queries": "", - "title-parsing-error-switch-builder": "" + "label-explain": "說明", + "run-queries": "執行查詢", + "title-parsing-error-switch-builder": "剖析錯誤:要切換至編輯器模式嗎?" }, "prom-query-legend-editor": { - "label-legend": "", - "placeholder-select-legend-mode": "", - "tooltip-legend": "" + "label-legend": "圖例", + "placeholder-select-legend-mode": "選擇圖例模式", + "tooltip-legend": "序列名稱覆寫或範本。例如:{{templateExample}} 將替換為{{labelName}}的標籤值。" }, "query-builder-hints": { - "hint-details": "" + "hint-details": "提示:{{hintDetails}}" }, "query-editor-hints": { - "hint-details": "" + "hint-details": "提示: {{hintDetails}}" + }, + "query-editor-mode-toggle": { + "editor-modes": { + "label-builder": "", + "label-code": "" + } }, "query-pattern": { - "apply-query": "", - "aria-label-apply-query-starter-button": "", - "aria-label-back-button": "", - "aria-label-create-new-query-button": "", - "aria-label-raw-query": "", - "aria-label-use-this-query-button": "", + "apply-query": "套用查詢", + "aria-label-apply-query-starter-button": "套用查詢啟動按鈕", + "aria-label-back-button": "返回鍵", + "aria-label-create-new-query-button": "建立新查詢按鈕", + "aria-label-raw-query": "{{patternName}} 原始查詢", + "aria-label-use-this-query-button": "使用此查詢按鈕", "back": "返回", - "create-new-query": "", + "create-new-query": "建立新查詢", "use-this-query": "使用此查詢" }, "query-patterns-modal": { - "aria-label-close-kick-start-your-query-modal": "", + "aria-label-close-kick-start-your-query-modal": "關閉啟動您的查詢模式", "aria-label-kick-start-your-query-modal": "啟動您的查詢模式", - "aria-label-toggle-query-starter": "", + "aria-label-toggle-query-starter": "開啟和關閉{{patternType}}查詢啟動卡", "close": "關閉", - "description-kick-start-your-query": "", - "label-toggle-query-starter": "", + "description-kick-start-your-query": "選擇其中一項查詢來啟動您的查詢。您可以接著繼續完成查詢。", + "label-toggle-query-starter": "{{patternType}} 查詢啟動器", "title-kick-start-your-query": "啟動您的查詢" }, "raw-query": { - "aria-label-selector": "" + "aria-label-selector": "選擇器" }, "results-table": { - "content-descriptive-type": "", + "content-descriptive-type": "建立{{descriptiveType}}時,Prometheus 會透過類型計數器顯示多個序列。", "description": "說明", + "message-expand-label-filters": "", + "message-expand-search": "", + "message-no-metrics-found": "", "name": "名稱(名字)", "select": "選取", "type": "類型" }, "update-function-args": { "text": { - "query-parsing-is-ambiguous": "" + "query-parsing-is-ambiguous": "查詢剖析不明確。" } } } diff --git a/packages/grafana-prometheus/src/promql.test.ts b/packages/grafana-prometheus/src/promql.test.ts index 8ce99e175fc..219d5eac3b8 100644 --- a/packages/grafana-prometheus/src/promql.test.ts +++ b/packages/grafana-prometheus/src/promql.test.ts @@ -1,23 +1,23 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/promql.test.ts import Prism from 'prismjs'; -import promql from './promql'; +import { promqlGrammar } from './promql'; describe('Loki syntax', () => { it('should highlight Loki query correctly', () => { - expect(Prism.highlight('{key="val#ue"}', promql, 'promql')).toBe( + expect(Prism.highlight('{key="val#ue"}', promqlGrammar, 'promql')).toBe( '{key="val#ue"}' ); - expect(Prism.highlight('{key="#value"}', promql, 'promql')).toBe( + expect(Prism.highlight('{key="#value"}', promqlGrammar, 'promql')).toBe( '{key="#value"}' ); - expect(Prism.highlight('{key="value#"}', promql, 'promql')).toBe( + expect(Prism.highlight('{key="value#"}', promqlGrammar, 'promql')).toBe( '{key="value#"}' ); - expect(Prism.highlight('#test{key="value"}', promql, 'promql')).toBe( + expect(Prism.highlight('#test{key="value"}', promqlGrammar, 'promql')).toBe( '#test{key="value"}' ); - expect(Prism.highlight('{key="value"}#test', promql, 'promql')).toBe( + expect(Prism.highlight('{key="value"}#test', promqlGrammar, 'promql')).toBe( '{key="value"}#test' ); }); diff --git a/packages/grafana-prometheus/src/promql.ts b/packages/grafana-prometheus/src/promql.ts index 912a009eb64..9fd1e4add62 100644 --- a/packages/grafana-prometheus/src/promql.ts +++ b/packages/grafana-prometheus/src/promql.ts @@ -656,5 +656,3 @@ export const promqlGrammar: Grammar = { operator: new RegExp(`/[-+*/=%^~]|&&?|\\|?\\||!=?|<(?:=>?|<|>)?|>[>=]?|\\b(?:${OPERATORS.join('|')})\\b`, 'i'), punctuation: /[{};()`,.]/, }; - -export default promqlGrammar; diff --git a/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx b/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx index 30370d068a3..b61f64ec58d 100644 --- a/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx +++ b/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; import { Button, Card, useStyles2 } from '@grafana/ui'; -import promqlGrammar from '../promql'; +import { promqlGrammar } from '../promql'; import { RawQuery } from './shared/RawQuery'; import { promQueryModeller } from './shared/modeller_instance'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx index 0ffa50ce3e1..d8d76ae21b3 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelFilterItem.tsx @@ -11,7 +11,7 @@ import { AsyncSelect, Select } from '@grafana/ui'; import { truncateResult } from '../../language_utils'; import { QueryBuilderLabelFilter } from '../shared/types'; -export interface LabelFilterItemProps { +interface LabelFilterItemProps { defaultOp: string; item: Partial; onChange: (value: QueryBuilderLabelFilter) => void; diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx index ed978167f12..0fc93905f4d 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { DataSourceApi, SelectableValue, TimeRange, toOption } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; import { getOperationParamId } from '../shared/param_utils'; @@ -12,7 +13,7 @@ import { PromVisualQuery, PromQueryModellerInterface } from '../types'; * Props for the LabelParamEditor component. * This editor specifically requires a Prometheus query modeller instance. */ -export interface LabelParamEditorProps extends Omit { +interface LabelParamEditorProps extends Omit { queryModeller: PromQueryModellerInterface; } @@ -47,8 +48,14 @@ export function LabelParamEditor({ }} isLoading={state.isLoading} allowCustomValue - noOptionsMessage="No labels found" - loadingMessage="Loading labels" + noOptionsMessage={t( + 'grafana-prometheus.querybuilder.label-param-editor.noOptionsMessage-no-labels-found', + 'No labels found' + )} + loadingMessage={t( + 'grafana-prometheus.querybuilder.label-param-editor.loadingMessage-loading-labels', + 'Loading labels' + )} options={state.options} value={toOption(value as string)} onChange={(value) => onChange(index, value.value!)} diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx index a19ee523c45..321cf89a792 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx @@ -15,7 +15,7 @@ import { PromVisualQuery } from '../types'; import { LabelFilters } from './LabelFilters'; import { MetricCombobox } from './MetricCombobox'; -export interface MetricsLabelsSectionProps { +interface MetricsLabelsSectionProps { query: PromVisualQuery; datasource: PrometheusDatasource; onChange: (update: PromVisualQuery) => void; @@ -194,7 +194,7 @@ async function getMetrics( })); } -export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined { +function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined { if (!metadata[metric]) { return; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx b/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx index 5d9569fa5c9..8ac17b1b854 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx @@ -13,7 +13,7 @@ import { PromVisualQueryBinary } from '../types'; import { QueryBuilderContent } from './shared/QueryBuilderContent'; -export interface NestedQueryProps { +interface NestedQueryProps { nestedQuery: PromVisualQueryBinary; datasource: PrometheusDatasource; index: number; diff --git a/packages/grafana-prometheus/src/querybuilder/components/NestedQueryList.tsx b/packages/grafana-prometheus/src/querybuilder/components/NestedQueryList.tsx index f0aca4ae1fd..ba0d62d5f2a 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/NestedQueryList.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/NestedQueryList.tsx @@ -6,7 +6,7 @@ import { PromVisualQuery, PromVisualQueryBinary } from '../types'; import { NestedQuery } from './NestedQuery'; -export interface NestedQueryListProps { +interface NestedQueryListProps { query: PromVisualQuery; datasource: PrometheusDatasource; onChange: (query: PromVisualQuery) => void; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx index befa0b530d3..03bf97718bc 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx @@ -8,7 +8,7 @@ import { PromVisualQuery } from '../types'; import { BaseQueryBuilder } from './shared/BaseQueryBuilder'; -export interface PromQueryBuilderProps { +interface PromQueryBuilderProps { query: PromVisualQuery; datasource: PrometheusDatasource; onChange: (update: PromVisualQuery) => void; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx index f2cd9d1f798..318d4a71213 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx @@ -14,7 +14,7 @@ import { PromQueryBuilder } from './PromQueryBuilder'; import { QueryPreview } from './QueryPreview'; import { getSettings, MetricsModalSettings } from './metrics-modal/state/state'; -export interface PromQueryBuilderContainerProps { +interface PromQueryBuilderContainerProps { query: PromQuery; datasource: PrometheusDatasource; onChange: (update: PromQuery) => void; @@ -23,7 +23,7 @@ export interface PromQueryBuilderContainerProps { showExplain: boolean; } -export interface State { +interface State { visQuery?: PromVisualQuery; expr: string; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx index 2b8dac3329e..a89a2a25c83 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx @@ -3,7 +3,7 @@ import { memo } from 'react'; import { Stack } from '@grafana/ui'; -import promqlGrammar from '../../promql'; +import { promqlGrammar } from '../../promql'; import { buildVisualQueryFromString } from '../parsing'; import { OperationExplainedBox } from '../shared/OperationExplainedBox'; import { OperationListExplained } from '../shared/OperationListExplained'; @@ -13,7 +13,7 @@ import { PromVisualQuery } from '../types'; export const EXPLAIN_LABEL_FILTER_CONTENT = 'Fetch all series matching metric name and label filters.'; -export interface PromQueryBuilderExplainedProps { +interface PromQueryBuilderExplainedProps { query: string; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx index 4be75ddf2e8..8d1d76351c1 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx @@ -16,28 +16,13 @@ import { QueryOptionGroup } from '../shared/QueryOptionGroup'; import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor'; -export interface UIOptions { - exemplars: boolean; - type: boolean; - format: boolean; - minStep: boolean; - legend: boolean; - resolution: boolean; -} - -export interface PromQueryBuilderOptionsProps { +interface PromQueryBuilderOptionsProps { query: PromQuery; app?: CoreApp; onChange: (update: PromQuery) => void; onRunQuery: () => void; } -const FORMAT_OPTIONS: Array> = [ - { label: 'Time series', value: 'time_series' }, - { label: 'Table', value: 'table' }, - { label: 'Heatmap', value: 'heatmap' }, -]; - const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2, 3, 4, 5, 10], (value: number) => ({ value, label: '1/' + value, @@ -45,6 +30,24 @@ const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2, 3, 4, export const PromQueryBuilderOptions = React.memo( ({ query, app, onChange, onRunQuery }) => { + const FORMAT_OPTIONS: Array> = [ + { + label: t( + 'grafana-prometheus.querybuilder.prom-query-builder-options.format-options.label-time-series', + 'Time series' + ), + value: 'time_series', + }, + { + label: t('grafana-prometheus.querybuilder.prom-query-builder-options.format-options.label-table', 'Table'), + value: 'table', + }, + { + label: t('grafana-prometheus.querybuilder.prom-query-builder-options.format-options.label-heatmap', 'Heatmap'), + value: 'heatmap', + }, + ]; + const onChangeFormat = (value: SelectableValue) => { onChange({ ...query, format: value.value }); onRunQuery(); @@ -182,17 +185,25 @@ function getQueryTypeValue(query: PromQuery) { function getCollapsedInfo(query: PromQuery, formatOption: string, queryType: string, app?: CoreApp): string[] { const items: string[] = []; - items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`); - items.push(`Format: ${formatOption}`); - items.push(`Step: ${query.interval ?? 'auto'}`); - items.push(`Type: ${queryType}`); + items.push( + t('grafana-prometheus.querybuilder.get-collapsed-info.legend', 'Legend: {{value}}', { + value: getLegendModeLabel(query.legendFormat), + }) + ); + items.push( + t('grafana-prometheus.querybuilder.get-collapsed-info.format', 'Format: {{value}}', { value: formatOption }) + ); + items.push( + t('grafana-prometheus.querybuilder.get-collapsed-info.step', 'Step: {{value}}', { value: query.interval ?? 'auto' }) + ); + items.push(t('grafana-prometheus.querybuilder.get-collapsed-info.type', 'Type: {{value}}', { value: queryType })); if (shouldShowExemplarSwitch(query, app)) { - if (query.exemplar) { - items.push(`Exemplars: true`); - } else { - items.push(`Exemplars: false`); - } + items.push( + t('grafana-prometheus.querybuilder.get-collapsed-info.exemplars', 'Exemplars: {{value}}', { + value: query.exemplar ? 'true' : 'false', + }) + ); } return items; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx index 1d500669f5b..adf3afebf28 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx @@ -98,8 +98,11 @@ export const PromQueryEditorSelector = memo((props) => { 'grafana-prometheus.querybuilder.prom-query-editor-selector.title-parsing-error-switch-builder', 'Parsing error: Switch to the builder mode?' )} - body="There is a syntax error, or the query structure cannot be visualized when switching to the builder mode. Parts of the query may be lost. " - confirmText="Continue" + body={t( + 'grafana-prometheus.querybuilder.prom-query-editor-selector.body-syntax-error', + 'There is a syntax error, or the query structure cannot be visualized when switching to the builder mode. Parts of the query may be lost.' + )} + confirmText={t('grafana-prometheus.querybuilder.prom-query-editor-selector.confirmText-continue', 'Continue')} onConfirm={() => { changeEditorMode(query, QueryEditorMode.Builder, onChange); setParseModalOpen(false); diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryLegendEditor.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryLegendEditor.tsx index a793b2df79b..2d9cf038bc2 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryLegendEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryLegendEditor.tsx @@ -10,20 +10,37 @@ import { AutoSizeInput, Select } from '@grafana/ui'; import { LegendFormatMode } from '../../types'; -export interface PromQueryLegendEditorProps { +interface PromQueryLegendEditorProps { legendFormat: string | undefined; onChange: (legendFormat: string) => void; onRunQuery: () => void; } -const legendModeOptions = [ +const getLegendModeOptions = () => [ { - label: 'Auto', + label: t('grafana-prometheus.prom-query-legend-editor.get-legend-mode-options.label-auto', 'Auto'), value: LegendFormatMode.Auto, - description: 'Only includes unique labels', + description: t( + 'grafana-prometheus.prom-query-legend-editor.get-legend-mode-options.description-auto', + 'Only includes unique labels' + ), + }, + { + label: t('grafana-prometheus.prom-query-legend-editor.get-legend-mode-options.label-verbose', 'Verbose'), + value: LegendFormatMode.Verbose, + description: t( + 'grafana-prometheus.prom-query-legend-editor.get-legend-mode-options.description-verbose', + 'All label names and values' + ), + }, + { + label: t('grafana-prometheus.prom-query-legend-editor.get-legend-mode-options.label-custom', 'Custom'), + value: LegendFormatMode.Custom, + description: t( + 'grafana-prometheus.prom-query-legend-editor.get-legend-mode-options.description-custom', + 'Provide a naming template' + ), }, - { label: 'Verbose', value: LegendFormatMode.Verbose, description: 'All label names and values' }, - { label: 'Custom', value: LegendFormatMode.Custom, description: 'Provide a naming template' }, ]; /** @@ -33,6 +50,7 @@ export const PromQueryLegendEditor = React.memo( ({ legendFormat, onChange, onRunQuery }) => { const mode = getLegendMode(legendFormat); const inputRef = useRef(null); + const legendModeOptions = getLegendModeOptions(); const onLegendFormatChanged = (evt: React.FormEvent) => { let newFormat = evt.currentTarget.value; @@ -124,6 +142,7 @@ function getLegendMode(legendFormat: string | undefined) { } export function getLegendModeLabel(legendFormat: string | undefined) { + const legendModeOptions = getLegendModeOptions(); const mode = getLegendMode(legendFormat); if (mode !== LegendFormatMode.Custom) { return legendModeOptions.find((x) => x.value === mode)?.label; diff --git a/packages/grafana-prometheus/src/querybuilder/components/QueryPreview.tsx b/packages/grafana-prometheus/src/querybuilder/components/QueryPreview.tsx index 613a7111c7d..1c9d9fa0151 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/QueryPreview.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/QueryPreview.tsx @@ -1,10 +1,10 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx import { EditorFieldGroup, EditorRow } from '@grafana/plugin-ui'; -import promqlGrammar from '../../promql'; +import { promqlGrammar } from '../../promql'; import { RawQuery } from '../shared/RawQuery'; -export interface QueryPreviewProps { +interface QueryPreviewProps { query: string; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx index a60a070b5b4..4ce04117dd9 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx @@ -7,7 +7,7 @@ import { Icon, Switch, Tooltip, useTheme2 } from '@grafana/ui'; import { metricsModaltestIds } from './shared/testIds'; import { AdditionalSettingsProps } from './shared/types'; -import { placeholders } from './state/helpers'; +import { getPlaceholders } from './state/helpers'; export function AdditionalSettings(props: AdditionalSettingsProps) { const { state, onChangeFullMetaSearch, onChangeIncludeNullMetadata, onChangeDisableTextWrap, onChangeUseBackend } = @@ -16,6 +16,8 @@ export function AdditionalSettings(props: AdditionalSettingsProps) { const theme = useTheme2(); const styles = getStyles(theme); + const placeholders = getPlaceholders(); + return ( <>
diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/FeedbackLink.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/FeedbackLink.tsx index de5ec8a57d1..e62e4ce73e8 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/FeedbackLink.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/FeedbackLink.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Icon, useStyles2, Stack } from '@grafana/ui'; -export interface Props { +interface Props { feedbackUrl?: string; } diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx index 3d2c500e795..f38935ea4ea 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx @@ -32,16 +32,30 @@ import { calculatePageList, calculateResultsPerPage, displayedMetrics, - placeholders, - promTypes, + getPlaceholders, + getPromTypes, setMetrics, tracking, } from './state/helpers'; import { + buildMetrics, DEFAULT_RESULTS_PER_PAGE, + filterMetricsBackend, initialState, MAXIMUM_RESULTS_PER_PAGE, MetricsModalMetadata, + setDisableTextWrap, + setFullMetaSearch, + setFuzzySearchQuery, + setIncludeNullMetadata, + setIsLoading, + setMetaHaystack, + setNameHaystack, + setPageNum, + setResultsPerPage, + setSelectedTypes, + setUseBackend, + showAdditionalSettings, stateSlice, } from './state/state'; import { getStyles } from './styles'; @@ -55,6 +69,8 @@ export const MetricsModal = (props: MetricsModalProps) => { const theme = useTheme2(); const styles = getStyles(theme, state.disableTextWrap); + const placeholders = getPlaceholders(); + const promTypes = getPromTypes(); /** * loads metrics and metadata on opening modal and switching off useBackend @@ -88,7 +104,7 @@ export const MetricsModal = (props: MetricsModalProps) => { const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => { return { value: t.value, - label: t.value, + label: t.label, description: t.description, }; }); @@ -352,21 +368,3 @@ export const metricsModaltestIds = { setUseBackend: 'set-use-backend', showAdditionalSettings: 'show-additional-settings', }; -// actions to update the state -export const { - setIsLoading, - buildMetrics, - filterMetricsBackend, - setResultsPerPage, - setPageNum, - setFuzzySearchQuery, - setNameHaystack, - setMetaHaystack, - setFullMetaSearch, - setIncludeNullMetadata, - setSelectedTypes, - setUseBackend, - setDisableTextWrap, - showAdditionalSettings, - setFilteredMetricCount, -} = stateSlice.actions; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx index 47c990c091f..1e179d336ee 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx @@ -4,7 +4,7 @@ import { ReactElement } from 'react'; import Highlighter from 'react-highlight-words'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui'; import { docsTip } from '../../../configuration/shared/utils'; @@ -106,15 +106,24 @@ export function ResultsTable(props: ResultsTableProps) { let message; if (!state.fuzzySearchQuery) { - message = 'There are no metrics found in the data source.'; + message = t( + 'grafana-prometheus.querybuilder.results-table.message-no-metrics-found', + 'There are no metrics found in the data source.' + ); } if (query.labels.length > 0) { - message = 'There are no metrics found. Try to expand your label filters.'; + message = t( + 'grafana-prometheus.querybuilder.results-table.message-expand-label-filters', + 'There are no metrics found. Try to expand your label filters.' + ); } if (state.fuzzySearchQuery || state.selectedTypes.length > 0) { - message = 'There are no metrics found. Try to expand your search and filters.'; + message = t( + 'grafana-prometheus.querybuilder.results-table.message-expand-search', + 'There are no metrics found. Try to expand your search and filters.' + ); } return ( diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts index 255131670a7..76ee015c1e9 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts @@ -3,7 +3,7 @@ import { TimeRange } from '@grafana/data'; import { PrometheusDatasource } from '../../../../datasource'; import { PromVisualQuery } from '../../../types'; -export interface MetricsModalState { +interface MetricsModalState { useBackend: boolean; disableTextWrap: boolean; includeNullMetadata: boolean; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts index 66fa83e2624..79d41287197 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts @@ -1,16 +1,16 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts import { AnyAction } from '@reduxjs/toolkit'; +import { t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { PrometheusDatasource } from '../../../../datasource'; import { PromMetricsMetadata } from '../../../../types'; -import { regexifyLabelValuesQueryString } from '../../../parsingUtils'; -import { QueryBuilderLabelFilter } from '../../../shared/types'; import { PromVisualQuery } from '../../../types'; import { HaystackDictionary, MetricData, MetricsData, PromFilterOption } from '../types'; import { MetricsModalMetadata, MetricsModalState, setFilteredMetricCount } from './state'; + export async function setMetrics( datasource: PrometheusDatasource, query: PromVisualQuery, @@ -85,11 +85,11 @@ function buildMetricData(metric: string, datasource: PrometheusDatasource): Metr return metricData; } -export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined { +function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined { return metadata[metric]?.help; } -export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined { +function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined { return metadata[metric]?.type; } @@ -109,7 +109,7 @@ export function displayedMetrics(state: MetricsModalState, dispatch: React.Dispa /** * Filter the metrics with all the options, fuzzy, type, null metadata */ -export function filterMetrics(state: MetricsModalState): MetricsData { +function filterMetrics(state: MetricsModalState): MetricsData { let filteredMetrics: MetricsData = state.metrics; if (state.fuzzySearchQuery && !state.useBackend) { @@ -161,7 +161,7 @@ export function calculatePageList(state: MetricsModalState) { return [...Array(pages).keys()].map((i) => i + 1); } -export function sliceMetrics(metrics: MetricsData, pageNum: number, resultsPerPage: number) { +function sliceMetrics(metrics: MetricsData, pageNum: number, resultsPerPage: number) { const calcResultsPerPage: number = resultsPerPage === 0 ? 1 : resultsPerPage; const start: number = pageNum === 1 ? 0 : (pageNum - 1) * calcResultsPerPage; const end: number = start + calcResultsPerPage; @@ -180,34 +180,6 @@ export const calculateResultsPerPage = (results: number, defaultResults: number, return results ?? defaultResults; }; -/** - * The backend query that replaces the uFuzzy search when the option 'useBackend' has been selected - * this is a regex search either to the series or labels Prometheus endpoint - * depending on which the Prometheus type or version supports - * @param metricText - * @param labels - * @param datasource - */ -export async function getBackendSearchMetrics( - metricText: string, - labels: QueryBuilderLabelFilter[], - datasource: PrometheusDatasource -): Promise> { - const queryString = regexifyLabelValuesQueryString(metricText); - - const labelsParams = labels.map((label) => { - return `,${label.label}="${label.value}"`; - }); - - const params = `label_values({__name__=~".*${queryString}"${labels ? labelsParams.join() : ''}},__name__)`; - - const results = datasource.metricFindQuery(params); - - return await results.then((results) => { - return results.map((result) => buildMetricData(result.text, datasource)); - }); -} - export function tracking(event: string, state?: MetricsModalState | null, metric?: string, query?: PromVisualQuery) { switch (event) { case 'grafana_prom_metric_encycopedia_tracking': @@ -232,45 +204,75 @@ export function tracking(event: string, state?: MetricsModalState | null, metric } } -export const promTypes: PromFilterOption[] = [ +export const getPromTypes: () => PromFilterOption[] = () => [ { value: 'counter', - description: - 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-counter', 'Counter'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-counter', + 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.' + ), }, { value: 'gauge', - description: 'A metric that represents a single numerical value that can arbitrarily go up and down.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-gauge', 'Gauge'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-gauge', + 'A metric that represents a single numerical value that can arbitrarily go up and down.' + ), }, { value: 'histogram', - description: - 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-histogram', 'Histogram'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-histogram', + 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.' + ), }, { value: 'native histogram', - description: - 'Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-native-histogram', 'Native histogram'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-native-histogram', + 'Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.' + ), }, { value: 'summary', - description: - 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-summary', 'Summary'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-summary', + 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.' + ), }, { value: 'unknown', - description: 'These metrics have been given the type unknown in the metadata.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-unknown', 'Unknown'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-unknown', + 'These metrics have been given the type unknown in the metadata.' + ), }, { value: 'no type', - description: 'These metrics have no defined type in the metadata.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-no-type', 'No type'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-no-type', + 'These metrics have no defined type in the metadata.' + ), }, ]; -export const placeholders = { - browse: 'Search metrics by name', - metadataSearchSwitch: 'Include description in search', - type: 'Filter by type', - includeNullMetadata: 'Include results with no metadata', - setUseBackend: 'Enable regex search', -}; +export const getPlaceholders = () => ({ + browse: t('grafana-prometheus.querybuilder.get-placeholders.browse', 'Search metrics by name'), + metadataSearchSwitch: t( + 'grafana-prometheus.querybuilder.get-placeholders.metadata-search-switch', + 'Include description in search' + ), + type: t('grafana-prometheus.querybuilder.get-placeholders.type', 'Filter by type'), + includeNullMetadata: t( + 'grafana-prometheus.querybuilder.get-placeholders.include-null-metadata', + 'Include results with no metadata' + ), + setUseBackend: t('grafana-prometheus.querybuilder.get-placeholders.set-use-backend', 'Enable regex search'), +}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts index 233f3765e8a..2bdb85a276b 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts @@ -9,23 +9,10 @@ export type MetricData = { export type PromFilterOption = { value: string; + label: string; description: string; }; export interface HaystackDictionary { [needle: string]: MetricData; } - -export type UFuzzyInfo = { - idx: number[]; - start: number[]; - chars: number[]; - terms: number[]; - interIns: number[]; - intraIns: number[]; - interLft2: number[]; - interRgt2: number[]; - interLft1: number[]; - interRgt1: number[]; - ranges: number[][]; -}; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts index 7b070b9ab58..297e216b560 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts @@ -10,7 +10,7 @@ const uf = new uFuzzy({ intraDel: 1, }); -export function fuzzySearch(haystack: string[], query: string, dispatcher: (data: string[][]) => void) { +function fuzzySearch(haystack: string[], query: string, dispatcher: (data: string[][]) => void) { const [idxs, info, order] = uf.search(haystack, query, 0, 1e5); let haystackOrder: string[] = []; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx b/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx index e95dbbb731f..69b73a2484b 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx @@ -5,7 +5,7 @@ import { DataSourceApi, getDefaultTimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { EditorRow } from '@grafana/plugin-ui'; -import promqlGrammar from '../../../promql'; +import { promqlGrammar } from '../../../promql'; import { getInitHints } from '../../../query_hints'; import { buildVisualQueryFromString } from '../../parsing'; import { OperationExplainedBox } from '../../shared/OperationExplainedBox'; diff --git a/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts b/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts index f0df6046d58..9b6028ec0b0 100644 --- a/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts +++ b/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts @@ -5,7 +5,7 @@ import { store } from '@grafana/data'; export const promQueryEditorExplainKey = 'PrometheusQueryEditorExplainDefault'; -export type QueryEditorFlags = typeof promQueryEditorExplainKey; +type QueryEditorFlags = typeof promQueryEditorExplainKey; function getFlagValue(key: QueryEditorFlags, defaultValue = false): boolean { const val = store.get(key); diff --git a/packages/grafana-prometheus/src/querybuilder/operations.ts b/packages/grafana-prometheus/src/querybuilder/operations.ts index dba2bd17699..3f9e3ac21d4 100644 --- a/packages/grafana-prometheus/src/querybuilder/operations.ts +++ b/packages/grafana-prometheus/src/querybuilder/operations.ts @@ -284,7 +284,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { return list; } -export function createFunction(definition: Partial): QueryBuilderOperationDef { +function createFunction(definition: Partial): QueryBuilderOperationDef { return { ...definition, id: definition.id!, @@ -297,7 +297,7 @@ export function createFunction(definition: Partial): Q }; } -export function createRangeFunction(name: string, withRateInterval = false): QueryBuilderOperationDef { +function createRangeFunction(name: string, withRateInterval = false): QueryBuilderOperationDef { return { id: name, name: getPromOperationDisplayName(name), @@ -325,7 +325,7 @@ function operationTypeChangedHandlerForRangeFunction( return operation; } -export function operationWithRangeVectorRenderer( +function operationWithRangeVectorRenderer( model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string diff --git a/packages/grafana-prometheus/src/querybuilder/parsing.ts b/packages/grafana-prometheus/src/querybuilder/parsing.ts index e18c6071bab..9a517b53bd2 100644 --- a/packages/grafana-prometheus/src/querybuilder/parsing.ts +++ b/packages/grafana-prometheus/src/querybuilder/parsing.ts @@ -110,7 +110,7 @@ interface Context { * @param node * @param context */ -export function handleExpression(expr: string, node: SyntaxNode, context: Context) { +function handleExpression(expr: string, node: SyntaxNode, context: Context) { const visQuery = context.query; switch (node.type.id) { diff --git a/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts b/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts index 0346042ca02..3017bc33526 100644 --- a/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts +++ b/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts @@ -70,7 +70,7 @@ const varTypeFunc = [ * Get back the text with variables in their original format. * @param expr */ -export function returnVariables(expr: string) { +function returnVariables(expr: string) { return expr.replace(/__V_(\d)__(.+?)__V__(?:__F__(\w+)__F__)?/g, (match, type, v, f) => { return varTypeFunc[parseInt(type, 10)](v, f); }); diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx index 08178120a33..79ab7cd2219 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx @@ -19,7 +19,7 @@ import { VisualQueryModeller, } from './types'; -export interface Props { +interface Props { operation: QueryBuilderOperation; index: number; query: any; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationExplainedBox.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationExplainedBox.tsx index 4546f3cd427..23c8ca822f2 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationExplainedBox.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationExplainedBox.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { GrafanaTheme2, renderMarkdown } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -export interface Props { +interface Props { title?: React.ReactNode; children?: React.ReactNode; markdown?: string; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationHeader.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationHeader.tsx index 69a16649a12..d26f6f8f55f 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationHeader.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationHeader.tsx @@ -11,7 +11,7 @@ import { Button, Select, useStyles2 } from '@grafana/ui'; import { OperationInfoButton } from './OperationInfoButton'; import { QueryBuilderOperation, QueryBuilderOperationDef, VisualQueryModeller } from './types'; -export interface Props { +interface Props { operation: QueryBuilderOperation; def: QueryBuilderOperationDef; index: number; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx index 03b00cc1e4d..3669ece9146 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx @@ -19,7 +19,7 @@ import { Button, Portal, useStyles2 } from '@grafana/ui'; import { QueryBuilderOperation, QueryBuilderOperationDef } from './types'; -export interface Props { +interface Props { operation: QueryBuilderOperation; def: QueryBuilderOperationDef; } diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationList.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationList.tsx index a3a6a92090e..a02ca396c3f 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationList.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationList.tsx @@ -11,7 +11,7 @@ import { Button, Cascader, CascaderOption, useStyles2, Stack } from '@grafana/ui import { OperationEditor } from './OperationEditor'; import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from './types'; -export interface Props { +interface Props { query: T; datasource: DataSourceApi; onChange: (query: T) => void; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationListExplained.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationListExplained.tsx index 92afe395aaf..e53f93435b5 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationListExplained.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationListExplained.tsx @@ -5,7 +5,7 @@ import { OperationExplainedBox } from './OperationExplainedBox'; import { RawQuery } from './RawQuery'; import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from './types'; -export interface Props { +interface Props { query: T; queryModeller: VisualQueryModeller; explainMode?: boolean; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/QueryBuilderHints.tsx b/packages/grafana-prometheus/src/querybuilder/shared/QueryBuilderHints.tsx index c581c85cea8..8ffba3bb291 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/QueryBuilderHints.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/QueryBuilderHints.tsx @@ -10,7 +10,7 @@ import { Button, Tooltip, useStyles2 } from '@grafana/ui'; import { PrometheusDatasource } from '../../datasource'; import { PromQueryModellerInterface, PromVisualQuery } from '../types'; -export interface Props { +interface Props { query: PromVisualQuery; datasource: PrometheusDatasource; queryModeller: PromQueryModellerInterface; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorModeToggle.tsx b/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorModeToggle.tsx index 0b0951cac5a..4fdfe90931b 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorModeToggle.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/QueryEditorModeToggle.tsx @@ -1,19 +1,25 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx +import { t } from '@grafana/i18n'; import { RadioButtonGroup } from '@grafana/ui'; import { QueryEditorMode } from './types'; -export interface Props { +interface Props { mode: QueryEditorMode; onChange: (mode: QueryEditorMode) => void; } -const editorModes = [ - { label: 'Builder', value: QueryEditorMode.Builder }, - { label: 'Code', value: QueryEditorMode.Code }, -]; - export function QueryEditorModeToggle({ mode, onChange }: Props) { + const editorModes = [ + { + label: t('grafana-prometheus.querybuilder.query-editor-mode-toggle.editor-modes.label-builder', 'Builder'), + value: QueryEditorMode.Builder, + }, + { + label: t('grafana-prometheus.querybuilder.query-editor-mode-toggle.editor-modes.label-code', 'Code'), + value: QueryEditorMode.Code, + }, + ]; return (
diff --git a/packages/grafana-prometheus/src/querybuilder/shared/QueryHeaderSwitch.tsx b/packages/grafana-prometheus/src/querybuilder/shared/QueryHeaderSwitch.tsx index a5b484dd01e..39a0cc2ef71 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/QueryHeaderSwitch.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/QueryHeaderSwitch.tsx @@ -6,7 +6,7 @@ import { HTMLProps, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Switch, useStyles2, Stack } from '@grafana/ui'; -export interface Props extends Omit, 'value' | 'ref'> { +interface Props extends Omit, 'value' | 'ref'> { value?: boolean; label: string; } diff --git a/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx b/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx index 066e0f805ed..554201a820d 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/QueryOptionGroup.tsx @@ -6,7 +6,7 @@ import { useToggle } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { Collapse, useStyles2, Stack } from '@grafana/ui'; -export interface Props { +interface Props { title: string; collapsedInfo: string[]; children: React.ReactNode; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/RawQuery.tsx b/packages/grafana-prometheus/src/querybuilder/shared/RawQuery.tsx index 42bfa0842b9..a08c5109810 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/RawQuery.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/RawQuery.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useTheme2 } from '@grafana/ui'; -export interface Props { +interface Props { query: string; lang: { grammar: Grammar; diff --git a/packages/grafana-prometheus/src/querybuilder/shared/rendering/query.ts b/packages/grafana-prometheus/src/querybuilder/shared/rendering/query.ts index 0835de82020..936ce22dc9a 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/rendering/query.ts +++ b/packages/grafana-prometheus/src/querybuilder/shared/rendering/query.ts @@ -23,7 +23,7 @@ export function renderBinaryQueries( /** * Renders a binary query */ -export function renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary): string { +function renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary): string { let result = leftOperand + ` ${binaryQuery.operator} `; if (binaryQuery.vectorMatches) { diff --git a/packages/grafana-prometheus/src/querybuilder/shared/types.ts b/packages/grafana-prometheus/src/querybuilder/shared/types.ts index 35934943428..a06b0823c80 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/types.ts +++ b/packages/grafana-prometheus/src/querybuilder/shared/types.ts @@ -39,21 +39,17 @@ export interface QueryBuilderOperationDef extends RegistryItem { changeTypeHandler?: (op: QueryBuilderOperation, newDef: QueryBuilderOperationDef) => QueryBuilderOperation; } -export type QueryBuilderAddOperationHandler = ( - def: QueryBuilderOperationDef, - query: T, - modeller: VisualQueryModeller -) => T; +type QueryBuilderAddOperationHandler = (def: QueryBuilderOperationDef, query: T, modeller: VisualQueryModeller) => T; -export type QueryBuilderExplainOperationHandler = (op: QueryBuilderOperation, def?: QueryBuilderOperationDef) => string; +type QueryBuilderExplainOperationHandler = (op: QueryBuilderOperation, def?: QueryBuilderOperationDef) => string; -export type QueryBuilderOnParamChangedHandler = ( +type QueryBuilderOnParamChangedHandler = ( index: number, operation: QueryBuilderOperation, operationDef: QueryBuilderOperationDef ) => QueryBuilderOperation; -export type QueryBuilderOperationRenderer = ( +type QueryBuilderOperationRenderer = ( model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string @@ -75,16 +71,6 @@ export interface QueryBuilderOperationParamDef { runQueryOnEnter?: boolean; } -export interface QueryBuilderOperationEditorProps { - operation: QueryBuilderOperation; - index: number; - query: any; - datasource: DataSourceApi; - queryModeller: VisualQueryModeller; - onChange: (index: number, update: QueryBuilderOperation) => void; - onRemove: (index: number) => void; -} - export interface QueryBuilderOperationParamEditorProps { onChange: (index: number, value: QueryBuilderOperationParamValue) => void; onRunQuery: () => void; diff --git a/packages/grafana-prometheus/src/querybuilder/types.ts b/packages/grafana-prometheus/src/querybuilder/types.ts index e4a5781194d..39042a9b975 100644 --- a/packages/grafana-prometheus/src/querybuilder/types.ts +++ b/packages/grafana-prometheus/src/querybuilder/types.ts @@ -19,12 +19,19 @@ export interface PromVisualQuery { export interface PromQueryModellerInterface { renderLabels(labels: QueryBuilderLabelFilter[]): string; + renderQuery(query: PromVisualQuery, nested?: boolean): string; + hasBinaryOp(query: PromVisualQuery): boolean; + getQueryPatterns(): PromQueryPattern[]; + getOperationsForCategory(category: string): QueryBuilderOperationDef[]; + getOperationDef(id: string): QueryBuilderOperationDef | undefined; + getAlternativeOperations(key: string): QueryBuilderOperationDef[]; + getCategories(): string[]; } diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts index 28e10381b61..10462e3b3f4 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts @@ -646,7 +646,7 @@ describe('QueryCache: Prometheus', function () { }); describe('findDataPointStep', () => { - it('should interpolate custom interval', () => { + it('should interpolate custom interval when there is no calculatedMinStep in response', () => { const mockApplyInterpolation = jest.fn().mockImplementation(() => '1m'); const req = mockPromRequest(); req.targets[0].interval = '$interval'; @@ -654,4 +654,20 @@ describe('findDataPointStep', () => { findDatapointStep(req, respFrames, mockApplyInterpolation); expect(mockApplyInterpolation).toBeCalledTimes(1); }); + + it('should use the calculated minStep coming in response frame', () => { + const mockApplyInterpolation = jest.fn().mockImplementation(() => '1m'); + const req = mockPromRequest(); + // Cannot be interpolated on frontend as it was calculated on backend. + req.targets[0].interval = '$__rate_interval'; + const respFrames = trimmedFirstPointInPromFrames as unknown as DataFrame[]; + const expectedMinStep = 60000; + respFrames[0].meta = { + custom: { + calculatedMinStep: expectedMinStep, + }, + }; + const result = findDatapointStep(req, respFrames, mockApplyInterpolation); + expect(result).toBe(expectedMinStep); + }); }); diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.ts b/packages/grafana-prometheus/src/querycache/QueryCache.ts index 3afc3344f20..bb3dca14d4f 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.ts @@ -48,7 +48,7 @@ export interface CacheRequestInfo { * This is the string used to uniquely identify a field within a "target" * @param field */ -export const getFieldIdentity = (field: Field) => `${field.type}|${field.name}|${JSON.stringify(field.labels ?? '')}`; +const getFieldIdentity = (field: Field) => `${field.type}|${field.name}|${JSON.stringify(field.labels ?? '')}`; /** * NOMENCLATURE @@ -282,7 +282,9 @@ export function findDatapointStep( let dataPointStep = request.intervalMs; if (target?.interval) { - const minStepMs = rangeUtil.intervalToMs(applyInterpolation(target.interval)); + const minStepMs = + respFrames[0].meta?.custom?.['calculatedMinStep'] ?? + rangeUtil.intervalToMs(applyInterpolation(target.interval, request.scopedVars)); if (minStepMs > request.intervalMs) { dataPointStep = minStepMs; } diff --git a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts index ae283225f1b..2f9b8b9d490 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts @@ -7,7 +7,7 @@ import { clone } from 'lodash'; * @param start - First timestamp (ms) * @param step - step duration (ms) */ -export const getMockTimeFrameArray = (length: number, start: number, step: number) => { +const getMockTimeFrameArray = (length: number, start: number, step: number) => { let timeValues: number[] = []; for (let i = 0; i < length; i++) { timeValues.push(start + i * step); @@ -21,7 +21,7 @@ export const getMockTimeFrameArray = (length: number, start: number, step: numbe * @param values * @param high */ -export const getMockValueFrameArray = (length: number, values = 0): number[] => { +const getMockValueFrameArray = (length: number, values = 0): number[] => { return Array(length).fill(values); }; diff --git a/packages/grafana-prometheus/src/test/helpers/selectOptionInTest.ts b/packages/grafana-prometheus/src/test/helpers/selectOptionInTest.ts index 90b14190b56..e902abf315a 100644 --- a/packages/grafana-prometheus/src/test/helpers/selectOptionInTest.ts +++ b/packages/grafana-prometheus/src/test/helpers/selectOptionInTest.ts @@ -1,20 +1,9 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/packages/grafana-ui/src/components/Select/SelectBase.tsx import { waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { select } from 'react-select-event'; -import { byRole } from 'testing-library-selector'; // Used to select an option or options from a Select in unit tests export const selectOptionInTest = async ( input: HTMLElement, optionOrOptions: string | RegExp | Array ) => await waitFor(() => select(input, optionOrOptions, { container: document.body })); - -// Finds the parent of the Select so you can assert if it has a value -export const getSelectParent = (input: HTMLElement) => - input.parentElement?.parentElement?.parentElement?.parentElement?.parentElement; - -export const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise => { - await userEvent.click(byRole('combobox').get(selectElement)); - await selectOptionInTest(selectElement, optionText); -}; diff --git a/packages/grafana-prometheus/src/test/mocks/datasource.ts b/packages/grafana-prometheus/src/test/mocks/datasource.ts index 6befc818c38..f61535e3071 100644 --- a/packages/grafana-prometheus/src/test/mocks/datasource.ts +++ b/packages/grafana-prometheus/src/test/mocks/datasource.ts @@ -12,7 +12,7 @@ import { import { PromOptions, PromQuery } from '../../types'; -export const getMockDataSource = ( +const getMockDataSource = ( overrides?: Partial> ): DataSourceSettings => merge( @@ -98,82 +98,6 @@ export function createDefaultPromResponse() { }; } -export function createAnnotationResponse() { - const response = { - data: { - results: { - X: { - frames: [ - { - schema: { - name: 'bar', - refId: 'X', - meta: { - typeVersion: [0, 0], - executedQueryString: 'Expr: ALERTS{}\nStep: 1m0s', - }, - fields: [ - { - name: 'Time', - type: 'time', - typeInfo: { - frame: 'time.Time', - }, - }, - { - name: 'Value', - type: 'number', - typeInfo: { - frame: 'float64', - }, - labels: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - }, - ], - }, - data: { - values: [[123], [456]], - }, - }, - ], - }, - }, - }, - }; - - return { ...response }; -} - -export function createEmptyAnnotationResponse() { - const response = { - data: { - results: { - X: { - frames: [ - { - schema: { - name: 'bar', - refId: 'X', - fields: [], - }, - data: { - values: [], - }, - }, - ], - }, - }, - }, - }; - - return { ...response }; -} - export function getMockTimeRange(range = '6h'): TimeRange { return rangeUtil.convertRawToRange({ from: `now-${range}`, diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 3bef89b278e..3d129df19ae 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -58,7 +58,7 @@ "@grafana/faro-web-sdk": "^1.13.2", "@grafana/schema": "12.1.0-pre", "@grafana/ui": "12.1.0-pre", - "@types/systemjs": "6.15.1", + "@types/systemjs": "6.15.3", "history": "4.10.1", "lodash": "4.17.21", "react-loading-skeleton": "3.5.0", @@ -68,17 +68,17 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "16.0.1", "@rollup/plugin-terser": "0.4.4", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/history": "4.7.11", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "esbuild": "0.25.0", + "esbuild": "0.25.6", "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", @@ -87,7 +87,7 @@ "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", "rollup-plugin-sourcemaps": "0.6.3", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index ccfe41b0c9d..ca7bc045f9c 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -181,6 +181,7 @@ class DataSourceWithBackend< if (datasource.uid?.length) { dsUIDs.add(datasource.uid); } + return { ...(shouldApplyTemplateVariables ? this.applyTemplateVariables(q, request.scopedVars, request.filters) : q), datasource, @@ -225,9 +226,6 @@ class DataSourceWithBackend< if (!(config.featureToggles.queryService || config.featureToggles.grafanaAPIServerWithExperimentalAPIs)) { console.warn('feature toggle queryServiceFromUI also requires the queryService to be running'); } else { - if (!hasExpr && dsUIDs.size === 1) { - // TODO? can we talk directly to the apiserver? - } url = `/apis/query.grafana.app/v0alpha1/namespaces/${config.namespace}/query?ds_type=${this.type}`; } } diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 35c120f7cf0..8dc2e5efe1f 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -37,14 +37,14 @@ }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "16.0.0", - "esbuild": "0.25.0", + "@rollup/plugin-node-resolve": "16.0.1", + "esbuild": "0.25.6", "glob": "^11.0.0", "rimraf": "6.0.1", "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.0", "rollup-plugin-node-externals": "^8.0.0", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "dependencies": { "tslib": "2.8.1" diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 7986476fa56..7efc5e3ff60 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -708,6 +708,7 @@ export enum TableCellDisplayMode { Image = 'image', JSONView = 'json-view', LcdGauge = 'lcd-gauge', + Pill = 'pill', Sparkline = 'sparkline', } @@ -838,7 +839,38 @@ export enum TableCellHeight { * Table cell options. Each cell has a display mode * and other potential options for that display. */ -export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions); +export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions); + +/** + * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) + * Generally defines alignment, filtering capabilties, display options, etc. + */ +export interface TableFieldOptions { + align: FieldTextAlignment; + cellOptions: TableCellOptions; + /** + * This field is deprecated in favor of using cellOptions + */ + displayMode?: TableCellDisplayMode; + filterable?: boolean; + hidden?: boolean; // ?? default is missing or false ?? + /** + * Hides any header for a column, useful for columns that show some static content or buttons. + */ + hideHeader?: boolean; + inspect: boolean; + minWidth?: number; + width?: number; + /** + * Enables text wrapping for column headers + */ + wrapHeaderText?: boolean; +} + +export const defaultTableFieldOptions: Partial = { + align: 'auto', + inspect: false, +}; /** * Use UTC/GMT timezone @@ -856,6 +888,7 @@ export type TimeZoneBrowser = 'browser'; */ export enum VariableFormatID { CSV = 'csv', + CustomQueryParam = 'customqueryparam', Date = 'date', Distributed = 'distributed', DoubleQuote = 'doublequote', @@ -944,33 +977,10 @@ export enum ComparisonOperation { NEQ = 'neq', } -/** - * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) - * Generally defines alignment, filtering capabilties, display options, etc. - */ -export interface TableFieldOptions { - align: FieldTextAlignment; - cellOptions: TableCellOptions; - /** - * This field is deprecated in favor of using cellOptions - */ - displayMode?: TableCellDisplayMode; - filterable?: boolean; - hidden?: boolean; // ?? default is missing or false ?? - /** - * Hides any header for a column, useful for columns that show some static content or buttons. - */ - hideHeader?: boolean; - inspect: boolean; - minWidth?: number; - width?: number; +export interface TablePillCellOptions { + type: TableCellDisplayMode.Pill; } -export const defaultTableFieldOptions: Partial = { - align: 'auto', - inspect: false, -}; - /** * A specific timezone from https://en.wikipedia.org/wiki/Tz_database */ diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index 7548275ada7..acf4a6b7480 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -4,7 +4,7 @@ package common // in the table such as colored text, JSON, gauge, etc. // The color-background-solid, gradient-gauge, and lcd-gauge // modes are deprecated in favor of new cell subOptions -TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions") +TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill") // Display mode to the "Colored Background" display // mode for table cells. Either displays a solid color (basic mode) @@ -89,7 +89,7 @@ TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum") // Table cell options. Each cell has a display mode // and other potential options for that display. -TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") +TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") // Field options for each field within a table (e.g 10, "The String", 64.20, etc.) // Generally defines alignment, filtering capabilties, display options, etc. @@ -105,5 +105,10 @@ TableFieldOptions: { filterable?: bool // Hides any header for a column, useful for columns that show some static content or buttons. hideHeader?: bool + // Enables text wrapping for column headers + wrapHeaderText?: bool } @cuetsy(kind="interface") +TablePillCellOptions: { + type: TableCellDisplayMode & "pill" +} @cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/common/variables.cue b/packages/grafana-schema/src/common/variables.cue index 2c161ee06ea..d29d1409989 100644 --- a/packages/grafana-schema/src/common/variables.cue +++ b/packages/grafana-schema/src/common/variables.cue @@ -38,4 +38,6 @@ VariableFormatID: // Format variables in their text representation. Example in multi-variable scenario A + B + C. "text" | // Format variables as URL parameters. Example in multi-variable scenario A + B + C => var-foo=A&var-foo=B&var-foo=C. - "queryparam" @cuetsy(kind="enum",memberNames="Join|Lucene|Raw|Regex|Pipe|Distributed|CSV|HTML|JSON|PercentEncode|UriEncode|SingleQuote|DoubleQuote|SQLString|Date|Glob|Text|QueryParam") + "queryparam" | + // Format variables as URL parameters with custom parameter name and value prefix. + "customqueryparam" @cuetsy(kind="enum",memberNames="Join|Lucene|Raw|Regex|Pipe|Distributed|CSV|HTML|JSON|PercentEncode|UriEncode|SingleQuote|DoubleQuote|SQLString|Date|Glob|Text|QueryParam|CustomQueryParam") diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index dbf274e51bc..73b7933cd79 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -15,6 +15,7 @@ export const pluginVersion = "12.1.0-pre"; export interface Options { controlsStorageKey?: string; dedupStrategy: common.LogsDedupStrategy; + detailsMode?: ('inline' | 'sidebar'); displayedFields?: Array; enableInfiniteScrolling?: boolean; enableLogDetails: boolean; diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index fbaa9c8f09b..5b043cd96d0 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -191,6 +191,14 @@ export interface VariableModel { * Options sort order */ sort?: VariableSort; + /** + * Additional static options for query variable + */ + staticOptions?: Array; + /** + * Ordering of static options in relation to options returned from data source for query variable + */ + staticOptionsOrder?: ('before' | 'after' | 'sorted'); /** * Type of variable */ @@ -203,6 +211,7 @@ export const defaultVariableModel: Partial = { multi: false, options: [], skipUrlSync: false, + staticOptions: [], }; /** diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 34ee46e0cff..04fc5092d8a 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -1005,6 +1005,8 @@ export interface QueryVariableSpec { allValue?: string; placeholder?: string; allowCustomValue: boolean; + staticOptions?: VariableOption[]; + staticOptionsOrder?: "before" | "after" | "sorted"; } export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/dashboard_object_gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha2/dashboard_object_gen.ts new file mode 100644 index 00000000000..a89d50ebd2d --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha2/dashboard_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Dashboard { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.metadata.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.spec.gen.ts new file mode 100644 index 00000000000..04fc5092d8a --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.spec.gen.ts @@ -0,0 +1,1404 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface AnnotationQueryKind { + kind: "AnnotationQuery"; + spec: AnnotationQuerySpec; +} + +export const defaultAnnotationQueryKind = (): AnnotationQueryKind => ({ + kind: "AnnotationQuery", + spec: defaultAnnotationQuerySpec(), +}); + +export interface AnnotationQuerySpec { + datasource?: DataSourceRef; + query?: DataQueryKind; + enable: boolean; + hide: boolean; + iconColor: string; + name: string; + builtIn?: boolean; + filter?: AnnotationPanelFilter; + // Catch-all field for datasource-specific properties + legacyOptions?: Record; +} + +export const defaultAnnotationQuerySpec = (): AnnotationQuerySpec => ({ + enable: false, + hide: false, + iconColor: "", + name: "", + builtIn: false, +}); + +export interface DataSourceRef { + // The plugin type-id + type?: string; + // Specific datasource instance + uid?: string; +} + +export const defaultDataSourceRef = (): DataSourceRef => ({ +}); + +export interface DataQueryKind { + // The kind of a DataQueryKind is the datasource type + kind: string; + spec: Record; +} + +export const defaultDataQueryKind = (): DataQueryKind => ({ + kind: "", + spec: {}, +}); + +export interface AnnotationPanelFilter { + // Should the specified panels be included or excluded + exclude?: boolean; + // Panel IDs that should be included or excluded + ids: number[]; +} + +export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({ + exclude: false, + ids: [], +}); + +// "Off" for no shared crosshair or tooltip (default). +// "Crosshair" for shared crosshair. +// "Tooltip" for shared crosshair AND shared tooltip. +export type DashboardCursorSync = "Crosshair" | "Tooltip" | "Off"; + +export const defaultDashboardCursorSync = (): DashboardCursorSync => ("Off"); + +// Supported dashboard elements +// |* more element types in the future +export type Element = PanelKind | LibraryPanelKind; + +export const defaultElement = (): Element => (defaultPanelKind()); + +export interface PanelKind { + kind: "Panel"; + spec: PanelSpec; +} + +export const defaultPanelKind = (): PanelKind => ({ + kind: "Panel", + spec: defaultPanelSpec(), +}); + +export interface PanelSpec { + id: number; + title: string; + description: string; + links: DataLink[]; + data: QueryGroupKind; + vizConfig: VizConfigKind; + transparent?: boolean; +} + +export const defaultPanelSpec = (): PanelSpec => ({ + id: 0, + title: "", + description: "", + links: [], + data: defaultQueryGroupKind(), + vizConfig: defaultVizConfigKind(), +}); + +export interface DataLink { + title: string; + url: string; + targetBlank?: boolean; +} + +export const defaultDataLink = (): DataLink => ({ + title: "", + url: "", +}); + +export interface QueryGroupKind { + kind: "QueryGroup"; + spec: QueryGroupSpec; +} + +export const defaultQueryGroupKind = (): QueryGroupKind => ({ + kind: "QueryGroup", + spec: defaultQueryGroupSpec(), +}); + +export interface QueryGroupSpec { + queries: PanelQueryKind[]; + transformations: TransformationKind[]; + queryOptions: QueryOptionsSpec; +} + +export const defaultQueryGroupSpec = (): QueryGroupSpec => ({ + queries: [], + transformations: [], + queryOptions: defaultQueryOptionsSpec(), +}); + +export interface PanelQueryKind { + kind: "PanelQuery"; + spec: PanelQuerySpec; +} + +export const defaultPanelQueryKind = (): PanelQueryKind => ({ + kind: "PanelQuery", + spec: defaultPanelQuerySpec(), +}); + +export interface PanelQuerySpec { + query: DataQueryKind; + datasource?: DataSourceRef; + refId: string; + hidden: boolean; +} + +export const defaultPanelQuerySpec = (): PanelQuerySpec => ({ + query: defaultDataQueryKind(), + refId: "", + hidden: false, +}); + +export interface TransformationKind { + // The kind of a TransformationKind is the transformation ID + kind: string; + spec: DataTransformerConfig; +} + +export const defaultTransformationKind = (): TransformationKind => ({ + kind: "", + spec: defaultDataTransformerConfig(), +}); + +// Transformations allow to manipulate data returned by a query before the system applies a visualization. +// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, +// use the output of one transformation as the input to another transformation, etc. +export interface DataTransformerConfig { + // Unique identifier of transformer + id: string; + // Disabled transformations are skipped + disabled?: boolean; + // Optional frame matcher. When missing it will be applied to all results + filter?: MatcherConfig; + // Where to pull DataFrames from as input to transformation + topic?: DataTopic; + // Options to be passed to the transformer + // Valid options depend on the transformer id + options: any; +} + +export const defaultDataTransformerConfig = (): DataTransformerConfig => ({ + id: "", + options: {}, +}); + +// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. +// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. +export interface MatcherConfig { + // The matcher id. This is used to find the matcher implementation from registry. + id: string; + // The matcher options. This is specific to the matcher implementation. + options?: any; +} + +export const defaultMatcherConfig = (): MatcherConfig => ({ + id: "", +}); + +// A topic is attached to DataFrame metadata in query results. +// This specifies where the data should be used. +export type DataTopic = "series" | "annotations" | "alertStates"; + +export const defaultDataTopic = (): DataTopic => ("series"); + +export interface QueryOptionsSpec { + timeFrom?: string; + maxDataPoints?: number; + timeShift?: string; + queryCachingTTL?: number; + interval?: string; + cacheTimeout?: string; + hideTimeOverride?: boolean; +} + +export const defaultQueryOptionsSpec = (): QueryOptionsSpec => ({ +}); + +export interface VizConfigKind { + // The kind of a VizConfigKind is the plugin ID + kind: string; + spec: VizConfigSpec; +} + +export const defaultVizConfigKind = (): VizConfigKind => ({ + kind: "", + spec: defaultVizConfigSpec(), +}); + +// --- Kinds --- +export interface VizConfigSpec { + pluginVersion: string; + options: Record; + fieldConfig: FieldConfigSource; +} + +export const defaultVizConfigSpec = (): VizConfigSpec => ({ + pluginVersion: "", + options: {}, + fieldConfig: defaultFieldConfigSource(), +}); + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +export interface FieldConfigSource { + // Defaults are the options applied to all fields. + defaults: FieldConfig; + // Overrides are the options applied to specific fields overriding the defaults. + overrides: { + matcher: MatcherConfig; + properties: DynamicConfigValue[]; + }[]; +} + +export const defaultFieldConfigSource = (): FieldConfigSource => ({ + defaults: defaultFieldConfig(), + overrides: [], +}); + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +export interface FieldConfig { + // The display value for this field. This supports template variables blank is auto + displayName?: string; + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string; + // Human readable field metadata + description?: string; + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string; + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: boolean; + // True if data source field supports ad-hoc filters + filterable?: boolean; + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + unit?: string; + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + decimals?: number; + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + min?: number; + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + max?: number; + // Convert input values into a display string + mappings?: ValueMapping[]; + // Map numeric values to states + thresholds?: ThresholdsConfig; + // Panel color configuration + color?: FieldColor; + // The behavior when clicking on a result + links?: any[]; + // Alternative to empty string + noValue?: string; + // custom is specified by the FieldConfig field + // in panel plugin schemas. + custom?: Record; +} + +export const defaultFieldConfig = (): FieldConfig => ({ +}); + +export type ValueMapping = ValueMap | RangeMap | RegexMap | SpecialValueMap; + +export const defaultValueMapping = (): ValueMapping => (defaultValueMap()); + +// Maps text values to a color or different display text and color. +// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +export interface ValueMap { + type: "value"; + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + options: Record; +} + +export const defaultValueMap = (): ValueMap => ({ + type: "value", + options: {}, +}); + +// Supported value mapping types +// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. +export type MappingType = "value" | "range" | "regex" | "special"; + +export const defaultMappingType = (): MappingType => ("value"); + +// Result used as replacement with text and color when the value matches +export interface ValueMappingResult { + // Text to display when the value matches + text?: string; + // Text to use when the value matches + color?: string; + // Icon to display when the value matches. Only specific visualizations. + icon?: string; + // Position in the mapping array. Only used internally. + index?: number; +} + +export const defaultValueMappingResult = (): ValueMappingResult => ({ +}); + +// Maps numerical ranges to a display text and color. +// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +export interface RangeMap { + type: "range"; + // Range to match against and the result to apply when the value is within the range + options: { + // Min value of the range. It can be null which means -Infinity + from: number | null; + // Max value of the range. It can be null which means +Infinity + to: number | null; + // Config to apply when the value is within the range + result: ValueMappingResult; + }; +} + +export const defaultRangeMap = (): RangeMap => ({ + type: "range", + options: { + from: 0, + to: 0, + result: defaultValueMappingResult(), +}, +}); + +// Maps regular expressions to replacement text and a color. +// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +export interface RegexMap { + type: "regex"; + // Regular expression to match against and the result to apply when the value matches the regex + options: { + // Regular expression to match against + pattern: string; + // Config to apply when the value matches the regex + result: ValueMappingResult; + }; +} + +export const defaultRegexMap = (): RegexMap => ({ + type: "regex", + options: { + pattern: "", + result: defaultValueMappingResult(), +}, +}); + +// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. +// See SpecialValueMatch to see the list of special values. +// For example, you can configure a special value mapping so that null values appear as N/A. +export interface SpecialValueMap { + type: "special"; + options: { + // Special value to match against + match: SpecialValueMatch; + // Config to apply when the value matches the special value + result: ValueMappingResult; + }; +} + +export const defaultSpecialValueMap = (): SpecialValueMap => ({ + type: "special", + options: { + match: "true", + result: defaultValueMappingResult(), +}, +}); + +// Special value types supported by the `SpecialValueMap` +export type SpecialValueMatch = "true" | "false" | "null" | "nan" | "null+nan" | "empty"; + +export const defaultSpecialValueMatch = (): SpecialValueMatch => ("true"); + +export interface ThresholdsConfig { + mode: ThresholdsMode; + steps: Threshold[]; +} + +export const defaultThresholdsConfig = (): ThresholdsConfig => ({ + mode: "absolute", + steps: [], +}); + +export type ThresholdsMode = "absolute" | "percentage"; + +export const defaultThresholdsMode = (): ThresholdsMode => ("absolute"); + +export interface Threshold { + value: number; + color: string; +} + +export const defaultThreshold = (): Threshold => ({ + value: 0, + color: "", +}); + +// Map a field to a color. +export interface FieldColor { + // The main color scheme mode. + mode: FieldColorModeId; + // The fixed color value for fixed or shades color modes. + fixedColor?: string; + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: FieldColorSeriesByMode; +} + +export const defaultFieldColor = (): FieldColor => ({ + mode: "thresholds", +}); + +// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. +// Continuous color interpolates a color using the percentage of a value relative to min and max. +// Accepted values are: +// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold +// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations +// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations +// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode +// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode +// `continuous-YlRd`: Continuous Yellow-Red palette mode +// `continuous-BlPu`: Continuous Blue-Purple palette mode +// `continuous-YlBl`: Continuous Yellow-Blue palette mode +// `continuous-blues`: Continuous Blue palette mode +// `continuous-reds`: Continuous Red palette mode +// `continuous-greens`: Continuous Green palette mode +// `continuous-purples`: Continuous Purple palette mode +// `shades`: Shades of a single color. Specify a single color, useful in an override rule. +// `fixed`: Fixed color mode. Specify a single color, useful in an override rule. +export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"; + +export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds"); + +// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. +export type FieldColorSeriesByMode = "min" | "max" | "last"; + +export const defaultFieldColorSeriesByMode = (): FieldColorSeriesByMode => ("min"); + +export interface DynamicConfigValue { + id: string; + value?: any; +} + +export const defaultDynamicConfigValue = (): DynamicConfigValue => ({ + id: "", +}); + +export interface LibraryPanelKind { + kind: "LibraryPanel"; + spec: LibraryPanelKindSpec; +} + +export const defaultLibraryPanelKind = (): LibraryPanelKind => ({ + kind: "LibraryPanel", + spec: defaultLibraryPanelKindSpec(), +}); + +export interface LibraryPanelKindSpec { + // Panel ID for the library panel in the dashboard + id: number; + // Title for the library panel in the dashboard + title: string; + libraryPanel: LibraryPanelRef; +} + +export const defaultLibraryPanelKindSpec = (): LibraryPanelKindSpec => ({ + id: 0, + title: "", + libraryPanel: defaultLibraryPanelRef(), +}); + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +export interface LibraryPanelRef { + // Library panel name + name: string; + // Library panel uid + uid: string; +} + +export const defaultLibraryPanelRef = (): LibraryPanelRef => ({ + name: "", + uid: "", +}); + +export interface GridLayoutKind { + kind: "GridLayout"; + spec: GridLayoutSpec; +} + +export const defaultGridLayoutKind = (): GridLayoutKind => ({ + kind: "GridLayout", + spec: defaultGridLayoutSpec(), +}); + +export interface GridLayoutSpec { + items: GridLayoutItemKind[]; +} + +export const defaultGridLayoutSpec = (): GridLayoutSpec => ({ + items: [], +}); + +export interface GridLayoutItemKind { + kind: "GridLayoutItem"; + spec: GridLayoutItemSpec; +} + +export const defaultGridLayoutItemKind = (): GridLayoutItemKind => ({ + kind: "GridLayoutItem", + spec: defaultGridLayoutItemSpec(), +}); + +export interface GridLayoutItemSpec { + x: number; + y: number; + width: number; + height: number; + // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference + element: ElementReference; + repeat?: RepeatOptions; +} + +export const defaultGridLayoutItemSpec = (): GridLayoutItemSpec => ({ + x: 0, + y: 0, + width: 0, + height: 0, + element: defaultElementReference(), +}); + +export interface ElementReference { + kind: "ElementReference"; + name: string; +} + +export const defaultElementReference = (): ElementReference => ({ + kind: "ElementReference", + name: "", +}); + +export interface RepeatOptions { + mode: "variable"; + value: string; + direction?: "h" | "v"; + maxPerRow?: number; +} + +export const defaultRepeatOptions = (): RepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + +// other repeat modes will be added in the future: label, frame +export const RepeatMode = "variable"; + +export interface RowsLayoutKind { + kind: "RowsLayout"; + spec: RowsLayoutSpec; +} + +export const defaultRowsLayoutKind = (): RowsLayoutKind => ({ + kind: "RowsLayout", + spec: defaultRowsLayoutSpec(), +}); + +export interface RowsLayoutSpec { + rows: RowsLayoutRowKind[]; +} + +export const defaultRowsLayoutSpec = (): RowsLayoutSpec => ({ + rows: [], +}); + +export interface RowsLayoutRowKind { + kind: "RowsLayoutRow"; + spec: RowsLayoutRowSpec; +} + +export const defaultRowsLayoutRowKind = (): RowsLayoutRowKind => ({ + kind: "RowsLayoutRow", + spec: defaultRowsLayoutRowSpec(), +}); + +export interface RowsLayoutRowSpec { + title?: string; + collapse?: boolean; + hideHeader?: boolean; + fillScreen?: boolean; + conditionalRendering?: ConditionalRenderingGroupKind; + repeat?: RowRepeatOptions; + layout: GridLayoutKind | AutoGridLayoutKind | TabsLayoutKind | RowsLayoutKind; +} + +export const defaultRowsLayoutRowSpec = (): RowsLayoutRowSpec => ({ + layout: defaultGridLayoutKind(), +}); + +export interface ConditionalRenderingGroupKind { + kind: "ConditionalRenderingGroup"; + spec: ConditionalRenderingGroupSpec; +} + +export const defaultConditionalRenderingGroupKind = (): ConditionalRenderingGroupKind => ({ + kind: "ConditionalRenderingGroup", + spec: defaultConditionalRenderingGroupSpec(), +}); + +export interface ConditionalRenderingGroupSpec { + visibility: "show" | "hide"; + condition: "and" | "or"; + items: (ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind)[]; +} + +export const defaultConditionalRenderingGroupSpec = (): ConditionalRenderingGroupSpec => ({ + visibility: "show", + condition: "and", + items: [], +}); + +export interface ConditionalRenderingVariableKind { + kind: "ConditionalRenderingVariable"; + spec: ConditionalRenderingVariableSpec; +} + +export const defaultConditionalRenderingVariableKind = (): ConditionalRenderingVariableKind => ({ + kind: "ConditionalRenderingVariable", + spec: defaultConditionalRenderingVariableSpec(), +}); + +export interface ConditionalRenderingVariableSpec { + variable: string; + operator: "equals" | "notEquals"; + value: string; +} + +export const defaultConditionalRenderingVariableSpec = (): ConditionalRenderingVariableSpec => ({ + variable: "", + operator: "equals", + value: "", +}); + +export interface ConditionalRenderingDataKind { + kind: "ConditionalRenderingData"; + spec: ConditionalRenderingDataSpec; +} + +export const defaultConditionalRenderingDataKind = (): ConditionalRenderingDataKind => ({ + kind: "ConditionalRenderingData", + spec: defaultConditionalRenderingDataSpec(), +}); + +export interface ConditionalRenderingDataSpec { + value: boolean; +} + +export const defaultConditionalRenderingDataSpec = (): ConditionalRenderingDataSpec => ({ + value: false, +}); + +export interface ConditionalRenderingTimeRangeSizeKind { + kind: "ConditionalRenderingTimeRangeSize"; + spec: ConditionalRenderingTimeRangeSizeSpec; +} + +export const defaultConditionalRenderingTimeRangeSizeKind = (): ConditionalRenderingTimeRangeSizeKind => ({ + kind: "ConditionalRenderingTimeRangeSize", + spec: defaultConditionalRenderingTimeRangeSizeSpec(), +}); + +export interface ConditionalRenderingTimeRangeSizeSpec { + value: string; +} + +export const defaultConditionalRenderingTimeRangeSizeSpec = (): ConditionalRenderingTimeRangeSizeSpec => ({ + value: "", +}); + +export interface RowRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultRowRepeatOptions = (): RowRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + +export interface AutoGridLayoutKind { + kind: "AutoGridLayout"; + spec: AutoGridLayoutSpec; +} + +export const defaultAutoGridLayoutKind = (): AutoGridLayoutKind => ({ + kind: "AutoGridLayout", + spec: defaultAutoGridLayoutSpec(), +}); + +export interface AutoGridLayoutSpec { + maxColumnCount?: number; + columnWidthMode: "narrow" | "standard" | "wide" | "custom"; + columnWidth?: number; + rowHeightMode: "short" | "standard" | "tall" | "custom"; + rowHeight?: number; + fillScreen?: boolean; + items: AutoGridLayoutItemKind[]; +} + +export const defaultAutoGridLayoutSpec = (): AutoGridLayoutSpec => ({ + maxColumnCount: 3, + columnWidthMode: "standard", + rowHeightMode: "standard", + fillScreen: false, + items: [], +}); + +export interface AutoGridLayoutItemKind { + kind: "AutoGridLayoutItem"; + spec: AutoGridLayoutItemSpec; +} + +export const defaultAutoGridLayoutItemKind = (): AutoGridLayoutItemKind => ({ + kind: "AutoGridLayoutItem", + spec: defaultAutoGridLayoutItemSpec(), +}); + +export interface AutoGridLayoutItemSpec { + element: ElementReference; + repeat?: AutoGridRepeatOptions; + conditionalRendering?: ConditionalRenderingGroupKind; +} + +export const defaultAutoGridLayoutItemSpec = (): AutoGridLayoutItemSpec => ({ + element: defaultElementReference(), +}); + +export interface AutoGridRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultAutoGridRepeatOptions = (): AutoGridRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + +export interface TabsLayoutKind { + kind: "TabsLayout"; + spec: TabsLayoutSpec; +} + +export const defaultTabsLayoutKind = (): TabsLayoutKind => ({ + kind: "TabsLayout", + spec: defaultTabsLayoutSpec(), +}); + +export interface TabsLayoutSpec { + tabs: TabsLayoutTabKind[]; +} + +export const defaultTabsLayoutSpec = (): TabsLayoutSpec => ({ + tabs: [], +}); + +export interface TabsLayoutTabKind { + kind: "TabsLayoutTab"; + spec: TabsLayoutTabSpec; +} + +export const defaultTabsLayoutTabKind = (): TabsLayoutTabKind => ({ + kind: "TabsLayoutTab", + spec: defaultTabsLayoutTabSpec(), +}); + +export interface TabsLayoutTabSpec { + title?: string; + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind; + conditionalRendering?: ConditionalRenderingGroupKind; + repeat?: TabRepeatOptions; +} + +export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({ + layout: defaultGridLayoutKind(), +}); + +export interface TabRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultTabRepeatOptions = (): TabRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + +// Links with references to other dashboards or external resources +export interface DashboardLink { + // Title to display with the link + title: string; + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` + type: DashboardLinkType; + // Icon name to be displayed with the link + icon: string; + // Tooltip to display when the user hovers their mouse over it + tooltip: string; + // Link URL. Only required/valid if the type is link + url?: string; + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: string[]; + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: boolean; + // If true, the link will be opened in a new tab + targetBlank: boolean; + // If true, includes current template variables values in the link as query params + includeVars: boolean; + // If true, includes current time range in the link as query params + keepTime: boolean; +} + +export const defaultDashboardLink = (): DashboardLink => ({ + title: "", + type: "link", + icon: "", + tooltip: "", + tags: [], + asDropdown: false, + targetBlank: false, + includeVars: false, + keepTime: false, +}); + +// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) +export type DashboardLinkType = "link" | "dashboards"; + +export const defaultDashboardLinkType = (): DashboardLinkType => ("link"); + +// Time configuration +// It defines the default time config for the time picker, the refresh picker for the specific dashboard. +export interface TimeSettingsSpec { + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string; + // Start time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + from: string; + // End time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + to: string; + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + // v1: refresh + autoRefresh: string; + // Interval options available in the refresh picker dropdown. + // v1: timepicker.refresh_intervals + autoRefreshIntervals: string[]; + // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. + // v1: timepicker.quick_ranges , not exposed in the UI + quickRanges?: TimeRangeOption[]; + // Whether timepicker is visible or not. + // v1: timepicker.hidden + hideTimepicker: boolean; + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: "saturday" | "monday" | "sunday"; + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth: number; + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + // v1: timepicker.nowDelay + nowDelay?: string; +} + +export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({ + timezone: "browser", + from: "now-6h", + to: "now", + autoRefresh: "", + autoRefreshIntervals: [ +"5s", +"10s", +"30s", +"1m", +"5m", +"15m", +"30m", +"1h", +"2h", +"1d", +], + hideTimepicker: false, + fiscalYearStartMonth: 0, +}); + +export interface TimeRangeOption { + display: string; + from: string; + to: string; +} + +export const defaultTimeRangeOption = (): TimeRangeOption => ({ + display: "Last 6 hours", + from: "now-6h", + to: "now", +}); + +export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind; + +export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind()); + +// Query variable kind +export interface QueryVariableKind { + kind: "QueryVariable"; + spec: QueryVariableSpec; +} + +export const defaultQueryVariableKind = (): QueryVariableKind => ({ + kind: "QueryVariable", + spec: defaultQueryVariableSpec(), +}); + +// Query variable specification +export interface QueryVariableSpec { + name: string; + current: VariableOption; + label?: string; + hide: VariableHide; + refresh: VariableRefresh; + skipUrlSync: boolean; + description?: string; + datasource?: DataSourceRef; + query: DataQueryKind; + regex: string; + sort: VariableSort; + definition?: string; + options: VariableOption[]; + multi: boolean; + includeAll: boolean; + allValue?: string; + placeholder?: string; + allowCustomValue: boolean; + staticOptions?: VariableOption[]; + staticOptionsOrder?: "before" | "after" | "sorted"; +} + +export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ + name: "", + current: { text: "", value: "", }, + hide: "dontHide", + refresh: "never", + skipUrlSync: false, + query: defaultDataQueryKind(), + regex: "", + sort: "disabled", + options: [], + multi: false, + includeAll: false, + allowCustomValue: true, +}); + +// Variable option specification +export interface VariableOption { + // Whether the option is selected or not + selected?: boolean; + // Text to be displayed for the option + text: string | string[]; + // Value of the option + value: string | string[]; +} + +export const defaultVariableOption = (): VariableOption => ({ + text: "", + value: "", +}); + +// Determine if the variable shows on dashboard +// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). +export type VariableHide = "dontHide" | "hideLabel" | "hideVariable"; + +export const defaultVariableHide = (): VariableHide => ("dontHide"); + +// Options to config when to refresh a variable +// `never`: Never refresh the variable +// `onDashboardLoad`: Queries the data source every time the dashboard loads. +// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. +export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"; + +export const defaultVariableRefresh = (): VariableRefresh => ("never"); + +// Sort variable options +// Accepted values are: +// `disabled`: No sorting +// `alphabeticalAsc`: Alphabetical ASC +// `alphabeticalDesc`: Alphabetical DESC +// `numericalAsc`: Numerical ASC +// `numericalDesc`: Numerical DESC +// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC +// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC +// `naturalAsc`: Natural ASC +// `naturalDesc`: Natural DESC +// VariableSort enum with default value +export type VariableSort = "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc"; + +export const defaultVariableSort = (): VariableSort => ("disabled"); + +// Text variable kind +export interface TextVariableKind { + kind: "TextVariable"; + spec: TextVariableSpec; +} + +export const defaultTextVariableKind = (): TextVariableKind => ({ + kind: "TextVariable", + spec: defaultTextVariableSpec(), +}); + +// Text variable specification +export interface TextVariableSpec { + name: string; + current: VariableOption; + query: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultTextVariableSpec = (): TextVariableSpec => ({ + name: "", + current: { text: "", value: "", }, + query: "", + hide: "dontHide", + skipUrlSync: false, +}); + +// Constant variable kind +export interface ConstantVariableKind { + kind: "ConstantVariable"; + spec: ConstantVariableSpec; +} + +export const defaultConstantVariableKind = (): ConstantVariableKind => ({ + kind: "ConstantVariable", + spec: defaultConstantVariableSpec(), +}); + +// Constant variable specification +export interface ConstantVariableSpec { + name: string; + query: string; + current: VariableOption; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultConstantVariableSpec = (): ConstantVariableSpec => ({ + name: "", + query: "", + current: { text: "", value: "", }, + hide: "dontHide", + skipUrlSync: false, +}); + +// Datasource variable kind +export interface DatasourceVariableKind { + kind: "DatasourceVariable"; + spec: DatasourceVariableSpec; +} + +export const defaultDatasourceVariableKind = (): DatasourceVariableKind => ({ + kind: "DatasourceVariable", + spec: defaultDatasourceVariableSpec(), +}); + +// Datasource variable specification +export interface DatasourceVariableSpec { + name: string; + pluginId: string; + refresh: VariableRefresh; + regex: string; + current: VariableOption; + options: VariableOption[]; + multi: boolean; + includeAll: boolean; + allValue?: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; + allowCustomValue: boolean; +} + +export const defaultDatasourceVariableSpec = (): DatasourceVariableSpec => ({ + name: "", + pluginId: "", + refresh: "never", + regex: "", + current: { text: "", value: "", }, + options: [], + multi: false, + includeAll: false, + hide: "dontHide", + skipUrlSync: false, + allowCustomValue: true, +}); + +// Interval variable kind +export interface IntervalVariableKind { + kind: "IntervalVariable"; + spec: IntervalVariableSpec; +} + +export const defaultIntervalVariableKind = (): IntervalVariableKind => ({ + kind: "IntervalVariable", + spec: defaultIntervalVariableSpec(), +}); + +// Interval variable specification +export interface IntervalVariableSpec { + name: string; + query: string; + current: VariableOption; + options: VariableOption[]; + auto: boolean; + auto_min: string; + auto_count: number; + refresh: VariableRefresh; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({ + name: "", + query: "", + current: { text: "", value: "", }, + options: [], + auto: false, + auto_min: "", + auto_count: 0, + refresh: "never", + hide: "dontHide", + skipUrlSync: false, +}); + +// Custom variable kind +export interface CustomVariableKind { + kind: "CustomVariable"; + spec: CustomVariableSpec; +} + +export const defaultCustomVariableKind = (): CustomVariableKind => ({ + kind: "CustomVariable", + spec: defaultCustomVariableSpec(), +}); + +// Custom variable specification +export interface CustomVariableSpec { + name: string; + query: string; + current: VariableOption; + options: VariableOption[]; + multi: boolean; + includeAll: boolean; + allValue?: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; + allowCustomValue: boolean; +} + +export const defaultCustomVariableSpec = (): CustomVariableSpec => ({ + name: "", + query: "", + current: defaultVariableOption(), + options: [], + multi: false, + includeAll: false, + hide: "dontHide", + skipUrlSync: false, + allowCustomValue: true, +}); + +// Group variable kind +export interface GroupByVariableKind { + kind: "GroupByVariable"; + spec: GroupByVariableSpec; +} + +export const defaultGroupByVariableKind = (): GroupByVariableKind => ({ + kind: "GroupByVariable", + spec: defaultGroupByVariableSpec(), +}); + +// GroupBy variable specification +export interface GroupByVariableSpec { + name: string; + datasource?: DataSourceRef; + defaultValue?: VariableOption; + current: VariableOption; + options: VariableOption[]; + multi: boolean; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({ + name: "", + current: { text: "", value: "", }, + options: [], + multi: false, + hide: "dontHide", + skipUrlSync: false, +}); + +// Adhoc variable kind +export interface AdhocVariableKind { + kind: "AdhocVariable"; + spec: AdhocVariableSpec; +} + +export const defaultAdhocVariableKind = (): AdhocVariableKind => ({ + kind: "AdhocVariable", + spec: defaultAdhocVariableSpec(), +}); + +// Adhoc variable specification +export interface AdhocVariableSpec { + name: string; + datasource?: DataSourceRef; + baseFilters: AdHocFilterWithLabels[]; + filters: AdHocFilterWithLabels[]; + defaultKeys: MetricFindValue[]; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; + allowCustomValue: boolean; +} + +export const defaultAdhocVariableSpec = (): AdhocVariableSpec => ({ + name: "", + baseFilters: [], + filters: [], + defaultKeys: [], + hide: "dontHide", + skipUrlSync: false, + allowCustomValue: true, +}); + +// Define the AdHocFilterWithLabels type +export interface AdHocFilterWithLabels { + key: string; + operator: string; + value: string; + values?: string[]; + keyLabel?: string; + valueLabels?: string[]; + forceEdit?: boolean; + origin?: "dashboard"; + // @deprecated + condition?: string; +} + +export const defaultAdHocFilterWithLabels = (): AdHocFilterWithLabels => ({ + key: "", + operator: "", + value: "", +}); + +// Determine the origin of the adhoc variable filter +export const FilterOrigin = "dashboard"; + +// Define the MetricFindValue type +export interface MetricFindValue { + text: string; + value?: string | number; + group?: string; + expandable?: boolean; +} + +export const defaultMetricFindValue = (): MetricFindValue => ({ + text: "", +}); + +export interface Spec { + annotations: AnnotationQueryKind[]; + // Configuration of dashboard cursor sync behavior. + // "Off" for no shared crosshair or tooltip (default). + // "Crosshair" for shared crosshair. + // "Tooltip" for shared crosshair AND shared tooltip. + cursorSync: DashboardCursorSync; + // Description of dashboard. + description?: string; + // Whether a dashboard is editable or not. + editable?: boolean; + elements: Record; + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind; + // Links with references to other dashboards or external websites. + links: DashboardLink[]; + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data. + liveNow?: boolean; + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + preload: boolean; + // Plugins only. The version of the dashboard installed together with the plugin. + // This is used to determine if the dashboard should be updated when the plugin is updated. + revision?: number; + // Tags associated with dashboard. + tags: string[]; + timeSettings: TimeSettingsSpec; + // Title of dashboard. + title: string; + // Configured template variables. + variables: VariableKind[]; +} + +export const defaultSpec = (): Spec => ({ + annotations: [], + cursorSync: "Off", + editable: true, + elements: {}, + layout: defaultGridLayoutKind(), + links: [], + preload: false, + tags: [], + timeSettings: defaultTimeSettingsSpec(), + title: "", + variables: [], +}); + diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.status.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.status.gen.ts new file mode 100644 index 00000000000..29494dfaf24 --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha2/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// ConversionStatus is the status of the conversion of the dashboard. +export interface ConversionStatus { + // Whether from another version has failed. + // If true, means that the dashboard is not valid, + // and the caller should instead fetch the stored version. + failed: boolean; + // The version which was stored when the dashboard was created / updated. + // Fetching this version should always succeed. + storedVersion: string; + // The error message from the conversion. + // Empty if the conversion has not failed. + error: string; +} + +export const defaultConversionStatus = (): ConversionStatus => ({ + failed: false, + storedVersion: "", + error: "", +}); + +export interface Status { + // Optional conversion status. + conversion?: ConversionStatus; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/packages/grafana-schema/src/veneer/common.types.ts b/packages/grafana-schema/src/veneer/common.types.ts index 8a73c1149f9..fb3bfe18c57 100644 --- a/packages/grafana-schema/src/veneer/common.types.ts +++ b/packages/grafana-schema/src/veneer/common.types.ts @@ -41,6 +41,7 @@ export * from '../common/common.gen'; export const defaultTableFieldOptions: raw.TableFieldOptions = { align: 'auto', inspect: false, + wrapHeaderText: false, cellOptions: { type: raw.TableCellDisplayMode.Auto, }, diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 4b241f727a9..2068a48f9de 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -27,9 +27,9 @@ "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", - "react-virtualized-auto-sizer": "1.0.25", + "react-virtualized-auto-sizer": "1.0.26", "rxjs": "7.8.2", "sql-formatter-plus": "^1.3.6", "tslib": "2.8.1", @@ -42,18 +42,18 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "^29.5.4", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "@types/react-virtualized-auto-sizer": "1.0.4", - "@types/systemjs": "6.15.1", + "@types/react-virtualized-auto-sizer": "1.0.8", + "@types/systemjs": "6.15.3", "@types/uuid": "10.0.0", "i18next-parser": "9.3.0", "jest": "^29.6.4", "ts-jest": "29.2.5", "ts-node": "10.9.2", - "typescript": "5.7.3" + "typescript": "5.8.3" }, "peerDependencies": { "@grafana/runtime": "10.4.0-pre" diff --git a/packages/grafana-sql/src/components/QueryHeader.tsx b/packages/grafana-sql/src/components/QueryHeader.tsx index de27061e390..240b73a3c27 100644 --- a/packages/grafana-sql/src/components/QueryHeader.tsx +++ b/packages/grafana-sql/src/components/QueryHeader.tsx @@ -28,11 +28,6 @@ export interface QueryHeaderProps { queryRowFilter: QueryRowFilter; } -const editorModes = [ - { label: 'Builder', value: EditorMode.Builder }, - { label: 'Code', value: EditorMode.Code }, -]; - export function QueryHeader({ db, dialect, @@ -51,6 +46,14 @@ export function QueryHeader({ const htmlId = useId(); + const editorModes = [ + { + label: t('grafana-sql.components.query-header.editor-modes.label-builder', 'Builder'), + value: EditorMode.Builder, + }, + { label: t('grafana-sql.components.query-header.editor-modes.label-code', 'Code'), value: EditorMode.Code }, + ]; + const onEditorModeChange = useCallback( (newEditorMode: EditorMode) => { if (newEditorMode === EditorMode.Code) { diff --git a/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json b/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json index 52ab74544e9..ee03668f7c7 100644 --- a/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json +++ b/packages/grafana-sql/src/locales/cs-CZ/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtrovat", "label-format": "Formát", diff --git a/packages/grafana-sql/src/locales/de-DE/grafana-sql.json b/packages/grafana-sql/src/locales/de-DE/grafana-sql.json index 895927b7c1f..5f921f12dba 100644 --- a/packages/grafana-sql/src/locales/de-DE/grafana-sql.json +++ b/packages/grafana-sql/src/locales/de-DE/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filter", "label-format": "Format", diff --git a/packages/grafana-sql/src/locales/en-US/grafana-sql.json b/packages/grafana-sql/src/locales/en-US/grafana-sql.json index d9cb9d1fa61..bc986568f18 100644 --- a/packages/grafana-sql/src/locales/en-US/grafana-sql.json +++ b/packages/grafana-sql/src/locales/en-US/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "Your query is invalid. Check below for details. <1>However, you can still run this query.", + "editor-modes": { + "label-builder": "Builder", + "label-code": "Code" + }, "label-dataset": "Dataset", "label-filter": "Filter", "label-format": "Format", diff --git a/packages/grafana-sql/src/locales/es-ES/grafana-sql.json b/packages/grafana-sql/src/locales/es-ES/grafana-sql.json index fbfe5c8bbfa..6fcd133d0fa 100644 --- a/packages/grafana-sql/src/locales/es-ES/grafana-sql.json +++ b/packages/grafana-sql/src/locales/es-ES/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtro", "label-format": "Formato", diff --git a/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json b/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json index ec35e126c7d..c970cbbd845 100644 --- a/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/fr-FR/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtrer", "label-format": "Format", diff --git a/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json b/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json index f0d9ad1eed5..0c1803084cb 100644 --- a/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json +++ b/packages/grafana-sql/src/locales/hu-HU/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Szűrő", "label-format": "Formátum", diff --git a/packages/grafana-sql/src/locales/id-ID/grafana-sql.json b/packages/grafana-sql/src/locales/id-ID/grafana-sql.json index 5733382d722..e6c977bca22 100644 --- a/packages/grafana-sql/src/locales/id-ID/grafana-sql.json +++ b/packages/grafana-sql/src/locales/id-ID/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filter", "label-format": "Format", diff --git a/packages/grafana-sql/src/locales/it-IT/grafana-sql.json b/packages/grafana-sql/src/locales/it-IT/grafana-sql.json index c155ef2a4dc..b7194050d6f 100644 --- a/packages/grafana-sql/src/locales/it-IT/grafana-sql.json +++ b/packages/grafana-sql/src/locales/it-IT/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtro", "label-format": "Formato", diff --git a/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json b/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json index 4c74bb5be58..4340b874c6e 100644 --- a/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json +++ b/packages/grafana-sql/src/locales/ja-JP/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "フィルタリング", "label-format": "形式", diff --git a/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json b/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json index f9778c0e23c..ab01d9e61a0 100644 --- a/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/ko-KR/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "필터", "label-format": "형식", diff --git a/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json b/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json index f1e27ce05a5..2f7d4aed633 100644 --- a/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json +++ b/packages/grafana-sql/src/locales/nl-NL/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filter", "label-format": "Formaat", diff --git a/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json b/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json index 0752f5afddc..47e1140a92a 100644 --- a/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json +++ b/packages/grafana-sql/src/locales/pl-PL/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtr", "label-format": "Format", diff --git a/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json b/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json index a0f9d14750b..8bb192f0a01 100644 --- a/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/pt-BR/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtro", "label-format": "Formato", diff --git a/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json b/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json index 8561b05941b..4a5f9824264 100644 --- a/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json +++ b/packages/grafana-sql/src/locales/pt-PT/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtro", "label-format": "Formato", diff --git a/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json b/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json index 2e0134da629..12440aa0ddc 100644 --- a/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json +++ b/packages/grafana-sql/src/locales/ru-RU/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Фильтр", "label-format": "Формат", diff --git a/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json b/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json index 0d7dbeaf9a2..4dd442d92a4 100644 --- a/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json +++ b/packages/grafana-sql/src/locales/sv-SE/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtrera", "label-format": "Format", diff --git a/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json b/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json index da83339c08f..06248f27ef1 100644 --- a/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json +++ b/packages/grafana-sql/src/locales/tr-TR/grafana-sql.json @@ -55,6 +55,10 @@ }, "query-header": { "content-invalid-query": "", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, "label-dataset": "", "label-filter": "Filtreleyin", "label-format": "Biçim", diff --git a/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json b/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json index d22700fa3ac..45f714b1637 100644 --- a/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json +++ b/packages/grafana-sql/src/locales/zh-Hans/grafana-sql.json @@ -2,26 +2,26 @@ "grafana-sql": { "components": { "confirm-modal": { - "builder-mode": "", + "builder-mode": "生成器模式不显示代码中所做的更改。查询生成器将显示您在生成器模式下所做的最后一次更改。", "cancel": "取消", - "clipboard": "", - "copy-code-and-switch": "", - "discard-code-and-switch": "", + "clipboard": "您想将代码复制到剪贴板吗?", + "copy-code-and-switch": "复制代码并切换", + "discard-code-and-switch": "丢弃代码并切换", "warning": "警告" }, "connection-limits": { - "auto-max-idle": "", - "content-auto-max-idle": "", - "content-max-idle": "", - "content-max-lifetime": "", - "content-max-open": "", - "max-idle": "", - "max-lifetime": "", - "max-open": "", - "title-connection-limits": "" + "auto-max-idle": "自动最大空闲", + "content-auto-max-idle": "如果启用,则自动将<1>最大空闲连接数的数字设置为与<3>最大打开连接数相同的值。如果未设置最大打开连接数的数字,则将其设置为默认值 ({{defaultMaxIdle}})。", + "content-max-idle": "空闲连接池中的最大连接数。如果<1>最大打开连接数大于 0 但小于<3>最大空闲连接数,则<5>最大空闲连接数将减少以匹配<8>最大打开连接数限制。如果设置为 0,则不保留任何空闲连接。", + "content-max-lifetime": "连接可以重复使用的最长时间(秒)。如果设置为 0,则连接将永久重复使用。", + "content-max-open": "数据库的最大打开连接数。如果<1>最大空闲连接数大于 0 且<3>最大打开连接数小于<5>最大空闲连接数,则<7>最大空闲连接数将减少以匹配<9>最大打开连接数限制。如果设置为 0,则对打开的连接数没有限制。", + "max-idle": "最大空闲", + "max-lifetime": "最大生命周期", + "max-open": "最大打开", + "title-connection-limits": "连接限制" }, "dataset-selector": { - "aria-label-dataset-selector": "" + "aria-label-dataset-selector": "数据集选择器" }, "error-boundary": { "fall-back": { @@ -31,21 +31,21 @@ "get-custom-operators": { "custom-operators": { "label": { - "macros": "" + "macros": "宏指令" } } }, "make-render-column": { "render-column": { "aria-label-group-by": "分组依据", - "title-remove-group-by-column": "" + "title-remove-group-by-column": "按列移除组" } }, "order-by-row": { - "aria-label-order-by": "", + "aria-label-order-by": "排序条件", "label-limit": "限制", - "label-offset": "", - "label-order-by": "" + "label-offset": "偏移", + "label-order-by": "排序条件" }, "preview": { "label-element": { @@ -54,96 +54,100 @@ } }, "query-header": { - "content-invalid-query": "", - "label-dataset": "", + "content-invalid-query": "您的查询无效。请查看下方详情。<1>但是,您仍然可以运行此查询。", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, + "label-dataset": "数据集", "label-filter": "筛选条件", "label-format": "格式", "label-group": "组", - "label-order": "", + "label-order": "顺序", "label-preview": "预览", "label-table": "表格", - "placeholder-select-format": "", + "placeholder-select-format": "选择格式", "run-query": "运行查询" }, "query-toolbox": { - "content-hit-ctrlcmdreturn-to-run-query": "", - "tooltip-collapse": "", - "tooltip-expand": "", - "tooltip-format-query": "" + "content-hit-ctrlcmdreturn-to-run-query": "按 CTRL/CMD+Return 运行查询", + "tooltip-collapse": "折叠编辑器", + "tooltip-expand": "展开编辑器", + "tooltip-format-query": "格式化查询" }, "query-validator": { - "query-will-process": "", - "validating-query": "" + "query-will-process": "<0> 此查询将在运行时处理 <2>{{bytes}}。", + "validating-query": "正在验证查询..." }, "raw-editor": { "render-placeholder": { - "editing-in-expanded-code-editor": "" + "editing-in-expanded-code-editor": "在展开的代码编辑器中编辑" }, - "title-query-num": "" + "title-query-num": "查询 {{queryNum}}" }, "select-column": { "label-column": "列" }, "select-custom-function-parameters": { - "aria-label-parameter": "", + "aria-label-parameter": "{{columnIndex}} 列的参数 {{index}}", "render-parameters": { "params": { - "title-remove-parameter": "" + "title-remove-parameter": "移除参数" } }, - "title-add-parameter": "" + "title-add-parameter": "添加参数" }, "select-row": { "aggregate-options": { "options": { "label": { - "aggregations": "", - "macros": "" + "aggregations": "聚合", + "macros": "宏指令" } } }, "label": { - "time": "", + "time": "时间", "value": "值" }, "label-alias": "别名", - "label-data-operations": "", - "title-add-column": "", - "title-remove-column": "" + "label-data-operations": "数据操作", + "title-add-column": "添加列", + "title-remove-column": "移除列" }, "settings": { - "aria-label-conjunction": "", + "aria-label-conjunction": "连词", "aria-label-field": "字段", "aria-label-operator": "运算符", - "title-button-filter": "" + "title-button-filter": "{{ buttonLabel }}筛选器" }, "sql-query-editor-lazy": { "text-loading-editor": "正在加载编辑器" }, "table-selector": { - "aria-label-table-selector": "", - "placeholder-loading": "", - "placeholder-select-table": "" + "aria-label-table-selector": "表选择器", + "placeholder-loading": "正在加载表格", + "placeholder-select-table": "选择表格" }, "tlssecrets-config": { - "content-tlsssl-client-certificate": "", - "content-tlsssl-client-key": "", - "content-tlsssl-root-certificate": "", - "tlsssl-client-certificate": "", - "tlsssl-client-key": "", + "content-tlsssl-client-certificate": "要使用 TLS/SSL 客户端证书进行身份验证,请在此处提供客户端证书。", + "content-tlsssl-client-key": "要使用客户端 TLS/SSL 证书进行身份验证,请在此处提供密钥。", + "content-tlsssl-root-certificate": "如果所选的 TLS/SSL 模式需要服务器根证书,请在此处提供", + "tlsssl-client-certificate": "TLS/SSL 客户端证书", + "tlsssl-client-key": "TLS 客户端密钥", "tlsssl-root-certificate": "TLS/SSL 根证书" }, "visual-editor": { - "label-filter-by-column-value": "", - "label-group-by-column": "" + "label-filter-by-column-value": "按列值筛选", + "label-group-by-column": "按列分组" }, "widgets": { - "aria-label-macros-value-selector": "" + "aria-label-macros-value-selector": "宏值选择器" } }, "utils": { "get-columns-width-indices": { - "label-selected-columns": "" + "label-selected-columns": "选定的列" } } } diff --git a/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json b/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json index 87c106d7b4d..d7acf261c24 100644 --- a/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json +++ b/packages/grafana-sql/src/locales/zh-Hant/grafana-sql.json @@ -2,26 +2,26 @@ "grafana-sql": { "components": { "confirm-modal": { - "builder-mode": "", + "builder-mode": "編輯模式不會顯示在程式碼中的變更。查詢編輯器將顯示您在編輯器模式下所做的最後變更。", "cancel": "取消", - "clipboard": "", - "copy-code-and-switch": "", - "discard-code-and-switch": "", + "clipboard": "要將程式碼複製到剪貼簿嗎?", + "copy-code-and-switch": "複製程式碼並切換", + "discard-code-and-switch": "捨棄程式碼並切換", "warning": "警告" }, "connection-limits": { - "auto-max-idle": "", - "content-auto-max-idle": "", - "content-max-idle": "", - "content-max-lifetime": "", - "content-max-open": "", - "max-idle": "", - "max-lifetime": "", - "max-open": "", - "title-connection-limits": "" + "auto-max-idle": "自動最大閒置時間", + "content-auto-max-idle": "若啟用,<1>最大閒置連線數的數量將會自動設定為與<3>最大開啟連線數相同的數值。如果未設定最大開啟連線數,則將設定為預設值 ({{defaultMaxIdle}})。", + "content-max-idle": "閒置連線池中的最大連線數。若<1>最大開啟連線數大於 0,但小於 <3>最大閒置連線數,則<5>最大閒置連線數將會縮減,以符合<8>最大開啟連線數限制。若設定為 0,則不會保留任何閒置連線。", + "content-max-lifetime": "可以重複使用連線的最長時間(秒)。若設定為 0,則會永遠重複使用連線。", + "content-max-open": "資料庫的最大開啟連線數。若<1>最大閒置連線數大於 0,且<3>最大開啟連線數小於<5>最大閒置連線數,則<7>最大閒置連線數將會縮減,以符合<9>最大開啟連線數限制。若設定為 0,則不會限制開啟連線數。", + "max-idle": "最大閒置", + "max-lifetime": "最大存活期", + "max-open": "最大開啟數量", + "title-connection-limits": "連線限制" }, "dataset-selector": { - "aria-label-dataset-selector": "" + "aria-label-dataset-selector": "資料集選擇器" }, "error-boundary": { "fall-back": { @@ -31,21 +31,21 @@ "get-custom-operators": { "custom-operators": { "label": { - "macros": "" + "macros": "巨集" } } }, "make-render-column": { "render-column": { "aria-label-group-by": "分組依據", - "title-remove-group-by-column": "" + "title-remove-group-by-column": "按欄位移除群組" } }, "order-by-row": { - "aria-label-order-by": "", + "aria-label-order-by": "排序方式", "label-limit": "限制", - "label-offset": "", - "label-order-by": "" + "label-offset": "位移量", + "label-order-by": "排序方式" }, "preview": { "label-element": { @@ -54,96 +54,100 @@ } }, "query-header": { - "content-invalid-query": "", - "label-dataset": "", + "content-invalid-query": "您的查詢無效。請參閱下方詳細資訊。<1>不過,您仍然可以執行此查詢。", + "editor-modes": { + "label-builder": "", + "label-code": "" + }, + "label-dataset": "資料集", "label-filter": "篩選", "label-format": "格式", "label-group": "群組", - "label-order": "", + "label-order": "排序", "label-preview": "預覽", "label-table": "表格", - "placeholder-select-format": "", + "placeholder-select-format": "選擇格式", "run-query": "執行查詢" }, "query-toolbox": { - "content-hit-ctrlcmdreturn-to-run-query": "", - "tooltip-collapse": "", - "tooltip-expand": "", - "tooltip-format-query": "" + "content-hit-ctrlcmdreturn-to-run-query": "按下 CTRL/CMD + Return 執行查詢", + "tooltip-collapse": "收合編輯器", + "tooltip-expand": "展開編輯器", + "tooltip-format-query": "格式化查詢" }, "query-validator": { - "query-will-process": "", - "validating-query": "" + "query-will-process": "<0>此查詢將在執行時處理 <2>{{bytes}}。", + "validating-query": "正在驗證查詢…" }, "raw-editor": { "render-placeholder": { - "editing-in-expanded-code-editor": "" + "editing-in-expanded-code-editor": "在展開的程式碼編輯器中編輯" }, - "title-query-num": "" + "title-query-num": "查詢 {{queryNum}}" }, "select-column": { "label-column": "欄位" }, "select-custom-function-parameters": { - "aria-label-parameter": "", + "aria-label-parameter": "欄位 {{columnIndex}} 的參數 {{index}}", "render-parameters": { "params": { - "title-remove-parameter": "" + "title-remove-parameter": "移除參數" } }, - "title-add-parameter": "" + "title-add-parameter": "新增參數" }, "select-row": { "aggregate-options": { "options": { "label": { - "aggregations": "", - "macros": "" + "aggregations": "聚合", + "macros": "巨集" } } }, "label": { - "time": "", + "time": "時間", "value": "值" }, "label-alias": "別名", - "label-data-operations": "", - "title-add-column": "", - "title-remove-column": "" + "label-data-operations": "資料操作", + "title-add-column": "新增欄位", + "title-remove-column": "移除欄位" }, "settings": { - "aria-label-conjunction": "", + "aria-label-conjunction": "連接條件", "aria-label-field": "欄位", "aria-label-operator": "運算子", - "title-button-filter": "" + "title-button-filter": "{{ buttonLabel }} 篩選條件" }, "sql-query-editor-lazy": { "text-loading-editor": "正在載入編輯器" }, "table-selector": { - "aria-label-table-selector": "", - "placeholder-loading": "", - "placeholder-select-table": "" + "aria-label-table-selector": "表格選擇器", + "placeholder-loading": "載入表格…", + "placeholder-select-table": "選擇表格" }, "tlssecrets-config": { - "content-tlsssl-client-certificate": "", - "content-tlsssl-client-key": "", - "content-tlsssl-root-certificate": "", - "tlsssl-client-certificate": "", - "tlsssl-client-key": "", + "content-tlsssl-client-certificate": "若要使用 TLS/SSL 用戶端憑證進行驗證,請在此提供用戶端憑證。", + "content-tlsssl-client-key": "若要使用用戶端 TLS/SSL 憑證進行驗證,請在此提供金鑰。", + "content-tlsssl-root-certificate": "若所選的 TLS/SSL 模式需要伺服器根憑證,請在此提供", + "tlsssl-client-certificate": "TLS/SSL 用戶端憑證", + "tlsssl-client-key": "TLS/SSL 用戶端金鑰", "tlsssl-root-certificate": "TLS/SSL 根憑證" }, "visual-editor": { - "label-filter-by-column-value": "", - "label-group-by-column": "" + "label-filter-by-column-value": "按欄位數值篩選", + "label-group-by-column": "按欄位分組" }, "widgets": { - "aria-label-macros-value-selector": "" + "aria-label-macros-value-selector": "巨集值選擇器" } }, "utils": { "get-columns-width-indices": { - "label-selected-columns": "" + "label-selected-columns": "選取的欄位" } } } diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index a74497f16f8..6df13ad831e 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -46,11 +46,11 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "dependencies": { - "msw": "2.10.2" + "msw": "2.10.4" }, "devDependencies": { "@grafana/tsconfig": "^2.0.0", "@types/node": "22.15.0", - "typescript": "5.7.3" + "typescript": "5.8.3" } } diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts index 1c75daef688..29258c4a228 100644 --- a/packages/grafana-ui/.storybook/main.ts +++ b/packages/grafana-ui/.storybook/main.ts @@ -2,13 +2,7 @@ import path, { dirname, join } from 'node:path'; import type { StorybookConfig } from '@storybook/react-webpack5'; import { copyAssetsSync } from './copyAssets'; -// Internal stories should only be visible during development -const coreComponentsGlobs: StorybookConfig['stories'] = [ - '../src/Intro.mdx', - process.env.NODE_ENV === 'production' - ? '../src/components/**/!(*.internal).story.tsx' - : '../src/components/**/*.story.tsx', -]; +const coreComponentsGlobs: StorybookConfig['stories'] = ['../src/Intro.mdx', '../src/**/*.story.tsx']; const alertingComponentsGlobs: StorybookConfig['stories'] = [ { diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index 41311f34244..803d45315e3 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -52,6 +52,7 @@ if (process.env.NODE_ENV === 'development') { initialize({ onUnhandledRequest: 'bypass', serviceWorker: { + // Important! The path must be relative to work when we deploy storybook to subpaths (e.g. /ui/canary) url: 'mockServiceWorker.js', }, }); @@ -70,20 +71,84 @@ const preview: Preview = { // Sort stories first by Docs Overview, then alphabetically // We should be able to use the builtin alphabetical sort, but is broken in SB 7.0 // https://github.com/storybookjs/storybook/issues/22470 + + // Story sorting is weird - All stories are sorted as a single 1D list, but then grouped in the UI. + // Story titles are generally in the format of [Category]/[Component]/[Story]. However, some categories + // will have an additional `Deprecated` sub folder before the [Component] + // + // We want to have multi-level sorting where: + // - The top level category has an explicit order + // - Components are sorted alphabetically within their category + // - Except the Deprecated folder, which is sorted to the bottom + // - Stories per component use the default file sort order storySort: (a, b) => { - // Skip sorting for stories with nosort tag - if (a.tags.includes('nosort') || b.tags.includes('nosort')) { - return 0; + const CATEGORY_ORDER = [ + // Should all be lowercase + 'docs overview', + 'foundations', + 'iconography', + 'layout', + + 'forms', + 'inputs', + 'pickers', + 'date time pickers', + + 'information', + 'overlays', + 'utilities', + 'navigation', + + 'plugins', + 'alerting', + 'developers', + ]; + + const aTitle = a.title.toLowerCase(); + const bTitle = b.title.toLowerCase(); + const [aCategory, aComponent] = aTitle.split('/'); + const [bCategory, bComponent] = bTitle.split('/'); + + // + // Sort by category order first + const aCategoryIndex = CATEGORY_ORDER.indexOf(aCategory); + const bCategoryIndex = CATEGORY_ORDER.indexOf(bCategory); + + if (aCategoryIndex === -1 || bCategoryIndex === -1) { + const category = aCategoryIndex === -1 ? aCategory : bCategory; + throw new Error( + `Category ${category} not found in CATEGORY_ORDER. Prefer reusing the existing categories, or add to CATEGORY_ORDER.` + ); } - if (a.title.startsWith('Docs Overview')) { - if (b.title.startsWith('Docs Overview')) { - return 0; - } + + if (aCategoryIndex !== bCategoryIndex) { + return aCategoryIndex - bCategoryIndex; + } + + // + // Sort 'Deprecated' subfolders to the bottom + if (aTitle.includes('deprecated') && !bTitle.includes('deprecated')) { + return 1; + } else if (bTitle.includes('deprecated') && !aTitle.includes('deprecated')) { return -1; - } else if (b.title.startsWith('Docs Overview')) { + } + + // + // Sort Docs to the top + if (a.type === 'docs' && b.type !== 'docs') { + return -1; + } else if (a.type !== 'docs' && b.type === 'docs') { return 1; } - return a.id === b.id ? 0 : a.id.localeCompare(b.id, undefined, { numeric: true }); + + // + // If sorting different components, sort alphabetically + if (aComponent !== bComponent) { + return aComponent.localeCompare(bComponent, undefined, { numeric: true }); + } + + // Otherwise, sort stories within componmments according to source order + return 0; }, }, }, diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 443b8cc01ae..61f0d9a0d15 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -65,7 +65,7 @@ "@emotion/css": "11.13.5", "@emotion/react": "11.14.0", "@emotion/serialize": "1.3.3", - "@floating-ui/react": "0.27.12", + "@floating-ui/react": "0.27.13", "@grafana/data": "12.1.0-pre", "@grafana/e2e-selectors": "12.1.0-pre", "@grafana/faro-web-sdk": "^1.13.2", @@ -80,15 +80,16 @@ "@react-aria/utils": "3.29.1", "@tanstack/react-virtual": "^3.5.1", "@types/jquery": "3.5.32", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/react-table": "7.7.20", "calculate-size": "1.1.1", "classnames": "2.5.1", + "clsx": "^2.1.1", "d3": "7.9.0", "date-fns": "4.1.0", "downshift": "^9.0.6", "hoist-non-react-statics": "3.3.2", - "i18next": "^24.0.0", + "i18next": "^25.0.0", "i18next-browser-languagedetector": "^8.0.0", "immutable": "5.1.3", "is-hotkey": "0.2.0", @@ -97,14 +98,14 @@ "micro-memoize": "^4.1.2", "moment": "2.30.1", "monaco-editor": "0.34.1", - "ol": "7.4.0", + "ol": "10.6.0", "prismjs": "1.30.0", "rc-cascader": "3.34.0", "rc-drawer": "7.3.0", "rc-picker": "4.11.3", "rc-slider": "11.1.8", "rc-tooltip": "6.4.0", - "react-calendar": "^5.1.0", + "react-calendar": "^6.0.0", "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", "react-data-grid": "grafana/react-data-grid#de920f0105cb2b7d774444e7443a675f3b568ad6", @@ -116,7 +117,7 @@ "react-loading-skeleton": "3.5.0", "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-table": "7.8.0", "react-transition-group": "4.4.5", "react-use": "17.6.0", @@ -135,7 +136,7 @@ "@babel/core": "7.26.10", "@faker-js/faker": "^9.0.0", "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/plugin-node-resolve": "16.0.1", "@storybook/addon-a11y": "^8.6.2", "@storybook/addon-actions": "^8.6.2", "@storybook/addon-docs": "^8.6.2", @@ -156,7 +157,7 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", - "@types/chance": "1.1.6", + "@types/chance": "^1.1.7", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.3", "@types/hoist-non-react-statics": "3.3.6", @@ -176,13 +177,13 @@ "@types/slate-react": "0.22.9", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", - "chance": "1.1.12", + "chance": "^1.1.13", "common-tags": "1.8.2", "core-js": "3.40.0", "css-loader": "7.1.2", "csstype": "3.1.3", - "esbuild": "0.25.0", - "expose-loader": "5.0.0", + "esbuild": "0.25.6", + "expose-loader": "5.0.1", "fs-extra": "^11.2.0", "mock-raf": "1.0.1", "msw": "^2.10.2", @@ -200,7 +201,7 @@ "sass-loader": "16.0.4", "storybook": "^8.6.2", "style-loader": "4.0.0", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "peerDependencies": { diff --git a/packages/grafana-ui/src/components/Alert/InlineBanner.story.tsx b/packages/grafana-ui/src/components/Alert/Alert.story.tsx similarity index 87% rename from packages/grafana-ui/src/components/Alert/InlineBanner.story.tsx rename to packages/grafana-ui/src/components/Alert/Alert.story.tsx index 61d573d0f1f..2c5ffba4f06 100644 --- a/packages/grafana-ui/src/components/Alert/InlineBanner.story.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.story.tsx @@ -10,7 +10,7 @@ import mdx from './Alert.mdx'; const severities: AlertVariant[] = ['error', 'warning', 'info', 'success']; const meta: Meta = { - title: 'Overlays/Alert/InlineBanner', + title: 'Information/Alert', component: Alert, parameters: { docs: { @@ -78,4 +78,15 @@ export const Examples: StoryFn = () => { ); }; +export const Toast: StoryFn = (args) => { + return To use as a toast, set the elevated and onRemove props.; +}; + +Toast.args = { + title: 'Toast', + severity: 'error', + onRemove: action('Remove button clicked'), + elevated: true, +}; + export default meta; diff --git a/packages/grafana-ui/src/components/Alert/Toast.story.tsx b/packages/grafana-ui/src/components/Alert/Toast.story.tsx deleted file mode 100644 index 12991adcb03..00000000000 --- a/packages/grafana-ui/src/components/Alert/Toast.story.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { StoryFn, Meta } from '@storybook/react'; - -import { StoryExample } from '../../utils/storybook/StoryExample'; -import { Stack } from '../Layout/Stack/Stack'; - -import { Alert, AlertVariant } from './Alert'; -import mdx from './Alert.mdx'; - -const severities: AlertVariant[] = ['error', 'warning', 'info', 'success']; - -const meta: Meta = { - title: 'Overlays/Alert/Toast', - component: Alert, - parameters: { - docs: { - page: mdx, - }, - controls: { exclude: ['onRemove'] }, - }, - argTypes: { - severity: { control: { type: 'select', options: severities } }, - }, - args: { - title: 'Toast', - severity: 'error', - onRemove: action('Remove button clicked'), - }, -}; - -export const Basic: StoryFn = (args) => { - return ( - - Child content that includes some alert details, like maybe what actually happened. - - ); -}; - -export function Examples() { - return ( - - - - {severities.map((severity) => ( - - ))} - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam metus urna, aliquam eu scelerisque non, - facilisis eget est. Morbi eleifend egestas massa id vulputate. Fusce dignissim magna lacus, ut molestie odio - feugiat sed. Cras fringilla justo sit amet turpis scelerisque, a volutpat purus iaculis. Nunc sagittis - molestie faucibus. Curabitur at neque luctus, pellentesque urna eget, posuere urna. Nunc malesuada elit in - ipsum dictum egestas. Praesent convallis mauris massa, porta mattis ex gravida ut. Proin consectetur ultrices - tortor sit amet efficitur. Suspendisse nec turpis dapibus mauris venenatis maximus quis eget orci. Ut semper - enim magna, ullamcorper elementum sapien pharetra vitae. Vivamus at nulla ut metus bibendum ornare et ut leo. - Proin ante turpis, ornare a malesuada et, rutrum nec lorem. Maecenas vestibulum orci vel nibh convallis - eleifend. Quisque vitae consectetur massa, vitae elementum mauris. Pellentesque sit amet ligula lorem. Fusce - sit amet lorem non augue rutrum varius. Donec sed imperdiet libero, eget venenatis elit. Fusce porttitor - dapibus urna. Duis fringilla ante vel tempor tincidunt. In euismod vestibulum odio sit amet iaculis. Donec vel - dapibus libero. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi lacinia commodo lectus. Aenean - in magna eget lectus luctus suscipit et vitae erat. Pellentesque quis ligula id lorem egestas sollicitudin sit - amet sed sem. Nullam et nibh a odio rhoncus efficitur sed nec est. Sed commodo lacus vitae sem congue, - accumsan dignissim metus iaculis. Praesent in dignissim nisl. Aliquam facilisis, sapien eget porttitor - ultrices, massa libero bibendum odio, at ornare diam arcu ac massa. Vestibulum egestas leo eget lorem congue - condimentum. Praesent egestas, neque id gravida vehicula, augue ex scelerisque lectus, finibus pellentesque - enim dolor vel ante. Cras convallis, sem at malesuada tincidunt, diam urna auctor leo, sed laoreet est ex in - libero. Ut condimentum ante eget ex gravida, id tempus metus ultricies. Pellentesque placerat, massa id - laoreet molestie, justo nisl varius metus, maximus vehicula erat libero vitae nulla. Mauris rhoncus ligula - vitae volutpat auctor. Suspendisse potenti. Quisque quis orci faucibus, ullamcorper dolor eget, mollis massa. - Etiam eu molestie ipsum. Sed laoreet diam metus, luctus maximus erat viverra quis. Ut eu felis dictum, - tincidunt erat sit amet, scelerisque neque. Orci varius natoque penatibus et magnis dis parturient montes, - nascetur ridiculus mus. Phasellus sit amet est tristique, fermentum massa ut, viverra metus. Interdum et - malesuada fames ac ante ipsum primis in faucibus. Nunc iaculis nunc elit, ut feugiat ipsum egestas eget. - Vestibulum pulvinar ligula mi, quis lacinia diam suscipit eget. Etiam consectetur vel nunc at hendrerit. - Pellentesque blandit eleifend aliquam. Etiam et malesuada purus, et bibendum sapien. Phasellus tincidunt - consequat eros consequat sodales. Vestibulum quis viverra neque. Integer sit amet lacinia nunc. Ut cursus, - elit id faucibus elementum, elit nunc dapibus tellus, non ornare nisi sapien et eros. Nunc sit amet suscipit - arcu. Nulla ut nunc tempor, auctor massa sed, consectetur orci. Pellentesque erat ante, placerat eget dictum - elementum, dapibus et ipsum. Nunc sit amet nulla gravida, finibus felis vel, tempus sem. In urna purus, - accumsan quis aliquam et, condimentum ac urna. Nullam volutpat ullamcorper sapien, quis ultricies purus - dignissim aliquam. Mauris quis enim ante. Etiam vulputate faucibus placerat. Ut pellentesque, purus vitae - euismod cursus, lacus enim vulputate sapien, in porttitor erat dui eu lectus. Duis eleifend, massa vel - vehicula gravida, magna urna rutrum ligula, vitae mollis ipsum neque id enim. Donec varius tristique nisi, et - vestibulum dolor efficitur eget. Cras mauris leo, bibendum eget pretium a, tincidunt faucibus massa. - Vestibulum hendrerit arcu magna, vel consequat est euismod nec. Vestibulum non lacus porttitor, congue tortor - ut, venenatis elit. Duis at lectus arcu. Nunc quis sapien eu ipsum rutrum accumsan. Orci varius natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus quis sapien luctus, volutpat nulla - eget, gravida nunc. Aenean placerat a felis quis imperdiet. Sed sapien tellus, ultrices non ipsum eget, - pretium rhoncus quam. Aliquam erat volutpat. Maecenas at interdum turpis, eu mattis ligula. Class aptent - taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In lobortis felis a leo - ultricies, venenatis mollis felis lobortis. Suspendisse placerat vel ante vel euismod. Aenean sit amet - ullamcorper mauris, id consectetur est. Ut ultricies enim non quam condimentum, et congue arcu commodo. - Praesent convallis eleifend turpis, vitae feugiat turpis imperdiet sit amet. Class aptent taciti sociosqu ad - litora torquent per conubia nostra, per inceptos himenaeos. Quisque vulputate porttitor mattis. Pellentesque - sed ullamcorper lectus. Suspendisse velit tortor, viverra eget facilisis condimentum, accumsan sit amet felis. - Cras lobortis mi fermentum ligula consectetur, vitae tincidunt mauris scelerisque. Aenean ac condimentum erat, - quis lacinia lacus. Ut magna nibh, tempor et ligula suscipit, placerat laoreet ipsum. In semper semper nisl. - Donec risus lorem, tempor sed sollicitudin vitae, fringilla et mi. Vivamus pulvinar quam nisl, et tincidunt - justo tempus quis. Duis semper magna nunc, vitae faucibus lectus facilisis sed. Phasellus consequat arcu vel - interdum fermentum. In condimentum euismod neque, sed aliquet mauris posuere nec. Etiam metus eros, - pellentesque eget scelerisque id, porttitor at ligula. Curabitur eget nibh maximus enim lobortis sodales. - Etiam vulputate ligula lobortis vestibulum pulvinar. Curabitur eros justo, accumsan sed elit ac, mattis - lacinia nisi. Suspendisse ullamcorper lectus sit amet tellus condimentum porttitor. Duis cursus, neque et - aliquam congue, odio lectus porta elit, id lacinia dolor justo non leo. Aliquam vehicula at tellus ullamcorper - tincidunt. Phasellus neque nibh, convallis sit amet arcu sit amet, convallis egestas tortor. Etiam sit amet - vehicula quam. Praesent id consequat lacus, ac facilisis quam. Integer tristique lorem eros, id consequat - lorem lobortis vitae. Aliquam luctus purus eget sem molestie iaculis. Duis nisl risus, sodales sit amet nunc - vitae, volutpat cursus augue. Pellentesque congue massa eu metus pellentesque consectetur at vel neque. Donec - bibendum hendrerit erat, vitae dictum enim lobortis a. Quisque ac dapibus tellus, sit amet facilisis orci. - Cras pretium tortor non condimentum semper. Phasellus mollis condimentum blandit. Pellentesque at arcu risus. - Vivamus sit amet dui semper, suscipit est nec, elementum arcu. Praesent ante turpis, convallis ac leo eget, - - - - ); -} - -export default meta; diff --git a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx index 1f033fcf69c..eb6cb427eba 100644 --- a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx +++ b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx @@ -12,7 +12,7 @@ import { AutoSaveField } from './AutoSaveField'; import mdx from './AutoSaveField.mdx'; const meta: Meta = { - title: 'Forms/AutoSaveField', + title: 'Inputs/AutoSaveField', component: AutoSaveField, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Badge/Badge.story.tsx b/packages/grafana-ui/src/components/Badge/Badge.story.tsx index 173ba2ab36d..69ee4c37d94 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.story.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.story.tsx @@ -6,7 +6,7 @@ import { Badge } from './Badge'; import mdx from './Badge.mdx'; const meta: Meta = { - title: 'Data Display/Badge', + title: 'Information/Badge', component: Badge, parameters: { docs: { page: mdx }, diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx index 4c5575563d0..f5f42236828 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx @@ -9,7 +9,7 @@ import { BarGauge, Props } from './BarGauge'; import mdx from './BarGauge.mdx'; const meta: Meta = { - title: 'Visualizations/BarGauge', + title: 'Plugins/BarGauge', component: BarGauge, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx index 816a377b25a..48c45f9843c 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx @@ -15,7 +15,7 @@ import { import mdx from './BigValue.mdx'; const meta: Meta = { - title: 'Visualizations/BigValue', + title: 'Plugins/BigValue', component: BigValue, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx index c87d6709567..5f26875852e 100644 --- a/packages/grafana-ui/src/components/Button/Button.story.tsx +++ b/packages/grafana-ui/src/components/Button/Button.story.tsx @@ -12,7 +12,7 @@ import { ButtonGroup } from './ButtonGroup'; const sizes: ComponentSize[] = ['lg', 'md', 'sm']; export default { - title: 'Buttons/Button', + title: 'Inputs/Button', component: Button, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Button/ButtonGroup.tsx b/packages/grafana-ui/src/components/Button/ButtonGroup.tsx index 3ae3043cb96..176249abf49 100644 --- a/packages/grafana-ui/src/components/Button/ButtonGroup.tsx +++ b/packages/grafana-ui/src/components/Button/ButtonGroup.tsx @@ -27,14 +27,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderRadius: theme.shape.radius.default, '> .button-group:not(:first-child) > button, > button:not(:first-child)': { - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, + borderTopLeftRadius: 'unset', + borderBottomLeftRadius: 'unset', borderLeft: `1px solid rgba(255, 255, 255, 0.12)`, }, '> .button-group:not(:last-child) > button, > button:not(:last-child)': { - borderTopRightRadius: 0, - borderBottomRightRadius: 0, + borderTopRightRadius: 'unset', + borderBottomRightRadius: 'unset', borderRight: `1px solid rgba(0, 0, 0, 0.12)`, }, }), diff --git a/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx b/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx index edb861bbfad..b9870ad11b2 100644 --- a/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx +++ b/packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx @@ -3,7 +3,7 @@ import { StoryFn, Meta } from '@storybook/react'; import { ButtonCascader } from './ButtonCascader'; const meta: Meta = { - title: 'Forms/Cascader/ButtonCascader', + title: 'Inputs/ButtonCascader', component: ButtonCascader, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.internal.story.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx similarity index 96% rename from packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.internal.story.tsx rename to packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx index 5d42373e1cc..8c107a20129 100644 --- a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.internal.story.tsx +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx @@ -6,7 +6,7 @@ import { Button } from '../Button/Button'; import { CallToActionCard, CallToActionCardProps } from './CallToActionCard'; const meta: Meta = { - title: 'Layout/CallToActionCard', + title: 'Information/Deprecated/CallToActionCard', component: CallToActionCard, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx index e71c688f3e7..ca3348c2b54 100644 --- a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx @@ -11,6 +11,7 @@ export interface CallToActionCardProps { className?: string; } +/** @deprecated Use instead */ export const CallToActionCard = ({ message, callToActionElement, footer, className }: CallToActionCardProps) => { const css = useStyles2(getStyles); diff --git a/packages/grafana-ui/src/components/Card/Card.story.tsx b/packages/grafana-ui/src/components/Card/Card.story.tsx index 8f2a9668dd2..34b25d96a05 100644 --- a/packages/grafana-ui/src/components/Card/Card.story.tsx +++ b/packages/grafana-ui/src/components/Card/Card.story.tsx @@ -9,10 +9,9 @@ import { Card } from './Card'; const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png'; const meta: Meta = { - title: 'General/Card', + title: 'Layout/Card', component: Card, - // nosort is a custom tag used so the stories shown in docs keep the order they are defined in the file - tags: ['autodocs', 'nosort'], + tags: ['autodocs'], parameters: { controls: { exclude: ['onClick', 'href', 'heading', 'description', 'className', 'noMargin'], diff --git a/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx b/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx index ffed8b27098..33a2c762582 100644 --- a/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx +++ b/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx @@ -15,7 +15,7 @@ const sampleImages = [ ]; const meta: Meta = { - title: 'Data Display/Carousel', + title: 'Overlays/Carousel', component: Carousel, parameters: { docs: { page: mdx }, diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx index 43d8c337589..761235d5a22 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx @@ -31,7 +31,7 @@ const options = [ ]; const meta: Meta = { - title: 'Forms/Cascader', + title: 'Inputs/Cascader', component: Cascader, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Cascader/styles.ts b/packages/grafana-ui/src/components/Cascader/styles.ts index cef38b62dcc..68055527b24 100644 --- a/packages/grafana-ui/src/components/Cascader/styles.ts +++ b/packages/grafana-ui/src/components/Cascader/styles.ts @@ -2,6 +2,8 @@ import { css, keyframes } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; +import { getIconPath } from '../Icon/utils'; + const slideUpIn = keyframes({ '0%': { opacity: 0, @@ -58,143 +60,146 @@ const slideDownOut = keyframes({ }, }); -export const getCascaderStyles = (theme: GrafanaTheme2) => ({ - dropdown: css({ - '&.rc-cascader-dropdown': { - position: 'absolute', - // Required, otherwise the portal that the popup is shown in will render under other components - zIndex: 9999, +export const getCascaderStyles = (theme: GrafanaTheme2) => { + const iconPath = getIconPath('angle-right'); + return { + dropdown: css({ + '&.rc-cascader-dropdown': { + position: 'absolute', + // Required, otherwise the portal that the popup is shown in will render under other components + zIndex: 9999, - '&-hidden': { - display: 'none', - }, - }, - '.rc-cascader': { - '&-menus': { - overflow: 'hidden', - background: theme.colors.background.elevated, - border: `none`, - borderRadius: theme.shape.radius.default, - boxShadow: theme.shadows.z3, - whiteSpace: 'nowrap', - - '&.slide-up-enter, &.slide-up-appear': { - animationDuration: '0.3s', - animationFillMode: 'both', - transformOrigin: '0 0', - opacity: 0, - animationTimingFunction: 'cubic-bezier(0.08, 0.82, 0.17, 1)', - animationPlayState: 'paused', - }, - - '&.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement, &.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement': - { - '&-bottomLeft': { - animationName: slideUpIn, - animationPlayState: 'running', - }, - - '&-topLeft': { - animationName: slideDownIn, - animationPlayState: 'running', - }, - }, - - '&.slide-up-leave': { - animationDuration: '0.3s', - animationFillMode: 'both', - transformOrigin: '0 0', - opacity: 1, - animationTimingFunction: 'cubic-bezier(0.6, 0.04, 0.98, 0.34)', - animationPlayState: 'paused', - - '&.slide-up-leave-active.rc-cascader-menus-placement': { - '&-bottomLeft': { - animationName: slideUpOut, - animationPlayState: 'running', - }, - - '&-topLeft': { - animationName: slideDownOut, - animationPlayState: 'running', - }, - }, + '&-hidden': { + display: 'none', }, }, - - '&-menu': { - display: 'inline-block', - maxWidth: '50vw', - height: '192px', - listStyle: 'none', - margin: 0, - padding: theme.spacing(0.5), - borderRight: `1px solid ${theme.colors.border.weak}`, - overflow: 'auto', - - '&:last-child': { - borderRight: 0, - }, - - '&-item': { - height: theme.spacing(4), - lineHeight: theme.spacing(4), - padding: theme.spacing(0, 4, 0, 2), - borderRadius: theme.shape.radius.default, - cursor: 'pointer', - whiteSpace: 'nowrap', + '.rc-cascader': { + '&-menus': { overflow: 'hidden', - textOverflow: 'ellipsis', - transition: 'all 0.3s ease', - position: 'relative', + background: theme.colors.background.elevated, + border: `none`, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + whiteSpace: 'nowrap', - '&:hover': { - background: theme.colors.action.hover, + '&.slide-up-enter, &.slide-up-appear': { + animationDuration: '0.3s', + animationFillMode: 'both', + transformOrigin: '0 0', + opacity: 0, + animationTimingFunction: 'cubic-bezier(0.08, 0.82, 0.17, 1)', + animationPlayState: 'paused', }, - '&-disabled': { - cursor: 'not-allowed', - color: theme.colors.text.disabled, + '&.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement, &.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement': + { + '&-bottomLeft': { + animationName: slideUpIn, + animationPlayState: 'running', + }, - '&:hover': { - background: 'transparent', + '&-topLeft': { + animationName: slideDownIn, + animationPlayState: 'running', + }, }, - '&:after': { - position: 'absolute', - right: '12px', - content: "'loading'", - color: theme.colors.text.disabled, - fontStyle: 'italic', + '&.slide-up-leave': { + animationDuration: '0.3s', + animationFillMode: 'both', + transformOrigin: '0 0', + opacity: 1, + animationTimingFunction: 'cubic-bezier(0.6, 0.04, 0.98, 0.34)', + animationPlayState: 'paused', + + '&.slide-up-leave-active.rc-cascader-menus-placement': { + '&-bottomLeft': { + animationName: slideUpOut, + animationPlayState: 'running', + }, + + '&-topLeft': { + animationName: slideDownOut, + animationPlayState: 'running', + }, }, }, + }, - '&-active': { - color: theme.colors.text.maxContrast, - background: theme.colors.background.secondary, + '&-menu': { + display: 'inline-block', + maxWidth: '50vw', + height: '192px', + listStyle: 'none', + margin: 0, + padding: theme.spacing(0.5), + borderRight: `1px solid ${theme.colors.border.weak}`, + overflow: 'auto', + + '&:last-child': { + borderRight: 0, + }, + + '&-item': { + height: theme.spacing(4), + lineHeight: theme.spacing(4), + padding: theme.spacing(0, 4, 0, 2), + borderRadius: theme.shape.radius.default, + cursor: 'pointer', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + transition: 'all 0.3s ease', + position: 'relative', '&:hover': { background: theme.colors.action.hover, }, - }, - '&-expand': { - position: 'relative', + '&-disabled': { + cursor: 'not-allowed', + color: theme.colors.text.disabled, - '&:after': { + '&:hover': { + background: 'transparent', + }, + + '&:after': { + position: 'absolute', + right: '12px', + content: "'loading'", + color: theme.colors.text.disabled, + fontStyle: 'italic', + }, + }, + + '&-active': { + color: theme.colors.text.maxContrast, background: theme.colors.background.secondary, - content: "''", - height: theme.spacing(3), - mask: 'url(../img/icons/unicons/angle-right.svg)', - maskType: 'luminance', - position: 'absolute', - right: 0, - top: theme.spacing(0.5), - width: theme.spacing(3), + + '&:hover': { + background: theme.colors.action.hover, + }, + }, + + '&-expand': { + position: 'relative', + + '&:after': { + background: theme.colors.text.primary, + content: "''", + height: theme.spacing(3), + mask: `url(${iconPath})`, + maskType: 'luminance', + position: 'absolute', + right: 0, + top: theme.spacing(0.5), + width: theme.spacing(3), + }, }, }, }, }, - }, - }), -}); + }), + }; +}; diff --git a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.story.tsx b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.story.tsx index c2fc4d41a24..53f56e7d128 100644 --- a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.story.tsx +++ b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.story.tsx @@ -5,7 +5,7 @@ import { ClickOutsideWrapper } from './ClickOutsideWrapper'; import mdx from './ClickOutsideWrapper.mdx'; const meta: Meta = { - title: 'Layout/ClickOutsideWrapper', + title: 'Utilities/ClickOutsideWrapper', component: ClickOutsideWrapper, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.story.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.story.tsx index 5089786ae97..64c0489590a 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.story.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.story.tsx @@ -7,7 +7,7 @@ import { ClipboardButton as ClipboardButtonImpl, Props } from './ClipboardButton import mdx from './ClipboardButton.mdx'; const meta: Meta = { - title: 'Buttons/ClipboardButton', + title: 'Inputs/ClipboardButton', component: ClipboardButtonImpl, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx index 196c8d65c97..c3967136503 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -5,12 +5,11 @@ import { Meta, StoryFn } from '@storybook/react'; import { useStyles2 } from '../../themes/ThemeContext'; import { clearButtonStyles } from '../Button/Button'; -import { ColorPicker, SeriesColorPicker } from './ColorPicker'; +import { ColorPicker } from './ColorPicker'; import mdx from './ColorPicker.mdx'; -import { ColorPickerInput } from './ColorPickerInput'; const meta: Meta = { - title: 'Pickers and Editors/ColorPicker', + title: 'Pickers/ColorPicker', component: ColorPicker, parameters: { docs: { @@ -43,24 +42,6 @@ export const Basic: StoryFn = ({ color, enableNamedColors }) ); }; -export const SeriesPicker: StoryFn = ({ color, enableNamedColors }) => { - const [, updateArgs] = useArgs(); - return ( -
- {}} - color={color} - onChange={(color) => { - action('Color changed')(color); - updateArgs({ color }); - }} - /> -
- ); -}; - export const CustomTrigger: StoryFn = ({ color, enableNamedColors }) => { const [, updateArgs] = useArgs(); const clearButton = useStyles2(clearButtonStyles); @@ -89,19 +70,4 @@ export const CustomTrigger: StoryFn = ({ color, enableNamedC ); }; -export const Input: StoryFn = ({ color }) => { - const [, updateArgs] = useArgs(); - return ( -
- { - action('Color changed')(color); - updateArgs({ color }); - }} - /> -
- ); -}; - export default meta; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx new file mode 100644 index 00000000000..0be0684ca33 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx @@ -0,0 +1,32 @@ +import { action } from '@storybook/addon-actions'; +import { useArgs } from '@storybook/preview-api'; +import { Meta, StoryFn } from '@storybook/react'; + +import { ColorPickerInput } from './ColorPickerInput'; + +const meta: Meta = { + title: 'Pickers/ColorPickerInput', + component: ColorPickerInput, + parameters: { + controls: { + exclude: ['onChange', 'onColorChange'], + }, + }, +}; + +export const Basic: StoryFn = ({ color }) => { + const [, updateArgs] = useArgs(); + return ( +
+ { + action('Color changed')(color); + updateArgs({ color }); + }} + /> +
+ ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx deleted file mode 100644 index a8ffbc6d129..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Meta } from '@storybook/react'; -import { useState } from 'react'; - -import { useTheme2 } from '../../themes/ThemeContext'; - -import mdx from './ColorPicker.mdx'; -import { ColorPickerPopover } from './ColorPickerPopover'; -import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; - -const meta: Meta = { - title: 'Pickers and Editors/ColorPicker/Popovers', - component: ColorPickerPopover, - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const Basic = () => { - return ( -
- { - console.log(color); - }} - /> -
- ); -}; - -export const SeriesColorPickerPopoverExample = () => { - const theme = useTheme2(); - const [yAxis, setYAxis] = useState(0); - - return ( -
- (yAxis ? setYAxis(0) : setYAxis(2))} - color="#BC67E6" - onChange={(color: string) => { - console.log(color); - }} - /> -
- ); -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/ColorPicker/Palettes.story.tsx b/packages/grafana-ui/src/components/ColorPicker/Palettes.story.tsx deleted file mode 100644 index 06042a9a30d..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/Palettes.story.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { useArgs } from '@storybook/preview-api'; -import { Meta, StoryFn } from '@storybook/react'; -import { useState } from 'react'; - -import mdx from './ColorPicker.mdx'; -import { NamedColorsPalette } from './NamedColorsPalette'; -import SpectrumPalette from './SpectrumPalette'; - -const meta: Meta = { - title: 'Pickers and Editors/ColorPicker/Palettes', - parameters: { - docs: { - page: mdx, - }, - controls: { - exclude: ['theme', 'color'], - }, - }, - args: { - color: 'green', - }, -}; - -export const NamedColors: StoryFn = ({ color }) => { - const [colorVal, setColor] = useState(color); - return ; -}; - -export const Spectrum: StoryFn = ({ color }) => { - const [, updateArgs] = useArgs(); - return ( - { - action('Color changed')(color); - updateArgs({ color }); - }} - /> - ); -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.story.tsx new file mode 100644 index 00000000000..a66b0b951dd --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.story.tsx @@ -0,0 +1,39 @@ +import { action } from '@storybook/addon-actions'; +import { useArgs } from '@storybook/preview-api'; +import { Meta, StoryFn } from '@storybook/react'; + +import { SeriesColorPicker } from './ColorPicker'; + +const meta: Meta = { + title: 'Pickers/SeriesColorPicker', + component: SeriesColorPicker, + parameters: { + controls: { + exclude: ['onChange', 'onColorChange'], + }, + }, + args: { + enableNamedColors: false, + color: '#00ff00', + }, +}; + +export const Basic: StoryFn = ({ color, enableNamedColors }) => { + const [, updateArgs] = useArgs(); + return ( +
+ {}} + color={color} + onChange={(color) => { + action('Color changed')(color); + updateArgs({ color }); + }} + /> +
+ ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index d8dc2ff2b7c..eea67016ada 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -16,7 +16,7 @@ type PropsAndCustomArgs = ComboboxProps & type Story = StoryObj>; const meta: Meta = { - title: 'Forms/Combobox', + title: 'Inputs/Combobox', component: Combobox, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx index 6c99190c64a..9456242aa8f 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx @@ -11,7 +11,7 @@ import { generateOptions, fakeSearchAPI, generateGroupingOptions } from './story import { ComboboxOption } from './types'; const meta: Meta = { - title: 'Forms/MultiCombobox', + title: 'Inputs/MultiCombobox', component: MultiCombobox, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx index 6d138a7cb90..106dfc19af6 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx @@ -8,7 +8,7 @@ import mdx from './ConfirmButton.mdx'; import { DeleteButton } from './DeleteButton'; const meta: Meta = { - title: 'Buttons/ConfirmButton', + title: 'Inputs/ConfirmButton', component: ConfirmButton, // SB7 has broken subcomponent types due to dropping support for the feature // https://github.com/storybookjs/storybook/issues/20782 diff --git a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx index ceebc2502b5..9122239ea0f 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx @@ -1,3 +1,5 @@ +import { t } from '@grafana/i18n'; + import { ComponentSize } from '../../types/size'; import { Button } from '../Button/Button'; @@ -18,7 +20,7 @@ export interface Props { export const DeleteButton = ({ size, disabled, onConfirm, 'aria-label': ariaLabel, closeOnConfirm }: Props) => { return ( = { - title: 'General/ContextMenu', + title: 'Overlays/ContextMenu', component: ContextMenu, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.story.tsx b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.story.tsx index 3557ef84dd6..b198f976280 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.story.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.story.tsx @@ -36,7 +36,7 @@ const settingsMock: HttpSettingsProps['dataSourceConfig'] = { }; const meta: Meta = { - title: 'Data Source/DataSourceHttpSettings', + title: 'Plugins/DataSourceHttpSettings', component: DataSourceHttpSettings, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.story.tsx index 29b10d03148..3f12563318a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.story.tsx @@ -7,7 +7,7 @@ import { DatePicker, DatePickerProps } from './DatePicker'; import mdx from './DatePicker.mdx'; const meta: Meta = { - title: 'Pickers and Editors/TimePickers/Pickers And Editors/DatePicker', + title: 'Date time pickers/DatePicker', component: DatePicker, argTypes: { minDate: { control: 'date' }, diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.story.tsx index 66b067846dd..158aa681b2d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.story.tsx @@ -13,7 +13,7 @@ const minimumDate = new Date(); minimumDate.setMonth(minimumDate.getMonth() - 1); const meta: Meta = { - title: 'Pickers and Editors/TimePickers/DatePickerWithInput', + title: 'Date time pickers/DatePickerWithInput', component: DatePickerWithInput, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.story.tsx index a7a8d87961f..19acd203c0a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.story.tsx @@ -15,7 +15,7 @@ const minimumDate = new Date(); minimumDate.setDate(minimumDate.getDate() - 7); const meta: Meta = { - title: 'Pickers and Editors/TimePickers/DateTimePicker', + title: 'Date time pickers/DateTimePicker', component: DateTimePicker, argTypes: { date: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx index a6a0813e210..cacf19faf92 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx @@ -5,7 +5,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { RelativeTimeRangePicker } from './RelativeTimeRangePicker'; const meta: Meta = { - title: 'Pickers and Editors/TimePickers/RelativeTimeRangePicker', + title: 'Date time pickers/RelativeTimeRangePicker', component: RelativeTimeRangePicker, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx index abd37242483..d3678e8dc84 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx @@ -7,7 +7,7 @@ import { dateTime } from '@grafana/data'; import { TimeOfDayPicker } from './TimeOfDayPicker'; const meta: Meta = { - title: 'Pickers and Editors/TimePickers/TimeOfDayPicker', + title: 'Date time pickers/TimeOfDayPicker', component: TimeOfDayPicker, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.story.tsx index ef2ce7d2923..c70656bc070 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.story.tsx @@ -30,7 +30,7 @@ const nullRange = { }; const meta: Meta = { - title: 'Pickers and Editors/TimePickers/TimeRangeInput', + title: 'Date time pickers/TimeRangeInput', component: TimeRangeInput, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.story.tsx index f7ae73b010c..dadb184d716 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.story.tsx @@ -10,7 +10,7 @@ const to = dateTime(); const from = to.subtract(6, 'h'); const meta: Meta = { - title: 'Pickers and Editors/TimePickers/TimeRangePicker', + title: 'Date time pickers/TimeRangePicker', component: TimeRangePicker, args: { value: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx index 7a130f08950..d564a28b8ee 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx @@ -1,11 +1,32 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { dateTimeParse, systemDateFormats, TimeRange } from '@grafana/data'; +import { dateTimeParse, FeatureToggles, systemDateFormats, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import * as commonFormatModule from '../commonFormat'; + import { TimeRangeContent } from './TimeRangeContent'; +// If this flag is deleted, this mock also should be, and the additional tests for when +// the flag was disabled. +type LocaleFormatPreferenceType = FeatureToggles['localeFormatPreference']; +jest.mock('../commonFormat', () => { + const format = 'YYYY-MM-DD HH:mm:ss' as const; + const moduleObject = { + __esModule: true, + commonFormat: format as undefined | 'YYYY-MM-DD HH:mm:ss', + mockSetCommonFormat, + }; + function mockSetCommonFormat(enabled: LocaleFormatPreferenceType = true) { + moduleObject.commonFormat = enabled ? format : undefined; + } + return moduleObject; +}); +// @ts-expect-error mockSetCommonFormat doesn't exist on the export type of commonFormat, +// but it's added above in the mock. +const mockSetCommonFormat: (enabled: LocaleFormatPreferenceType) => void = commonFormatModule.mockSetCommonFormat; + const mockClipboard = { writeText: jest.fn(), readText: jest.fn(), @@ -25,9 +46,16 @@ const customRawTimeRange = { to: '2023-06-19 23:59:00', }; +const mockOnApply = jest.fn(); + +beforeEach(() => { + mockSetCommonFormat(true); + mockOnApply.mockClear(); +}); + function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc') { return { - ...render( {}} timeZone={timeZone} />), + ...render(), getCalendarDayByLabelText: (label: string) => { const item = screen.getByLabelText(label); return item?.parentElement as HTMLButtonElement; @@ -54,7 +82,6 @@ describe('TimeRangeForm', () => { }); it('should display calendar when clicking the calendar icon', async () => { - const user = userEvent.setup(); setup(); const { TimePicker } = selectors.components; const openCalendarButton = screen.getAllByRole('button', { name: 'Open calendar' }); @@ -90,6 +117,29 @@ describe('TimeRangeForm', () => { expect(getByLabelText('To')).toHaveValue('2021-06-19 19:59:00'); }); + describe('when common format are entered', () => { + it('parses those dates in the current timezone', async () => { + setup(); + + const fromInput = screen.getByLabelText('From'); + const toInput = screen.getByLabelText('To'); + await user.clear(fromInput); + await user.type(fromInput, '2021-05-10 20:00:00'); + await user.clear(toInput); + await user.type(toInput, '2021-05-12 19:59:00'); + + await user.click(screen.getByRole('button', { name: 'Apply time range' })); + + const appliedOrUndefined = mockOnApply.mock.lastCall?.at(0) as undefined | TimeRange; + expect(appliedOrUndefined).not.toBe(undefined); + const applied = appliedOrUndefined!; // previous line throws if undefined + expect(applied.from.toISOString()).toBe('2021-05-10T20:00:00.000Z'); + expect(applied.to.toISOString()).toBe('2021-05-12T19:59:00.000Z'); + }); + }); + + // once localeFormatPreference is permanently on, the only tests that should remain + // in this block will be ones that ensures the system format is *not* used describe('Given custom system date format', () => { const originalFullDate = systemDateFormats.fullDate; beforeEach(() => { @@ -100,7 +150,7 @@ describe('TimeRangeForm', () => { systemDateFormats.fullDate = originalFullDate; }); - it('should parse UTC iso strings and render in current timezone', () => { + it('should parse UTC iso strings and render them in the common format and current timezone', () => { const { getByLabelText } = setup( { from: defaultTimeRange.from, @@ -113,8 +163,96 @@ describe('TimeRangeForm', () => { 'America/New_York' ); - expect(getByLabelText('From')).toHaveValue('16.06.2021 20:00:00'); - expect(getByLabelText('To')).toHaveValue('19.06.2021 19:59:00'); + expect(getByLabelText('From')).toHaveValue('2021-06-16 20:00:00'); + expect(getByLabelText('To')).toHaveValue('2021-06-19 19:59:00'); + }); + + describe('when common format dates are entered', () => { + it('parses those dates in the current timezone', async () => { + setup(); + + const fromInput = screen.getByLabelText('From'); + const toInput = screen.getByLabelText('To'); + await user.clear(fromInput); + await user.type(fromInput, '2021-05-10 20:00:00'); + await user.clear(toInput); + await user.type(toInput, '2021-05-12 19:59:00'); + + await user.click(screen.getByRole('button', { name: 'Apply time range' })); + + const appliedOrUndefined = mockOnApply.mock.lastCall?.at(0) as undefined | TimeRange; + expect(appliedOrUndefined).not.toBe(undefined); + const applied = appliedOrUndefined!; // previous line throws if undefined + expect(applied.from.toISOString()).toBe('2021-05-10T20:00:00.000Z'); + expect(applied.to.toISOString()).toBe('2021-05-12T19:59:00.000Z'); + }); + }); + + describe('when the localeFormatPreference feature toggle is off', () => { + beforeEach(() => { + // when localeFormatPreference is permanently on, the parent describe block ("Given custom systemdate format") + // needs to be cleared out as most of these tests will be redundant. + mockSetCommonFormat(false); + }); + + it('should parse UTC ISO strings and render them in the system format', () => { + const { getByLabelText } = setup( + { + from: defaultTimeRange.from, + to: defaultTimeRange.to, + raw: { + from: defaultTimeRange.from.toISOString(), + to: defaultTimeRange.to.toISOString(), + }, + }, + 'America/New_York' + ); + + expect(getByLabelText('From')).toHaveValue('16.06.2021 20:00:00'); + expect(getByLabelText('To')).toHaveValue('19.06.2021 19:59:00'); + }); + + describe('when common format dates are entered', () => { + it('should show an error because of parsing failure', async () => { + setup(); + + const fromInput = screen.getByLabelText('From'); + const toInput = screen.getByLabelText('To'); + await user.clear(fromInput); + await user.type(fromInput, '2021-05-10 20:00:00'); + await user.clear(toInput); + await user.type(toInput, '2021-05-12 19:59:00'); + + await user.click(screen.getByRole('button', { name: 'Apply time range' })); + + const error = screen.getAllByRole('alert'); + + expect(error).toHaveLength(2); + expect(error[0]).toBeVisible(); + expect(error[0]).toHaveTextContent('Please enter a past date or "now"'); + }); + }); + + describe('when common format dates are entered', () => { + it('should show an error because of parsing failure', async () => { + setup(); + + const fromInput = screen.getByLabelText('From'); + const toInput = screen.getByLabelText('To'); + await user.clear(fromInput); + await user.type(fromInput, '10.05.2021 20:00:00'); + await user.clear(toInput); + await user.type(toInput, '12.05.2021 19:59:00'); + + await user.click(screen.getByRole('button', { name: 'Apply time range' })); + + const appliedOrUndefined = mockOnApply.mock.lastCall?.at(0) as undefined | TimeRange; + expect(appliedOrUndefined).not.toBe(undefined); + const applied = appliedOrUndefined!; // previous line throws if undefined + expect(applied.from.toISOString()).toBe('2021-05-10T20:00:00.000Z'); + expect(applied.to.toISOString()).toBe('2021-05-12T19:59:00.000Z'); + }); + }); }); }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx index 87a751ca1a1..7f6438af66a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx @@ -23,6 +23,7 @@ import { Icon } from '../../Icon/Icon'; import { Input } from '../../Input/Input'; import { Tooltip } from '../../Tooltip/Tooltip'; import { WeekStart } from '../WeekStartPicker'; +import { commonFormat } from '../commonFormat'; import { isValid } from '../utils'; import TimePickerCalendar from './TimePickerCalendar'; @@ -92,7 +93,7 @@ export const TimeRangeContent = (props: Props) => { } const raw: RawTimeRange = { from: from.value, to: to.value }; - const timeRange = rangeUtil.convertRawToRange(raw, timeZone, fiscalYearStartMonth); + const timeRange = rangeUtil.convertRawToRange(raw, timeZone, fiscalYearStartMonth, commonFormat); onApplyFromProps(timeRange); }, [from.invalid, from.value, onApplyFromProps, timeZone, to.invalid, to.value, fiscalYearStartMonth]); @@ -237,7 +238,7 @@ export const TimeRangeContent = (props: Props) => { function isRangeInvalid(from: string, to: string, timezone?: string): boolean { const raw: RawTimeRange = { from, to }; - const timeRange = rangeUtil.convertRawToRange(raw, timezone); + const timeRange = rangeUtil.convertRawToRange(raw, timezone, undefined, commonFormat); const valid = timeRange.from.isSame(timeRange.to) || timeRange.from.isBefore(timeRange.to); return !valid; @@ -267,12 +268,12 @@ function valueToState( function valueAsString(value: DateTime | string, timeZone?: TimeZone): string { if (isDateTime(value)) { - return dateTimeFormat(value, { timeZone }); + return dateTimeFormat(value, { timeZone, format: commonFormat }); } if (value.endsWith('Z')) { const dt = dateTimeParse(value); - return dateTimeFormat(dt, { timeZone }); + return dateTimeFormat(dt, { timeZone, format: commonFormat }); } return value; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts index 0e3e0c1a8bf..cce0bb76e37 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts @@ -2,8 +2,9 @@ import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat } from '@gra import { formatDateRange } from '@grafana/i18n'; import { getFeatureToggle } from '../../../utils/featureToggle'; +import { commonFormat } from '../commonFormat'; export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => { - return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone); + return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone, undefined, commonFormat); }; const rangeFormatShort: Intl.DateTimeFormatOptions = { @@ -17,8 +18,8 @@ const rangeFormatFull: Intl.DateTimeFormatOptions = { }; export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => { - const from = dateTimeFormat(range.from, { timeZone }); - const to = dateTimeFormat(range.to, { timeZone }); + const from = dateTimeFormat(range.from, { timeZone, format: commonFormat }); + const to = dateTimeFormat(range.to, { timeZone, format: commonFormat }); let display = `${from} to ${to}`; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.story.tsx index ee1b5e7e9d5..da95bb70c81 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeZonePicker.story.tsx @@ -5,7 +5,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { TimeZonePicker } from './TimeZonePicker'; const meta: Meta = { - title: 'Pickers and Editors/TimePickers/TimeZonePicker', + title: 'Date time pickers/TimeZonePicker', component: TimeZonePicker, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.story.tsx index cf80764789c..cc41f958d7b 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.story.tsx @@ -5,7 +5,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { WeekStartPicker } from './WeekStartPicker'; const meta: Meta = { - title: 'Pickers and Editors/TimePickers/WeekStartPicker', + title: 'Date time pickers/WeekStartPicker', component: WeekStartPicker, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx index 139a03713bd..99ebf2d3b3a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'; import { BootData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { t } from '@grafana/i18n'; import { Combobox } from '../Combobox/Combobox'; import { ComboboxOption } from '../Combobox/types'; @@ -17,12 +18,6 @@ export interface Props { } export type WeekStart = 'saturday' | 'sunday' | 'monday'; -const weekStarts: ComboboxOption[] = [ - { value: '', label: 'Default' }, - { value: 'saturday', label: 'Saturday' }, - { value: 'sunday', label: 'Sunday' }, - { value: 'monday', label: 'Monday' }, -]; export function isWeekStart(value: string): value is WeekStart { return ['saturday', 'sunday', 'monday'].includes(value); @@ -53,6 +48,15 @@ export function getWeekStart(override?: string): WeekStart { export const WeekStartPicker = (props: Props) => { const { onChange, width, autoFocus = false, onBlur, value, disabled = false, inputId } = props; + const weekStarts: ComboboxOption[] = useMemo( + () => [ + { value: '', label: t('grafana-ui.week-start-picker.weekStarts-label-default', 'Default') }, + { value: 'saturday', label: t('grafana-ui.week-start-picker.weekStarts-label-saturday', 'Saturday') }, + { value: 'sunday', label: t('grafana-ui.week-start-picker.weekStarts-label-sunday', 'Sunday') }, + { value: 'monday', label: t('grafana-ui.week-start-picker.weekStarts-label-monday', 'Monday') }, + ], + [] + ); const onChangeWeekStart = useCallback( (selectable: ComboboxOption | null) => { @@ -63,7 +67,7 @@ export const WeekStartPicker = (props: Props) => { [onChange] ); - const selected = useMemo(() => weekStarts.find((item) => item.value === value)?.value ?? '', [value]); + const selected = useMemo(() => weekStarts.find((item) => item.value === value)?.value ?? '', [value, weekStarts]); return ( = { - title: 'General/Divider', + title: 'Layout/Divider', component: Divider, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.internal.story.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.story.tsx similarity index 95% rename from packages/grafana-ui/src/components/Dropdown/ButtonSelect.internal.story.tsx rename to packages/grafana-ui/src/components/Dropdown/ButtonSelect.story.tsx index edc03ecf4a4..a943ab93b73 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.internal.story.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.story.tsx @@ -5,7 +5,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { ButtonSelect } from './ButtonSelect'; const meta: Meta = { - title: 'Forms/Select/ButtonSelect', + title: 'Inputs/Deprecated/ButtonSelect', component: ButtonSelect, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index 7268565239e..3db07bcfcea 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -33,8 +33,7 @@ export interface Props extends HTMLAttributes { } /** - * @internal - * A temporary component until we have a proper dropdown component + * @deprecated Use Combobox or Dropdown instead */ const ButtonSelectComponent = (props: Props) => { const { className, options, value, onChange, narrow, variant, ...restProps } = props; diff --git a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.story.tsx b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.story.tsx index c44444597e0..c32e91e9777 100644 --- a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.story.tsx +++ b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.story.tsx @@ -4,7 +4,7 @@ import { EmptySearchResult } from './EmptySearchResult'; import mdx from './EmptySearchResult.mdx'; const meta: Meta = { - title: 'Visualizations/EmptySearchResult', + title: 'Information/Deprecated/EmptySearchResult', component: EmptySearchResult, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx index 23a794908bf..2ddafbf865b 100644 --- a/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx +++ b/packages/grafana-ui/src/components/EmptySearchResult/EmptySearchResult.tsx @@ -8,6 +8,7 @@ export interface Props { children: JSX.Element | string; } +/** @deprecated Use instead */ const EmptySearchResult = ({ children }: Props) => { const styles = useStyles2(getStyles); return
{children}
; diff --git a/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx b/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx index c483f4ed4a4..d9d5b67f526 100644 --- a/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx +++ b/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx @@ -6,7 +6,7 @@ import { EmptyState } from './EmptyState'; import mdx from './EmptyState.mdx'; const meta: Meta = { - title: 'General/EmptyState', + title: 'Information/EmptyState', component: EmptyState, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.story.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.story.tsx index 474cfc400fb..b4dce6ba569 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.story.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.story.tsx @@ -9,7 +9,7 @@ import mdx from './ErrorBoundary.mdx'; import { ErrorWithStack } from './ErrorWithStack'; const meta: Meta = { - title: 'General/ErrorBoundary', + title: 'Utilities/ErrorBoundary', component: ErrorBoundary, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.story.tsx b/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.story.tsx index e3e24bf77a9..e2a8d70dc0d 100644 --- a/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.story.tsx +++ b/packages/grafana-ui/src/components/FeatureBadge/FeatureBadge.story.tsx @@ -6,7 +6,7 @@ import { FeatureBadge } from './FeatureBadge'; import mdx from './FeatureBadge.mdx'; const meta: Meta = { - title: 'Data Display/FeatureBadge', + title: 'Information/FeatureBadge', component: FeatureBadge, parameters: { docs: { page: mdx }, diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx index de3ef47612e..ac9b70eb14c 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx @@ -4,7 +4,7 @@ import { FileDropzone } from './FileDropzone'; import mdx from './FileDropzone.mdx'; const meta: Meta = { - title: 'Forms/FileDropzone', + title: 'Inputs/FileDropzone', component: FileDropzone, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/FileDropzone/FileListItem.story.tsx b/packages/grafana-ui/src/components/FileDropzone/FileListItem.story.tsx index 3e10caf11f4..1b5d2eed5d2 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileListItem.story.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileListItem.story.tsx @@ -4,7 +4,7 @@ import { FileListItem as FileListItemComponent, FileListItemProps } from './File import mdx from './FileListItem.mdx'; const meta: Meta = { - title: 'Forms/FileListItem', + title: 'Inputs/FileListItem', component: FileListItemComponent, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx index f6e7025553a..bdd445316ae 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx @@ -4,7 +4,7 @@ import { FileUpload } from './FileUpload'; import mdx from './FileUpload.mdx'; const meta: Meta = { - title: 'Forms/FileUpload', + title: 'Inputs/FileUpload', component: FileUpload, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/FilterPill/FilterPill.story.tsx b/packages/grafana-ui/src/components/FilterPill/FilterPill.story.tsx index 54dec2ec7ea..eb4c48deae9 100644 --- a/packages/grafana-ui/src/components/FilterPill/FilterPill.story.tsx +++ b/packages/grafana-ui/src/components/FilterPill/FilterPill.story.tsx @@ -8,7 +8,7 @@ import { FilterPill } from './FilterPill'; import mdx from './FilterPill.mdx'; const meta: Meta = { - title: 'General/FilterPill', + title: 'Inputs/FilterPill', component: FilterPill, argTypes: { icon: { control: { type: 'select', options: getAvailableIcons() } }, diff --git a/packages/grafana-ui/src/components/FormField/FormField.internal.story.tsx b/packages/grafana-ui/src/components/FormField/FormField.story.tsx similarity index 95% rename from packages/grafana-ui/src/components/FormField/FormField.internal.story.tsx rename to packages/grafana-ui/src/components/FormField/FormField.story.tsx index efddb8d7f3c..845001dcacc 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.internal.story.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.story.tsx @@ -3,7 +3,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { FormField } from './FormField'; const meta: Meta = { - title: 'Forms/Legacy/FormField', + title: 'Forms/Deprecated/FormField', component: FormField, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.story.tsx b/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.story.tsx index 3358a90f179..5f7cad8d3ec 100644 --- a/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.story.tsx +++ b/packages/grafana-ui/src/components/FormattedValueDisplay/FormattedValueDisplay.story.tsx @@ -4,7 +4,7 @@ import { FormattedValueDisplay } from './FormattedValueDisplay'; import mdx from './FormattedValueDisplay.mdx'; const meta: Meta = { - title: 'Visualizations/FormattedValueDisplay', + title: 'Plugins/FormattedValueDisplay', component: FormattedValueDisplay, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx index 8cdf3daa56d..fa4d1fb1d8e 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx @@ -9,7 +9,7 @@ import mdx from './Checkbox.mdx'; import { Field } from './Field'; const meta: Meta = { - title: 'Forms/Checkbox', + title: 'Inputs/Checkbox', component: Checkbox, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.internal.story.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.internal.story.tsx deleted file mode 100644 index a837949c744..00000000000 --- a/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.internal.story.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Meta, StoryFn } from '@storybook/react'; -import { zip, fromPairs } from 'lodash'; -import { useState } from 'react'; - -import { EventsWithValidation } from '../../../../utils/validate'; - -import { Input } from './Input'; - -const meta: Meta = { - title: 'Forms/Legacy/Input', - component: Input, - parameters: { - controls: { - exclude: ['inputRef', 'onBlur', 'onFocus', 'onChange'], - }, - }, - argTypes: { - validationEvents: { - control: { - type: 'select', - options: fromPairs(zip(Object.keys(EventsWithValidation), Object.values(EventsWithValidation))), - }, - }, - validation: { name: 'Validation regex (will do a partial match if you do not anchor it)' }, - }, -}; - -const Wrapper: StoryFn = (args) => { - const [value, setValue] = useState(''); - const validations = { - [args.validationEvents]: [ - { - rule: (value: string) => { - return !!value.match(args.validation); - }, - errorMessage: args.validationErrorMessage, - }, - ], - }; - return ( - setValue(e.currentTarget.value)} - validationEvents={validations} - hideErrorMessage={args.hideErrorMessage} - /> - ); -}; - -export const Basic = Wrapper.bind({}); -Basic.args = { - validation: '', - validationErrorMessage: 'Input not valid', - validationEvents: EventsWithValidation.onBlur, - hideErrorMessage: false, -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.internal.story.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.internal.story.tsx deleted file mode 100644 index 5e156ab8f9c..00000000000 --- a/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.internal.story.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { useArgs } from '@storybook/preview-api'; -import { Meta, StoryFn } from '@storybook/react'; -import { useCallback } from 'react'; - -import { SelectableValue } from '@grafana/data'; - -import { Select, AsyncSelect as AsyncSelectComponent } from './Select'; - -const meta: Meta = { - title: 'Forms/Legacy/Select', - component: Select, - parameters: { - controls: { - exclude: [ - 'className', - 'menuPlacement', - 'menuPosition', - 'maxMenuHeight', - 'minMenuHeight', - 'maxVisibleValues', - 'prefix', - 'renderControl', - 'value', - 'tooltipContent', - 'components', - 'inputValue', - 'id', - 'inputId', - 'defaultValue', - 'aria-label', - 'noOptionsMessage', - 'onChange', - 'onBlur', - 'onKeyDown', - 'filterOption', - 'formatCreateLabel', - 'getOptionLabel', - 'getOptionValue', - 'onCloseMenu', - 'onCreateOption', - 'onInputChange', - 'onOpenMenu', - 'isOptionDisabled', - ], - }, - }, - argTypes: { - width: { control: { type: 'range', min: 5, max: 30 } }, - }, -}; - -const initialValue: SelectableValue = { label: 'A label', value: 'A value' }; - -const options = [ - initialValue, - { label: 'Another label', value: 'Another value 1' }, - { label: 'Another label', value: 'Another value 2' }, - { label: 'Another label', value: 'Another value 3' }, - { label: 'Another label', value: 'Another value 4' }, - { label: 'Another label', value: 'Another value 5' }, - { label: 'Another label', value: 'Another value ' }, -]; - -export const Basic: StoryFn = (args) => { - const [, updateArgs] = useArgs(); - return ( - = { - title: 'Forms/SecretInput', + title: 'Inputs/SecretInput', component: SecretInput, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx index 4e71c1fd66a..9ee9fef61fe 100644 --- a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx +++ b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.story.tsx @@ -4,7 +4,7 @@ import { useState, ChangeEvent } from 'react'; import { SecretTextArea } from './SecretTextArea'; const meta: Meta = { - title: 'Forms/SecretTextArea', + title: 'Inputs/SecretTextArea', component: SecretTextArea, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/Segment/Segment.story.tsx b/packages/grafana-ui/src/components/Segment/Segment.story.tsx index 735c0833f53..91ae6c47bfb 100644 --- a/packages/grafana-ui/src/components/Segment/Segment.story.tsx +++ b/packages/grafana-ui/src/components/Segment/Segment.story.tsx @@ -60,7 +60,7 @@ export const ArrayOptions = () => { }; const meta: Meta = { - title: 'Data Source/Segment/SegmentSync', + title: 'Inputs/Segment', component: Segment, }; diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx index 26460bf62c6..9b64c2fca74 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx @@ -61,7 +61,7 @@ export const ArrayOptions = () => { }; const meta: Meta = { - title: 'Data Source/Segment/SegmentAsync', + title: 'Inputs/SegmentAsync', component: SegmentAsync, }; diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx index e73d694f235..780f6b4aba2 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx @@ -30,7 +30,7 @@ export const BasicInput = () => { }; const meta: Meta = { - title: 'Data Source/Segment/SegmentInput', + title: 'Inputs/SegmentInput', component: SegmentInput, }; diff --git a/packages/grafana-ui/src/components/Select/Select.story.tsx b/packages/grafana-ui/src/components/Select/Select.story.tsx index e73c07b7311..7e8388442a1 100644 --- a/packages/grafana-ui/src/components/Select/Select.story.tsx +++ b/packages/grafana-ui/src/components/Select/Select.story.tsx @@ -37,12 +37,8 @@ const manyGroupedOptions = [ ]; const meta: Meta = { - title: 'Forms/Select', + title: 'Inputs/Deprecated/Select', component: Select, - // SB7 has broken subcomponent types due to dropping support for the feature - // https://github.com/storybookjs/storybook/issues/20782 - // @ts-ignore - subcomponents: { AsyncSelect, MultiSelect, AsyncMultiSelect }, parameters: { docs: { page: mdx, diff --git a/packages/grafana-ui/src/components/Select/SelectPerf.internal.story.tsx b/packages/grafana-ui/src/components/Select/SelectPerf.story.tsx similarity index 98% rename from packages/grafana-ui/src/components/Select/SelectPerf.internal.story.tsx rename to packages/grafana-ui/src/components/Select/SelectPerf.story.tsx index d147b90a2b5..357eee70561 100644 --- a/packages/grafana-ui/src/components/Select/SelectPerf.internal.story.tsx +++ b/packages/grafana-ui/src/components/Select/SelectPerf.story.tsx @@ -9,7 +9,7 @@ import { Label } from '../Forms/Label'; import { Select, VirtualizedSelect } from './Select'; const meta: Meta = { - title: 'Forms/Select (Perf)', + title: 'Developers/Select Perf', argTypes: { numberOfOptions: { defaultValue: 10_000, diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx index 458ee63941a..eccb916483a 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx @@ -3,7 +3,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { RangeSlider } from './RangeSlider'; const meta: Meta = { - title: 'Forms/Slider/Range', + title: 'Inputs/RangeSlider', component: RangeSlider, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx index 5b110318423..a4180712444 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx @@ -3,7 +3,7 @@ import { StoryFn, Meta } from '@storybook/react'; import { Slider } from './Slider'; const meta: Meta = { - title: 'Forms/Slider', + title: 'Inputs/Slider', component: Slider, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx index 4b6cc484c33..509f6c73525 100644 --- a/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx +++ b/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx @@ -4,7 +4,7 @@ import { Props, Spinner } from './Spinner'; import mdx from './Spinner.mdx'; const meta: Meta = { - title: 'Visualizations/Spinner', + title: 'Information/Spinner', component: Spinner, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx index d469c2fc12e..3c7b7bf2a29 100644 --- a/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx @@ -7,7 +7,7 @@ import { UseSplitterOptions, useSplitter } from './useSplitter'; import mdx from './useSplitter.mdx'; const meta: Meta = { - title: 'General/Layout/useSplitter', + title: 'Utilities/useSplitter', parameters: { docs: { page: mdx }, controls: { diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx index f586fa350c2..7e20cb9288f 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx @@ -37,7 +37,7 @@ class WrapperWithState extends PureComponent { } const meta: Meta = { - title: 'Pickers and Editors/StatsPicker', + title: 'Pickers/StatsPicker', component: StatsPicker, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/Switch/Switch.story.tsx b/packages/grafana-ui/src/components/Switch/Switch.story.tsx index 23d5ed1f796..ae4786d0fe5 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.story.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.story.tsx @@ -10,7 +10,7 @@ import { InlineSwitch, Switch } from './Switch'; import mdx from './Switch.mdx'; const meta: Meta = { - title: 'Forms/Switch', + title: 'Inputs/Switch', component: Switch, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx index 1bd8774479b..5b2e28c646f 100644 --- a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx +++ b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx @@ -74,8 +74,6 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt const dismiss = useDismiss(context); - const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length); - const { getFloatingProps, getReferenceProps } = useInteractions([dismiss]); if (links.length === 0 && !Boolean(actions?.length)) { @@ -84,23 +82,22 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt return ( <> + {/* TODO: we can remove `value` from this component when tableNextGen is fully rolled out */} {value} - {hasMultipleLinksOrActions && ( - -
- - - -
-
- )} + +
+ + + +
+
); }; diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index ff9fe3160f8..35944868d22 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -23,7 +23,7 @@ import mdx from './Table.mdx'; import { FooterItem, TableCellDisplayMode, TableCustomCellOptions } from './types'; const meta: Meta = { - title: 'Visualizations/Table', + title: 'Plugins/Table', component: Table, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx index 9616baa5a76..087efa47ecc 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; @@ -6,12 +7,16 @@ import { useStyles2 } from '../../../../themes/ThemeContext'; import { ActionButton } from '../../../Actions/ActionButton'; import { ActionCellProps } from '../types'; -export const ActionsCell = ({ actions }: ActionCellProps) => { +export const ActionsCell = ({ field, rowIdx, getActions }: ActionCellProps) => { const styles = useStyles2(getStyles); + const actions = useMemo(() => getActions(field, rowIdx), [getActions, field, rowIdx]); + return (
- {actions && actions.map((action, i) => )} + {actions.map((action, i) => ( + + ))}
); }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx deleted file mode 100644 index b65fa0e0297..00000000000 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { Field, FieldType, LinkModel } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; - -import { TableCellDisplayMode } from '../../types'; - -import AutoCell from './AutoCell'; - -describe('AutoCell', () => { - describe('Displays data Links', () => { - const getFieldWithLinks = (links: LinkModel[]): Field => { - return { - name: 'Category', - type: FieldType.string, - values: ['A', 'B', 'A', 'B', 'A'], - config: { - custom: { - cellOptions: { - type: TableCellDisplayMode.Auto, - wrapText: false, - }, - }, - }, - display: (value: unknown) => ({ - text: String(value), - numeric: 0, - color: undefined, - prefix: undefined, - suffix: undefined, - }), - state: {}, - getLinks: () => links, - }; - }; - - it('shows multiple datalinks in the tooltip', async () => { - const linksForField = [ - { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel, - { href: 'http://asdasd2.com', title: 'Test Title2' } as LinkModel, - ]; - - jest.mock('../utils', () => ({ - getCellLinks: () => linksForField, - })); - - const field = getFieldWithLinks(linksForField); - - render( - - ); - - const cell = screen.getByTestId(selectors.components.TablePanel.autoCell); - await userEvent.click(cell); - - const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper); - expect(tooltip).toBeInTheDocument(); - expect(screen.getByText('Test Title')).toBeInTheDocument(); - expect(screen.getByText('Test Title2')).toBeInTheDocument(); - }); - - it('does not show tooltip for multiple links if one is invalid', async () => { - const linksForField = [ - { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel, - { title: 'Test Title2' } as LinkModel, - ]; - - jest.mock('../utils', () => ({ - getCellLinks: () => linksForField, - })); - - const field = getFieldWithLinks(linksForField); - - render( - - ); - - const cell = screen.getByTestId(selectors.components.TablePanel.autoCell); - await userEvent.click(cell); - - expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument(); - }); - }); -}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx index 3fdcfc85491..45427f703e8 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -1,90 +1,13 @@ -import { css } from '@emotion/css'; -import { Property } from 'csstype'; -import { useState } from 'react'; +import { formattedValueToString } from '@grafana/data'; -import { GrafanaTheme2, formattedValueToString } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; - -import { useStyles2 } from '../../../../themes/ThemeContext'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { TableCellOptions, TableCellDisplayMode } from '../../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { useSingleLink } from '../hooks'; import { AutoCellProps } from '../types'; -import { getCellLinks } from '../utils'; - -export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions, actions }: AutoCellProps) { - const styles = useStyles2(getStyles, justifyContent); +export default function AutoCell({ value, field, rowIdx }: AutoCellProps) { const displayValue = field.display!(value); const formattedValue = formattedValueToString(displayValue); - const links = getCellLinks(field, rowIdx) || []; + const link = useSingleLink(field, rowIdx); - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], formattedValue, getLinkStyle(styles, cellOptions)) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - formattedValue - )} -
- ); + return link != null ? renderSingleLink(link, formattedValue) : formattedValue; } - -const getLinkStyle = (styles: ReturnType, cellOptions: TableCellOptions) => { - if (cellOptions.type === TableCellDisplayMode.Auto) { - return styles.linkCell; - } - - return styles.cellLinkForColoredCell; -}; - -const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({ - cell: css({ - display: 'flex', - justifyContent: justifyContent, - a: { - color: 'inherit', - }, - }), - cellLinkForColoredCell: css({ - cursor: 'pointer', - overflow: 'hidden', - textOverflow: 'ellipsis', - userSelect: 'text', - whiteSpace: 'nowrap', - fontWeight: theme.typography.fontWeightMedium, - textDecoration: 'underline', - }), - linkCell: css({ - cursor: 'pointer', - overflow: 'hidden', - textOverflow: 'ellipsis', - userSelect: 'text', - whiteSpace: 'nowrap', - color: `${theme.colors.text.link} !important`, - fontWeight: theme.typography.fontWeightMedium, - paddingRight: theme.spacing(1.5), - '&:hover': { - textDecoration: 'underline', - color: theme.colors.text.link, - }, - }), -}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx index 710a8c65726..69ebfac671c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx @@ -1,13 +1,11 @@ -import { useState } from 'react'; - import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data'; import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema'; import { BarGauge } from '../../../BarGauge/BarGauge'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { useSingleLink } from '../hooks'; import { BarGaugeCellProps } from '../types'; -import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils'; +import { extractPixelValue, getCellOptions, getAlignmentFactor } from '../utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, @@ -23,7 +21,7 @@ const defaultScale: ThresholdsConfig = { ], }; -export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actions }: BarGaugeCellProps) => { +export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => { const displayValue = field.display!(value); const cellOptions = getCellOptions(field); const heightOffset = extractPixelValue(theme.spacing(1)); @@ -48,51 +46,26 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actio } const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!); - const links = getCellLinks(field, rowIdx) || []; - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; - - const renderComponent = () => { - return ( - - ); - }; - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], renderComponent()) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - renderComponent() - )} -
+ const barGaugeComponent = ( + ); + + const link = useSingleLink(field, rowIdx); + + return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent); }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx index d1612de97c8..a83a623ee88 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx @@ -16,7 +16,7 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => { {links && links.map((link, idx) => { return !link.href && link.onClick == null ? ( - + {link.title} ) : ( @@ -34,25 +34,6 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => { const getStyles = (theme: GrafanaTheme2) => ({ linkCell: css({ - cursor: 'pointer', - overflow: 'hidden', - textOverflow: 'ellipsis', - userSelect: 'text', - whiteSpace: 'nowrap', - color: theme.colors.text.link, - fontWeight: theme.typography.fontWeightMedium, - paddingRight: theme.spacing(1.5), - a: { - color: theme.colors.text.link, - }, - '&:hover': { - textDecoration: 'underline', - color: theme.colors.text.link, - }, - }), - cellLinkEmpty: css({ - overflow: 'hidden', - textOverflow: 'ellipsis', userSelect: 'text', whiteSpace: 'nowrap', fontWeight: theme.typography.fontWeightMedium, diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx index c5f7b79400f..a62cbeb6beb 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx @@ -1,5 +1,5 @@ -import { css } from '@emotion/css'; -import React, { useEffect, useMemo } from 'react'; +import { css, cx } from '@emotion/css'; +import React, { useEffect } from 'react'; import { Column, SortDirection } from 'react-data-grid'; import { Field, GrafanaTheme2 } from '@grafana/data'; @@ -34,9 +34,10 @@ const HeaderCell: React.FC = ({ crossFilterRows, showTypeIcons, }) => { - const styles = useStyles2(getStyles); - const displayName = useMemo(() => getDisplayName(field), [field]); - const filterable = useMemo(() => field.config.custom?.filterable ?? false, [field]); + const headerCellWrap = field.config.custom?.wrapHeaderText ?? false; + const styles = useStyles2(getStyles, headerCellWrap); + const displayName = getDisplayName(field); + const filterable = field.config.custom?.filterable ?? false; // we have to remove/reset the filter if the column is not filterable useEffect(() => { @@ -50,15 +51,18 @@ const HeaderCell: React.FC = ({ }, [filterable, displayName, filter, setFilter]); return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions <> - - {showTypeIcons && } - {/* Used cached displayName if available, otherwise use the column name (nested tables) */} -
{getDisplayName(field)}
- {direction && (direction === 'ASC' ? : )} -
- + {showTypeIcons && ( + + )} + {getDisplayName(field)} + {direction && ( + + )} {filterable && ( = ({ field={field} crossFilterOrder={crossFilterOrder} crossFilterRows={crossFilterRows} + iconClassName={styles.headerCellIcon} /> )} ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (theme: GrafanaTheme2, headerTextWrap?: boolean) => ({ headerCellLabel: css({ - border: 'none', - padding: 0, - background: 'inherit', cursor: 'pointer', - whiteSpace: 'nowrap', + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.secondary, overflow: 'hidden', textOverflow: 'ellipsis', - fontWeight: theme.typography.fontWeightMedium, - display: 'flex', - alignItems: 'center', - color: theme.colors.text.secondary, - gap: theme.spacing(1), - + whiteSpace: headerTextWrap ? 'pre-line' : 'nowrap', '&:hover': { textDecoration: 'underline', color: theme.colors.text.link, }, }), + headerCellIcon: css({ + marginBottom: theme.spacing(0.5), + alignSelf: 'flex-end', + color: theme.colors.text.secondary, + }), + headerSortIcon: css({ + marginBottom: theme.spacing(0.25), + }), }); export { HeaderCell }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx index bc62e266b12..e0c1050396a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx @@ -1,55 +1,28 @@ import { css } from '@emotion/css'; import { Property } from 'csstype'; -import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../../themes/ThemeContext'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; import { TableCellDisplayMode } from '../../types'; -import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils'; +import { useSingleLink } from '../hooks'; import { ImageCellProps } from '../types'; -import { getCellLinks } from '../utils'; const DATALINKS_HEIGHT_OFFSET = 10; -export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx, actions }: ImageCellProps) => { +export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => { const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET; const styles = useStyles2(getStyles, calculatedHeight, justifyContent); - const links = getCellLinks(field, rowIdx) || []; - - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; const { text } = field.display!(value); const { alt, title } = cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { alt: undefined, title: undefined }; const img = {alt}; + const link = useSingleLink(field, rowIdx); - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], img) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - img - )} -
- ); + return
{link == null ? img : renderSingleLink(link, img)}
; }; const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({ diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx index 2506255cb03..dc195cbcd67 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx @@ -1,16 +1,14 @@ import { css } from '@emotion/css'; import { Property } from 'csstype'; -import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../../themes/ThemeContext'; -import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip'; -import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { useSingleLink } from '../hooks'; import { JSONCellProps } from '../types'; -import { getCellLinks } from '../utils'; -export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSONCellProps) => { +export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => { const styles = useStyles2(getStyles, justifyContent); let displayValue = value; @@ -33,34 +31,9 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSON } } - const links = getCellLinks(field, rowIdx) || []; + const link = useSingleLink(field, rowIdx); - const [tooltipCoords, setTooltipCoords] = useState(); - const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions); - const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined; - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
- {shouldShowLink ? ( - renderSingleLink(links[0], displayValue) - ) : shouldShowTooltip ? ( - setTooltipCoords(undefined)} - /> - ) : ( - displayValue - )} -
- ); + return
{link == null ? displayValue : renderSingleLink(link, displayValue)}
; }; const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({ diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx new file mode 100644 index 00000000000..b7249e88b2b --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.test.tsx @@ -0,0 +1,151 @@ +import { render, RenderResult } from '@testing-library/react'; + +import { DataFrame, Field, FieldType, GrafanaTheme2, MappingType, createTheme } from '@grafana/data'; +import { TableCellDisplayMode, TablePillCellOptions } from '@grafana/schema'; + +import { mockThemeContext } from '../../../../themes/ThemeContext'; + +import { PillCell, getStyles } from './PillCell'; + +describe('PillCell', () => { + let pillClass: string; + let restoreThemeContext: () => void; + + beforeEach(() => { + pillClass = getStyles(createTheme()).pill; + restoreThemeContext = mockThemeContext(createTheme()); + }); + + afterEach(() => { + restoreThemeContext(); + }); + + const mockCellOptions: TablePillCellOptions = { + type: TableCellDisplayMode.Pill, + }; + + const mockField: Field = { + name: 'test', + type: FieldType.string, + values: [], + config: {}, + }; + + const mockFrame: DataFrame = { + name: 'test', + fields: [mockField], + length: 1, + }; + + const defaultProps = { + field: mockField, + justifyContent: 'flex-start' as const, + cellOptions: mockCellOptions, + rowIdx: 0, + frame: mockFrame, + height: 30, + width: 100, + theme: {} as GrafanaTheme2, + cellInspect: false, + showFilters: false, + }; + + const ser = new XMLSerializer(); + + const expectHTML = (result: RenderResult, expected: string) => { + let actual = ser.serializeToString(result.asFragment()).replace(/xmlns=".*?" /g, ''); + expect(actual).toEqual(expected.replace(/^\s*|\n/gm, '')); + }; + + // one class for lightTextPill, darkTextPill + + describe('Color by hash (classic palette)', () => { + const props = { ...defaultProps }; + + it('single value', () => { + expectHTML( + render(), + `value1` + ); + }); + + it('empty string', () => { + expectHTML(render(), ''); + }); + + // it('null', () => { + // expectHTML( + // render(), + // 'value1' + // ); + // }); + + it('CSV values', () => { + expectHTML( + render(), + ` + value1 + value2 + value3 + ` + ); + }); + + it('JSON array values', () => { + expectHTML( + render(), + ` + value1 + value2 + value3 + ` + ); + }); + + // TODO: handle null values? + }); + + describe('Color by value mappings', () => { + const field: Field = { + ...mockField, + config: { + ...mockField.config, + mappings: [ + { + type: MappingType.ValueToText, + options: { + success: { color: '#00FF00' }, + error: { color: '#FF0000' }, + warning: { color: '#FFFF00' }, + }, + }, + ], + }, + display: (value: unknown) => ({ + text: String(value), + color: + value === 'success' ? '#00FF00' : value === 'error' ? '#FF0000' : value === 'warning' ? '#FFFF00' : '#FF780A', + numeric: 0, + }), + }; + + const props = { + ...defaultProps, + field, + }; + + it('CSV values', () => { + expectHTML( + render(), + ` + success + error + warning + unknown + ` + ); + }); + + // TODO: handle null values? + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx new file mode 100644 index 00000000000..c1ca23b7f69 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx @@ -0,0 +1,106 @@ +import { css } from '@emotion/css'; +import { useMemo } from 'react'; + +import { + GrafanaTheme2, + classicColors, + colorManipulator, + Field, + getColorByStringHash, + FALLBACK_COLOR, +} from '@grafana/data'; +import { FieldColorModeId } from '@grafana/schema'; + +import { useStyles2, useTheme2 } from '../../../../themes/ThemeContext'; +import { TableCellRendererProps } from '../types'; + +interface Pill { + value: string; + key: string; + bgColor: string; + color: string; +} + +function createPills(pillValues: string[], field: Field, theme: GrafanaTheme2): Pill[] { + return pillValues.map((pill, index) => { + const bgColor = getPillColor(pill, field, theme); + const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000'; + return { + value: pill, + key: `${pill}-${index}`, + bgColor, + color: textColor, + }; + }); +} + +export function PillCell({ value, field }: TableCellRendererProps) { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + + const pills: Pill[] = useMemo(() => { + const pillValues = inferPills(String(value)); + return createPills(pillValues, field, theme); + }, [value, field, theme]); + + return pills.map((pill) => ( + + {pill.value} + + )); +} + +const SPLIT_RE = /\s*,\s*/; +const TRANSPARENT = 'rgba(0,0,0,0)'; + +export function inferPills(value: string): string[] { + if (value === '') { + return []; + } + + if (value[0] === '[') { + try { + return JSON.parse(value); + } catch { + return value.trim().split(SPLIT_RE); + } + } + + return value.trim().split(SPLIT_RE); +} + +function getPillColor(value: string, field: Field, theme: GrafanaTheme2): string { + const cfg = field.config; + + if (cfg.mappings?.length ?? 0 > 0) { + return field.display!(value).color ?? FALLBACK_COLOR; + } + + if (cfg.color?.mode === FieldColorModeId.Fixed) { + return theme.visualization.getColorByName(cfg.color?.fixedColor ?? FALLBACK_COLOR); + } + + // TODO: instead of classicColors we need to pull colors from theme, same way as FieldColorModeId.PaletteClassicByName (see fieldColor.ts) + return getColorByStringHash(classicColors, value); +} + +export const getStyles = (theme: GrafanaTheme2) => ({ + pill: css({ + display: 'inline-block', + padding: theme.spacing(0.25, 0.75), + marginInlineEnd: theme.spacing(0.5), + marginBlock: theme.spacing(0.5), + borderRadius: theme.shape.radius.default, + fontSize: theme.typography.bodySmall.fontSize, + lineHeight: theme.typography.bodySmall.lineHeight, + whiteSpace: 'nowrap', + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx index eb67a369cb2..c66ed4be81c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx @@ -1,5 +1,5 @@ -import { WKT } from 'ol/format'; -import { Geometry } from 'ol/geom'; +import WKT from 'ol/format/WKT'; +import Geometry from 'ol/geom/Geometry'; import { FieldType } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -24,7 +24,10 @@ export function TableCellActions(props: TableCellActionsProps) { } = props; return ( -
+ // stopping propagation to prevent clicks within the actions menu from triggering the cell click events + // for things like the data links tooltip. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
ev.stopPropagation()}> {cellInspect && ( { expect(container).toBeInTheDocument(); expect(container.childNodes).toHaveLength(1); }); + + it('should return AutoCell when cellOptions is unmapped', () => { + const field = createField(FieldType.string); + + const { container } = renderCell(field, { type: 'number' } as unknown as TableCellOptions); + expect(container).toBeInTheDocument(); + expect(container.childNodes).toHaveLength(1); + }); }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx index 7bb4b39f2cb..fe4bc7e3f0c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx @@ -12,6 +12,7 @@ import { DataLinksCell } from './DataLinksCell'; import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; import { JSONCell } from './JSONCell'; +import { PillCell } from './PillCell'; import { SparklineCell } from './SparklineCell'; export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode; @@ -24,19 +25,11 @@ const GAUGE_RENDERER: TableCellRenderer = (props) => ( height={props.height} width={props.width} rowIdx={props.rowIdx} - actions={props.actions} /> ); const AUTO_RENDERER: TableCellRenderer = (props) => ( - + ); const SPARKLINE_RENDERER: TableCellRenderer = (props) => ( @@ -52,13 +45,7 @@ const SPARKLINE_RENDERER: TableCellRenderer = (props) => ( ); const JSON_RENDERER: TableCellRenderer = (props) => ( - + ); const GEO_RENDERER: TableCellRenderer = (props) => ( @@ -73,13 +60,16 @@ const IMAGE_RENDERER: TableCellRenderer = (props) => ( justifyContent={props.justifyContent} value={props.value} rowIdx={props.rowIdx} - actions={props.actions} /> ); const DATA_LINKS_RENDERER: TableCellRenderer = (props) => ; -const ACTIONS_RENDERER: TableCellRenderer = (props) => ; +const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => ( + +); + +const PILL_RENDERER: TableCellRenderer = (props) => ; function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions { return options.type === TableCellDisplayMode.Custom; @@ -104,6 +94,7 @@ const CELL_RENDERERS: Record = { [TableCellDisplayMode.ColorText]: AUTO_RENDERER, [TableCellDisplayMode.ColorBackground]: AUTO_RENDERER, [TableCellDisplayMode.Auto]: AUTO_RENDERER, + [TableCellDisplayMode.Pill]: PILL_RENDERER, }; /** @internal */ @@ -112,7 +103,13 @@ export function getCellRenderer(field: Field, cellOptions: TableCellOptions): Ta if (cellType === TableCellDisplayMode.Auto) { return getAutoRendererResult(field); } - return CELL_RENDERERS[cellType]; + + // TODO: add support boolean, enum, (maybe int). but for now just string fields + if (cellType === TableCellDisplayMode.Pill && field.type !== FieldType.string) { + return AUTO_RENDERER; + } + + return CELL_RENDERERS[cellType] ?? AUTO_RENDERER; } /** @internal */ diff --git a/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx b/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx index acde579e401..6f2c55fc3d7 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useMemo, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data'; @@ -19,9 +19,19 @@ interface Props { field?: Field; crossFilterOrder: string[]; crossFilterRows: { [key: string]: TableRow[] }; + iconClassName?: string; } -export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder, crossFilterRows }: Props) => { +export const Filter = ({ + name, + rows, + filter, + setFilter, + field, + crossFilterOrder, + crossFilterRows, + iconClassName, +}: Props) => { const filterValue = filter[name]?.filtered; // get rows for cross filtering @@ -42,13 +52,13 @@ export const Filter = ({ name, rows, filter, setFilter, field, crossFilterOrder, const ref = useRef(null); const [isPopoverVisible, setPopoverVisible] = useState(false); const styles = useStyles2(getStyles); - const filterEnabled = useMemo(() => Boolean(filterValue), [filterValue]); + const filterEnabled = Boolean(filterValue); const [searchFilter, setSearchFilter] = useState(filter[name]?.searchFilter || ''); const [operator, setOperator] = useState>(filter[name]?.operator || REGEX_OPERATOR); return (
+ ); + } return ( {...commonDataGridProps} - className={cx(styles.grid, styles.gridNested)} + className={clsx(styles.grid, styles.gridNested)} + headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: !hasNestedHeaders })} + headerRowHeight={hasNestedHeaders ? defaultHeaderHeight : 0} columns={nestedColumns} rows={expandedRecords} renderers={{ renderRow, renderCell: renderCellRoot }} @@ -474,24 +561,24 @@ export function TableNG(props: TableNGProps) { return result; }, [ + applyToRowBgFn, availableWidth, commonDataGridProps, crossFilterOrder, crossFilterRows, data, + defaultHeaderHeight, defaultRowHeight, enableSharedCrosshair, expandedRows, filter, footerCalcs, - getActions, hasNestedFrames, isCountRowsSet, onCellFilterAdded, panelContext, - replaceVariables, - rows, rowHeight, + rows, setFilter, showTypeIcons, sortColumns, @@ -499,10 +586,7 @@ export function TableNG(props: TableNGProps) { theme, visibleFields, widths, - applyToRowBgFn, - columnTypes, - height, - textWraps, + getCellActions, ]); // invalidate columns on every structureRev change. this supports width editing in the fieldConfig. @@ -518,13 +602,36 @@ export function TableNG(props: TableNGProps) { return cellRootRenderers[props.column.key](key, props); }; + const [tooltipState, setTooltipState] = useState(); + return ( <> {...commonDataGridProps} + ref={gridRef} className={styles.grid} columns={structureRevColumns} rows={paginatedRows} + headerRowClass={clsx(styles.headerRow, { [styles.displayNone]: noHeader })} + headerRowHeight={headerHeight} + onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => { + // Note: could be column.field; JS says yes, but TS says no! + const field = columns[column.idx].field; + + if (colsWithTooltip[getDisplayName(field)]) { + const rowIdx = row.__index; + setTooltipState({ + coords: { + clientX, + clientY, + }, + links: getCellLinks(field, rowIdx), + actions: getCellActions(field, rowIdx), + }); + + preventGridDefault(); + } + }} onCellKeyDown={ hasNestedFrames ? (_, event) => { @@ -561,6 +668,15 @@ export function TableNG(props: TableNGProps) {
)} + {tooltipState && ( + setTooltipState(undefined)} + /> + )} + {isContextMenuOpen && ( ({ grid: css({ - '--rdg-background-color': theme.colors.background.primary, - '--rdg-header-background-color': theme.colors.background.primary, - '--rdg-border-color': theme.isDark ? '#282b30' : '#ebebec', + '--rdg-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-border-color': theme.colors.border.weak, '--rdg-color': theme.colors.text.primary, // note: this cannot have any transparency since default cells that // overlay/overflow on hover inherit this background and need to occlude cells below - '--rdg-row-hover-background-color': theme.isDark ? '#212428' : '#f4f5f5', + '--rdg-row-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-row-hover-background-color': transparent + ? theme.colors.background.primary + : theme.colors.background.secondary, // TODO: magic 32px number is unfortunate. it would be better to have the content // flow using flexbox rather than hard-coding this size via a calc @@ -669,15 +788,24 @@ const getGridStyles = ( }), gridNested: css({ height: '100%', - width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - 1}px)`, + width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`, overflow: 'visible', - marginLeft: COLUMN.EXPANDER_WIDTH - 1, + marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1, + marginBlock: TABLE.CELL_PADDING, }), cellNested: css({ '&[aria-selected=true]': { outline: 'none', }, }), + noDataNested: css({ + height: TABLE.NESTED_NO_DATA_HEIGHT, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.colors.text.secondary, + fontSize: theme.typography.h4.fontSize, + }), cellActions: css({ display: 'none', position: 'absolute', @@ -685,7 +813,7 @@ const getGridStyles = ( margin: 'auto', height: '100%', color: theme.colors.text.primary, - background: theme.isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.7)', + background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', padding: theme.spacing.x0_5, paddingInlineStart: theme.spacing.x1, }), @@ -698,7 +826,13 @@ const getGridStyles = ( headerRow: css({ paddingBlockStart: 0, fontWeight: 'normal', - ...(noHeader && { display: 'none' }), + '& .rdg-cell': { + height: '100%', + alignItems: 'flex-end', + }, + }), + displayNone: css({ + display: 'none', }), paginationContainer: css({ alignItems: 'center', @@ -730,40 +864,43 @@ const getFooterStyles = (justifyContent: Property.JustifyContent) => ({ }), }); -const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({ - headerCell: css({ +const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => + css({ display: 'flex', gap: theme.spacing(0.5), zIndex: theme.zIndex.tooltip - 1, paddingInline: TABLE.CELL_PADDING, - paddingBlock: TABLE.CELL_PADDING, - borderInlineEnd: 'none', + paddingBlockEnd: TABLE.CELL_PADDING, justifyContent, - }), -}); - -const getCellStyles = ( - theme: GrafanaTheme2, - field: Field, - rowHeight: number, - shouldWrap: boolean, - shouldOverflow: boolean, - colors: CellColors -) => ({ - cell: css({ - textOverflow: 'initial', - background: colors.bgColor ?? 'inherit', - alignContent: 'center', - justifyContent: getTextAlign(field), - paddingInline: TABLE.CELL_PADDING, - height: '100%', - minHeight: rowHeight, // min height interacts with the fit-content property on the overflow container - ...(shouldWrap && { whiteSpace: 'pre-line' }), '&:last-child': { borderInlineEnd: 'none', }, + }); + +const getCellStyles = ( + theme: GrafanaTheme2, + justifyContent: Property.JustifyContent, + shouldWrap: boolean, + shouldOverflow: boolean, + hasTooltip: boolean, + isColorized: boolean +) => + css({ + display: 'flex', + alignItems: 'center', + justifyContent, + paddingInline: TABLE.CELL_PADDING, + minHeight: '100%', + backgroundClip: 'padding-box !important', // helps when cells have a bg color + ...(shouldWrap && { whiteSpace: 'pre-line' }), + ...(hasTooltip && { cursor: 'pointer' }), + + '&:last-child': { + borderInlineEnd: 'none', + }, + + // should omit if no cell actions, and no shouldOverflow '&:hover': { - background: colors.bgHoverColor, '.table-cell-actions': { display: 'flex', }, @@ -774,5 +911,20 @@ const getCellStyles = ( minWidth: 'fit-content', }), }, - }), -}); + + [hasTooltip ? '&' : 'a']: { + cursor: 'pointer', + ...(isColorized + ? { + color: 'inherit', + textDecoration: 'underline', + } + : { + color: theme.colors.text.link, + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline', + }, + }), + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/constants.ts b/packages/grafana-ui/src/components/Table/TableNG/constants.ts index 34490833395..31a537136d2 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/constants.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/constants.ts @@ -9,9 +9,11 @@ export const COLUMN = { /** Table layout and display constants */ export const TABLE = { CELL_PADDING: 6, - HEADER_ROW_HEIGHT: 28, MAX_CELL_HEIGHT: 48, PAGINATION_LIMIT: 750, SCROLL_BAR_WIDTH: 8, SCROLL_BAR_MARGIN: 2, + LINE_HEIGHT: 22, + NESTED_NO_DATA_HEIGHT: 60, + BORDER_RIGHT: 0.666667, }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts index 51c702e5d60..094804a3a28 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts @@ -1,9 +1,23 @@ import { act, renderHook } from '@testing-library/react'; +import { varPreLine } from 'uwrap'; import { Field, FieldType } from '@grafana/data'; -import { useFilteredRows, usePaginatedRows, useSortedRows, useFooterCalcs } from './hooks'; -import { getColumnTypes } from './utils'; +import { + useFilteredRows, + usePaginatedRows, + useSortedRows, + useFooterCalcs, + useHeaderHeight, + useTypographyCtx, +} from './hooks'; + +jest.mock('uwrap', () => ({ + // ...jest.requireActual('uwrap'), + varPreLine: jest.fn(() => ({ + count: jest.fn(() => 1), + })), +})); describe('TableNG hooks', () => { function setupData() { @@ -87,10 +101,8 @@ describe('TableNG hooks', () => { describe('useSortedRows', () => { it('should correctly set up the table with an initial sort', () => { const { fields, rows } = setupData(); - const columnTypes = getColumnTypes(fields); const { result } = renderHook(() => useSortedRows(rows, fields, { - columnTypes, hasNestedFrames: false, initialSortBy: [{ displayName: 'age', desc: false }], }) @@ -103,10 +115,8 @@ describe('TableNG hooks', () => { it('should change the sort on setSortColumns', () => { const { fields, rows } = setupData(); - const columnTypes = getColumnTypes(fields); const { result } = renderHook(() => useSortedRows(rows, fields, { - columnTypes, hasNestedFrames: false, initialSortBy: [{ displayName: 'age', desc: false }], }) @@ -134,7 +144,14 @@ describe('TableNG hooks', () => { it('should return defaults for pagination values when pagination is disabled', () => { const { rows } = setupData(); const { result } = renderHook(() => - usePaginatedRows(rows, { rowHeight: 30, height: 300, width: 800, enabled: false }) + usePaginatedRows(rows, { + rowHeight: 30, + height: 300, + width: 800, + enabled: false, + headerHeight: 28, + footerHeight: 0, + }) ); expect(result.current.page).toBe(-1); @@ -153,6 +170,39 @@ describe('TableNG hooks', () => { height: 60, width: 800, rowHeight: 10, + headerHeight: 0, + footerHeight: 0, + }) + ); + + expect(result.current.page).toBe(0); + expect(result.current.rowsPerPage).toBe(2); + expect(result.current.pageRangeStart).toBe(1); + expect(result.current.pageRangeEnd).toBe(2); + expect(result.current.rows.length).toBe(2); + + act(() => { + result.current.setPage(1); + }); + + expect(result.current.page).toBe(1); + expect(result.current.rowsPerPage).toBe(2); + expect(result.current.pageRangeStart).toBe(3); + expect(result.current.pageRangeEnd).toBe(3); + expect(result.current.rows.length).toBe(1); + }); + + it('should handle header and footer correctly', () => { + // with the numbers provided here, we have 3 rows, with 2 rows per page, over 2 pages total. + const { rows } = setupData(); + const { result } = renderHook(() => + usePaginatedRows(rows, { + enabled: true, + height: 140, + width: 800, + rowHeight: 10, + headerHeight: 28, + footerHeight: 45, }) ); @@ -332,4 +382,154 @@ describe('TableNG hooks', () => { expect(result.current).toEqual(['Total', '6', '13']); }); }); + + describe('useHeaderHeight', () => { + it('should return 0 when no header is present', () => { + const { fields } = setupData(); + const { result } = renderHook(() => { + const typographyCtx = useTypographyCtx(); + return useHeaderHeight({ + fields, + columnWidths: [], + enabled: false, + typographyCtx, + defaultHeight: 28, + sortColumns: [], + }); + }); + expect(result.current).toBe(0); + }); + + it('should return the default height when wrap is disabled', () => { + const { fields } = setupData(); + const { result } = renderHook(() => { + const typographyCtx = useTypographyCtx(); + return useHeaderHeight({ + fields, + columnWidths: [], + enabled: true, + typographyCtx, + defaultHeight: 28, + sortColumns: [], + }); + }); + expect(result.current).toBe(22); + }); + + it('should return the appropriate height for wrapped text', () => { + // Simulate 2 lines of text + jest.mocked(varPreLine).mockReturnValue({ + count: jest.fn(() => 2), + each: jest.fn(), + split: jest.fn(), + test: jest.fn(), + }); + + const { fields } = setupData(); + const { result } = renderHook(() => { + const typographyCtx = useTypographyCtx(); + return useHeaderHeight({ + fields: fields.map((field) => { + if (field.name === 'name') { + return { + ...field, + name: 'Longer name that needs wrapping', + config: { + ...field.config, + custom: { + ...field.config?.custom, + wrapHeaderText: true, + }, + }, + }; + } + return field; + }), + columnWidths: [100, 100, 100], + enabled: true, + typographyCtx: { ...typographyCtx, avgCharWidth: 5 }, + defaultHeight: 28, + sortColumns: [], + }); + }); + + expect(result.current).toBe(50); + }); + + it('should calculate the available width for a header cell based on the icons rendered within it', () => { + const countFn = jest.fn(() => 1); + + // Simulate 2 lines of text + jest.mocked(varPreLine).mockReturnValue({ + count: countFn, + each: jest.fn(), + split: jest.fn(), + test: jest.fn(), + }); + + const { fields } = setupData(); + + renderHook(() => { + const typographyCtx = useTypographyCtx(); + + return useHeaderHeight({ + fields: fields.map((field) => { + if (field.name === 'name') { + return { + ...field, + name: 'Longer name that needs wrapping', + config: { + ...field.config, + custom: { + ...field.config?.custom, + wrapHeaderText: true, + }, + }, + }; + } + return field; + }), + columnWidths: [100, 100, 100], + enabled: true, + typographyCtx: { ...typographyCtx, avgCharWidth: 10 }, + defaultHeight: 28, + sortColumns: [], + showTypeIcons: false, + }); + }); + + expect(countFn).toHaveBeenCalledWith('Longer name that needs wrapping', 87); + + renderHook(() => { + const typographyCtx = useTypographyCtx(); + return useHeaderHeight({ + fields: fields.map((field) => { + if (field.name === 'name') { + return { + ...field, + name: 'Longer name that needs wrapping', + config: { + ...field.config, + custom: { + ...field.config?.custom, + filterable: true, + wrapHeaderText: true, + }, + }, + }; + } + return field; + }), + columnWidths: [100, 100, 100], + enabled: true, + typographyCtx: { ...typographyCtx, avgCharWidth: 10 }, + defaultHeight: 28, + sortColumns: [{ columnKey: 'Longer name that needs wrapping', direction: 'ASC' }], + showTypeIcons: true, + }); + }); + + expect(countFn).toHaveBeenCalledWith('Longer name that needs wrapping', 27); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 865a4d01c67..c4bcabe0828 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -1,14 +1,24 @@ -import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect } from 'react'; -import { Column, DataGridProps, SortColumn } from 'react-data-grid'; +import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, RefObject } from 'react'; +import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; +import { varPreLine } from 'uwrap'; -import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data'; +import { Field, fieldReducers, FieldType, formattedValueToString, LinkModel, reduceField } from '@grafana/data'; import { useTheme2 } from '../../../themes/ThemeContext'; import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types'; import { TABLE } from './constants'; -import { ColumnTypes, FilterType, TableFooterCalc, TableRow, TableSortByFieldState, TableSummaryRow } from './types'; -import { getDisplayName, processNestedTableRows, getCellHeightCalculator, applySort, getCellOptions } from './utils'; +import { FilterType, TableFooterCalc, TableRow, TableSortByFieldState, TableSummaryRow } from './types'; +import { + getDisplayName, + processNestedTableRows, + applySort, + getCellOptions, + getColumnTypes, + GetMaxWrapCellOptions, + getMaxWrapCell, + getCellLinks, +} from './utils'; // Helper function to get displayed value const getDisplayedValue = (row: TableRow, key: string, fields: Field[]) => { @@ -79,7 +89,6 @@ export function useFilteredRows( } export interface SortedRowsOptions { - columnTypes: ColumnTypes; hasNestedFrames: boolean; initialSortBy?: TableSortByFieldState[]; } @@ -93,7 +102,7 @@ export interface SortedRowsResult { export function useSortedRows( rows: TableRow[], fields: Field[], - { initialSortBy, columnTypes, hasNestedFrames }: SortedRowsOptions + { initialSortBy, hasNestedFrames }: SortedRowsOptions ): SortedRowsResult { const initialSortColumns = useMemo( () => @@ -111,6 +120,7 @@ export function useSortedRows( [] // eslint-disable-line react-hooks/exhaustive-deps ); const [sortColumns, setSortColumns] = useState(initialSortColumns); + const columnTypes = useMemo(() => getColumnTypes(fields), [fields]); const sortedRows = useMemo( () => applySort(rows, fields, sortColumns, columnTypes, hasNestedFrames), @@ -128,10 +138,10 @@ export interface PaginatedRowsOptions { height: number; width: number; rowHeight: number | ((row: TableRow) => number); - hasHeader?: boolean; - hasFooter?: boolean; + headerHeight: number; + footerHeight: number; paginationHeight?: number; - enabled?: boolean; + enabled: boolean; } export interface PaginatedRowsResult { @@ -150,7 +160,7 @@ const PAGINATION_HEIGHT = 38; export function usePaginatedRows( rows: TableRow[], - { height, width, hasHeader, hasFooter, rowHeight, enabled }: PaginatedRowsOptions + { height, width, headerHeight, footerHeight, rowHeight, enabled }: PaginatedRowsOptions ): PaginatedRowsResult { // TODO: allow persisted page selection via url const [page, setPage] = useState(0); @@ -158,11 +168,17 @@ export function usePaginatedRows( // calculate average row height if row height is variable. const avgRowHeight = useMemo(() => { + if (!enabled) { + return 0; + } + if (typeof rowHeight === 'number') { return rowHeight; } - return rows.reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0); - }, [rows, rowHeight]); + + // we'll just measure 100 rows to estimate + return rows.slice(0, 100).reduce((avg, row, _, { length }) => avg + rowHeight(row) / length, 0); + }, [rows, rowHeight, enabled]); // using dimensions of the panel, calculate pagination parameters const { numPages, rowsPerPage, pageRangeStart, pageRangeEnd, smallPagination } = useMemo((): { @@ -177,8 +193,7 @@ export function usePaginatedRows( } // calculate number of rowsPerPage based on height stack - const rowAreaHeight = - height - (hasHeader ? TABLE.HEADER_ROW_HEIGHT : 0) - (hasFooter ? avgRowHeight : 0) - PAGINATION_HEIGHT; + const rowAreaHeight = height - headerHeight - footerHeight - PAGINATION_HEIGHT; const heightPerRow = Math.floor(rowAreaHeight / (avgRowHeight || 1)); // ensure at least one row per page is displayed let rowsPerPage = heightPerRow > 1 ? heightPerRow : 1; @@ -198,7 +213,7 @@ export function usePaginatedRows( pageRangeEnd, smallPagination, }; - }, [width, height, hasHeader, hasFooter, avgRowHeight, enabled, numRows, page]); + }, [width, height, headerHeight, footerHeight, avgRowHeight, enabled, numRows, page]); // safeguard against page overflow on panel resize or other factors useEffect(() => { @@ -294,22 +309,16 @@ export function useFooterCalcs( }, [fields, enabled, footerOptions, isCountRowsSet, rows]); } -export function useTextWraps(fields: Field[]): Record { - return useMemo( - () => - fields.reduce<{ [key: string]: boolean }>((acc, field) => { - const cellOptions = getCellOptions(field); - const displayName = getDisplayName(field); - const wrapText = 'wrapText' in cellOptions && cellOptions.wrapText; - return { ...acc, [displayName]: !!wrapText }; - }, {}), - [fields] - ); +interface TypographyCtx { + ctx: CanvasRenderingContext2D; + font: string; + avgCharWidth: number; + calcRowHeight: (text: string, cellWidth: number, defaultHeight: number) => number; } -export function useTypographyCtx() { +export function useTypographyCtx(): TypographyCtx { const theme = useTheme2(); - const { ctx, font, avgCharWidth } = useMemo(() => { + const typographyCtx = useMemo((): TypographyCtx => { const font = `${theme.typography.fontSize}px ${theme.typography.fontFamily}`; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d')!; @@ -322,23 +331,129 @@ export function useTypographyCtx() { "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"; const txtWidth = ctx.measureText(txt).width; const avgCharWidth = txtWidth / txt.length + letterSpacing; + const { count } = varPreLine(ctx); + + const calcRowHeight = (text: string, cellWidth: number, defaultHeight: number) => { + if (text === '') { + return defaultHeight; + } + const numLines = count(text, cellWidth); + const totalHeight = numLines * TABLE.LINE_HEIGHT + 2 * TABLE.CELL_PADDING; + return Math.max(totalHeight, defaultHeight); + }; return { + calcRowHeight, ctx, font, avgCharWidth, }; }, [theme.typography.fontSize, theme.typography.fontFamily]); - return { ctx, font, avgCharWidth }; + return typographyCtx; } -export function useRowHeight( - columnWidths: number[], - fields: Field[], - hasNestedFrames: boolean, - defaultRowHeight: number, - expandedRows: Record -): number | ((row: TableRow) => number) { +const ICON_WIDTH = 16; +const ICON_GAP = 4; + +interface UseHeaderHeightOptions { + enabled: boolean; + fields: Field[]; + columnWidths: number[]; + defaultHeight: number; + sortColumns: SortColumn[]; + typographyCtx: TypographyCtx; + showTypeIcons?: boolean; +} + +export function useHeaderHeight({ + fields, + enabled, + columnWidths, + defaultHeight, + sortColumns, + typographyCtx: { calcRowHeight, avgCharWidth }, + showTypeIcons = false, +}: UseHeaderHeightOptions): number { + const perIconSpace = ICON_WIDTH + ICON_GAP; + const columnAvailableWidths = useMemo( + () => + columnWidths.map((c, idx) => { + let width = c - 2 * TABLE.CELL_PADDING - TABLE.BORDER_RIGHT; + // filtering icon + if (fields[idx]?.config?.custom?.filterable) { + width -= perIconSpace; + } + // sorting icon + if (sortColumns.some((col) => col.columnKey === getDisplayName(fields[idx]))) { + width -= perIconSpace; + } + // type icon + if (showTypeIcons) { + width -= perIconSpace; + } + return Math.floor(width); + }), + [fields, columnWidths, sortColumns, showTypeIcons, perIconSpace] + ); + + const [wrappedColHeaderIdxs, hasWrappedColHeaders] = useMemo(() => { + let hasWrappedColHeaders = false; + return [ + fields.map((field) => { + const wrapText = field.config?.custom?.wrapHeaderText ?? false; + if (wrapText) { + hasWrappedColHeaders = true; + } + return wrapText; + }), + hasWrappedColHeaders, + ]; + }, [fields]); + + const maxWrapCellOptions = useMemo( + () => ({ + colWidths: columnAvailableWidths, + avgCharWidth, + wrappedColIdxs: wrappedColHeaderIdxs, + }), + [columnAvailableWidths, avgCharWidth, wrappedColHeaderIdxs] + ); + + // TODO: is there a less clunky way to subtract the top padding value? + const headerHeight = useMemo(() => { + if (!enabled) { + return 0; + } + if (!hasWrappedColHeaders) { + return defaultHeight - TABLE.CELL_PADDING; + } + + const { text: maxLinesText, idx: maxLinesIdx } = getMaxWrapCell(fields, -1, maxWrapCellOptions); + return calcRowHeight(maxLinesText, columnAvailableWidths[maxLinesIdx], defaultHeight) - TABLE.CELL_PADDING; + }, [fields, enabled, hasWrappedColHeaders, maxWrapCellOptions, calcRowHeight, columnAvailableWidths, defaultHeight]); + + return headerHeight; +} + +interface UseRowHeightOptions { + columnWidths: number[]; + fields: Field[]; + hasNestedFrames: boolean; + defaultHeight: number; + headerHeight: number; + expandedRows: Record; + typographyCtx: TypographyCtx; +} + +export function useRowHeight({ + columnWidths, + fields, + hasNestedFrames, + defaultHeight, + headerHeight, + expandedRows, + typographyCtx: { calcRowHeight, avgCharWidth }, +}: UseRowHeightOptions): number | ((row: TableRow) => number) { const [wrappedColIdxs, hasWrappedCols] = useMemo(() => { let hasWrappedCols = false; return [ @@ -360,22 +475,26 @@ export function useRowHeight( ]; }, [fields]); - const { ctx, avgCharWidth } = useTypographyCtx(); + const colWidths = useMemo( + () => columnWidths.map((c) => c - 2 * TABLE.CELL_PADDING - TABLE.BORDER_RIGHT), + [columnWidths] + ); + + const maxWrapCellOptions = useMemo( + () => ({ + colWidths, + avgCharWidth, + wrappedColIdxs, + }), + [colWidths, avgCharWidth, wrappedColIdxs] + ); const rowHeight = useMemo(() => { // row height is only complicated when there are nested frames or wrapped columns. if (!hasNestedFrames && !hasWrappedCols) { - return defaultRowHeight; + return defaultHeight; } - const HPADDING = TABLE.CELL_PADDING; - const VPADDING = TABLE.CELL_PADDING; - const BORDER_RIGHT = 0.666667; - const LINE_HEIGHT = 22; - - const wrapWidths = columnWidths.map((c) => c - 2 * HPADDING - BORDER_RIGHT); - const calc = getCellHeightCalculator(ctx, LINE_HEIGHT, defaultRowHeight, VPADDING); - return (row: TableRow) => { // nested rows if (Number(row.__depth) > 0) { @@ -384,50 +503,28 @@ export function useRowHeight( return 0; } - // Ensure we have a minimum height (defaultRowHeight) for the nested table even if data is empty - const headerCount = row?.data?.meta?.custom?.noHeader ? 0 : 1; const rowCount = row.data?.length ?? 0; - return Math.max(defaultRowHeight, defaultRowHeight * (rowCount + headerCount)); + if (rowCount === 0) { + return TABLE.NESTED_NO_DATA_HEIGHT + TABLE.CELL_PADDING * 2; + } + + const nestedHeaderHeight = row.data?.meta?.custom?.noHeader ? 0 : defaultHeight; + return Math.max(defaultHeight, defaultHeight * rowCount + nestedHeaderHeight + TABLE.CELL_PADDING * 2); } // regular rows - let maxLines = 1; - let maxLinesIdx = -1; - let maxLinesText = ''; - - for (let i = 0; i < columnWidths.length; i++) { - if (wrappedColIdxs[i]) { - const cellTextRaw = fields[i].values[row.__index]; - if (cellTextRaw != null) { - const cellText = String(cellTextRaw); - const charsPerLine = wrapWidths[i] / avgCharWidth; - const approxLines = cellText.length / charsPerLine; - - if (approxLines > maxLines) { - maxLines = approxLines; - maxLinesIdx = i; - maxLinesText = cellText; - } - } - } - } - - if (maxLinesIdx === -1) { - return defaultRowHeight; - } - - return calc(maxLinesText, wrapWidths[maxLinesIdx]); + const { text: maxLinesText, idx: maxLinesIdx } = getMaxWrapCell(fields, row.__index, maxWrapCellOptions); + return calcRowHeight(maxLinesText, colWidths[maxLinesIdx], defaultHeight); }; }, [ - avgCharWidth, - columnWidths, - ctx, - defaultRowHeight, + calcRowHeight, + defaultHeight, expandedRows, fields, hasNestedFrames, hasWrappedCols, - wrappedColIdxs, + maxWrapCellOptions, + colWidths, ]); return rowHeight; @@ -504,3 +601,24 @@ export function useColumnResize( return dataGridResizeHandler; } + +export function useSingleLink(field: Field, rowIdx: number): LinkModel | undefined { + const linksCount = field.config.links?.length ?? 0; + const actionsCount = field.config.actions?.length ?? 0; + const shouldShowLink = linksCount === 1 && actionsCount === 0; + return useMemo(() => (shouldShowLink ? (getCellLinks(field, rowIdx) ?? []) : [])[0], [field, shouldShowLink, rowIdx]); +} + +export function useScrollbarWidth(ref: RefObject, height: number, renderedRows: TableRow[]) { + const [scrollbarWidth, setScrollbarWidth] = useState(0); + + useLayoutEffect(() => { + const el = ref.current?.element; + + if (el) { + setScrollbarWidth(el.offsetWidth - el.clientWidth); + } + }, [ref, height, renderedRows]); + + return scrollbarWidth; +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 2946923db8f..742d495b806 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -10,7 +10,6 @@ import { TimeRange, FieldConfigSource, ActionModel, - InterpolateFunction, FieldType, DataFrameWithValue, SelectableValue, @@ -30,12 +29,9 @@ export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void; export type FooterItem = Array> | string | undefined; -export type GetActionsFunction = ( - frame: DataFrame, - field: Field, - rowIndex: number, - replaceVariables?: InterpolateFunction -) => ActionModel[]; +export type GetActionsFunction = (frame: DataFrame, field: Field, rowIndex: number) => ActionModel[]; + +export type GetActionsFunctionLocal = (field: Field, rowIndex: number) => ActionModel[]; export type TableFieldOptionsType = Omit & { cellOptions: TableCellOptions; @@ -135,6 +131,7 @@ export interface BaseTableProps { enablePagination?: boolean; cellHeight?: TableCellHeight; structureRev?: number; + transparent?: boolean; /** @alpha Used by SparklineCell when provided */ timeRange?: TimeRange; enableSharedCrosshair?: boolean; @@ -142,7 +139,6 @@ export interface BaseTableProps { initialRowIndex?: number; fieldConfig?: FieldConfigSource; getActions?: GetActionsFunction; - replaceVariables?: InterpolateFunction; // Used solely for testing as RTL can't correctly render the table otherwise enableVirtualization?: boolean; } @@ -151,7 +147,6 @@ export interface BaseTableProps { export interface TableNGProps extends BaseTableProps {} export interface TableCellRendererProps { - actions?: ActionModel[]; rowIdx: number; frame: DataFrame; timeRange?: TimeRange; @@ -165,6 +160,7 @@ export interface TableCellRendererProps { cellInspect: boolean; showFilters: boolean; justifyContent: Property.JustifyContent; + getActions?: GetActionsFunctionLocal; } export type ContextMenuProps = { @@ -205,7 +201,7 @@ export interface SparklineCellProps { width: number; } -export interface BarGaugeCellProps extends ActionCellProps { +export interface BarGaugeCellProps { field: Field; height: number; rowIdx: number; @@ -214,7 +210,7 @@ export interface BarGaugeCellProps extends ActionCellProps { width: number; } -export interface ImageCellProps extends ActionCellProps { +export interface ImageCellProps { cellOptions: TableCellOptions; field: Field; height: number; @@ -223,7 +219,7 @@ export interface ImageCellProps extends ActionCellProps { rowIdx: number; } -export interface JSONCellProps extends ActionCellProps { +export interface JSONCellProps { justifyContent: Property.JustifyContent; value: TableCellValue; field: Field; @@ -241,22 +237,22 @@ export interface GeoCellProps { height: number; } -export interface ActionCellProps { - actions?: ActionModel[]; -} - export interface CellColors { textColor?: string; bgColor?: string; bgHoverColor?: string; } -export interface AutoCellProps extends ActionCellProps { - value: TableCellValue; +export interface AutoCellProps { field: Field; - justifyContent: Property.JustifyContent; + value: TableCellValue; rowIdx: number; - cellOptions: TableCellOptions; +} + +export interface ActionCellProps { + field: Field; + rowIdx: number; + getActions: GetActionsFunctionLocal; } // Comparator for sorting table values diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 74a9f39d224..59e58541744 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -28,6 +28,7 @@ import { getTextAlign, migrateTableDisplayModeToCellOptions, getColumnTypes, + getMaxWrapCell, } from './utils'; describe('TableNG utils', () => { @@ -187,7 +188,6 @@ describe('TableNG utils', () => { const colors = getCellColors(theme, field, displayValue); expect(colors.bgColor).toBe('rgb(255, 0, 0)'); expect(colors.textColor).toBe('rgb(247, 248, 250)'); - expect(colors.bgHoverColor).toBe('rgb(255, 36, 36)'); }); it('should handle color background gradient mode', () => { @@ -205,7 +205,6 @@ describe('TableNG utils', () => { const colors = getCellColors(theme, field, displayValue); expect(colors.bgColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)'); expect(colors.textColor).toBe('rgb(247, 248, 250)'); - expect(colors.bgHoverColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), rgb(255, 54, 54))'); }); }); @@ -974,11 +973,6 @@ describe('TableNG utils', () => { }); }); - describe('getCellHeightCalculator', () => { - it.todo('returns a cell height calculator'); - it.todo('returns a minimum height of the default row height'); - }); - describe('getDefaultRowHeight', () => { const theme = createTheme(); @@ -1006,4 +1000,117 @@ describe('TableNG utils', () => { expect(result).toBe(expected); }); }); + + describe('getMaxWrapCell', () => { + it('should return the maximum wrap cell length from field state', () => { + const field1: Field = { + name: 'field1', + type: FieldType.string, + config: {}, + values: ['beep boop', 'foo bar baz', 'lorem ipsum dolor sit amet'], + }; + + const field2: Field = { + name: 'field2', + type: FieldType.string, + config: {}, + values: ['asdfasdf asdfasdf asdfasdf', 'asdf asdf asdf asdf asdf', ''], + }; + + const field3: Field = { + name: 'field3', + type: FieldType.string, + config: {}, + values: ['foo', 'bar', 'baz'], + // No alignmentFactors in state + }; + + const fields = [field1, field2, field3]; + + const result = getMaxWrapCell(fields, 0, { + colWidths: [30, 50, 100], + avgCharWidth: 5, + wrappedColIdxs: [true, true, true], + }); + expect(result).toEqual({ + text: 'asdfasdf asdfasdf asdfasdf', + idx: 1, + numLines: 2.6, + }); + }); + + it('should take colWidths into account when calculating max wrap cell', () => { + const fields: Field[] = [ + { + name: 'field', + type: FieldType.string, + config: {}, + values: ['short', 'a bit longer text'], + }, + { + name: 'field', + type: FieldType.string, + config: {}, + values: ['short', 'quite a bit longer text'], + }, + { + name: 'field', + type: FieldType.string, + config: {}, + values: ['short', 'less text'], + }, + ]; + + // Simulate a narrow column width that would cause wrapping + const colWidths = [50, 1000, 30]; // 50px width + const avgCharWidth = 5; // Assume average character width is 5px + + const result = getMaxWrapCell(fields, 1, { colWidths, avgCharWidth, wrappedColIdxs: [true, true, true] }); + + // With a 50px width and 5px per character, we can fit 10 characters per line + // "the longest text in this field" has 31 characters, so it should wrap to 4 lines + expect(result).toEqual({ + idx: 0, + numLines: 1.7, + text: 'a bit longer text', + }); + }); + + it('should use the display name if the rowIdx is -1 (which is used to calc header height in wrapped rows)', () => { + const fields: Field[] = [ + { + name: 'Field with a very long name', + type: FieldType.string, + config: {}, + values: ['short', 'a bit longer text'], + }, + { + name: 'Name', + type: FieldType.string, + config: {}, + values: ['short', 'quite a bit longer text'], + }, + { + name: 'Another field', + type: FieldType.string, + config: {}, + values: ['short', 'less text'], + }, + ]; + + // Simulate a narrow column width that would cause wrapping + const colWidths = [50, 1000, 30]; // 50px width + const avgCharWidth = 5; // Assume average character width is 5px + + const result = getMaxWrapCell(fields, -1, { colWidths, avgCharWidth, wrappedColIdxs: [true, true, true] }); + + // With a 50px width and 5px per character, we can fit 10 characters per line + // "the longest text in this field" has 31 characters, so it should wrap to 4 lines + expect(result).toEqual({ idx: 0, numLines: 2.7, text: 'Field with a very long name' }); + }); + + it.todo('should ignore columns which are not wrapped'); + + it.todo('should only apply wrapping on idiomatic break characters (space, -, etc)'); + }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 79afab8b076..1cd6d279856 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -1,7 +1,6 @@ import { Property } from 'csstype'; import { SortColumn } from 'react-data-grid'; import tinycolor from 'tinycolor2'; -import { varPreLine } from 'uwrap'; import { FieldType, @@ -29,26 +28,6 @@ import { CellColors, TableRow, TableFieldOptionsType, ColumnTypes, FrameToRowsCo /* ---------------------------- Cell calculations --------------------------- */ export type CellHeightCalculator = (text: string, cellWidth: number) => number; -/** - * @internal - * Returns a function that calculates the height of a cell based on its text content and width. - */ -export function getCellHeightCalculator( - // should be pre-configured with font and letterSpacing - ctx: CanvasRenderingContext2D, - lineHeight: number, - defaultRowHeight: number, - padding = 0 -) { - const { count } = varPreLine(ctx); - - return (text: string, cellWidth: number) => { - const numLines = count(text, cellWidth); - const totalHeight = numLines * lineHeight + 2 * padding; - return Math.max(totalHeight, defaultRowHeight); - }; -} - /** * @internal * Returns the default row height based on the theme and cell height setting. @@ -69,19 +48,97 @@ export function getDefaultRowHeight(theme: GrafanaTheme2, cellHeight?: TableCell return TABLE.CELL_PADDING * 2 + bodyFontSize * lineHeight; } +/** + * @internal + * Returns true if cell inspection (hover to see full content) is enabled for the field. + */ +export function isCellInspectEnabled(field: Field): boolean { + return field.config?.custom?.inspect ?? false; +} + +/** + * @internal + * Returns true if text wrapping should be applied to the cell. + */ +export function shouldTextWrap(field: Field): boolean { + const cellOptions = getCellOptions(field); + // @ts-ignore - a handful of cellTypes have boolean wrapText, but not all of them. + // we should be very careful to only use boolean type for cellOptions.wrapText. + // TBH we will probably move this up to a field option which is showIf rendered anyway, + // but that'll be a migration to do, so it needs to happen post-GA. + return Boolean(cellOptions?.wrapText); +} + +// matches characters which CSS +const spaceRegex = /[\s-]/; + +export interface GetMaxWrapCellOptions { + colWidths: number[]; + avgCharWidth: number; + wrappedColIdxs: boolean[]; +} + +/** + * @internal + * loop through the fields and their values, determine which cell is going to determine the + * height of the row based on its content and width, and then return the text, index, and number of lines for that cell. + */ +export function getMaxWrapCell( + fields: Field[], + rowIdx: number, + { colWidths, avgCharWidth, wrappedColIdxs }: GetMaxWrapCellOptions +): { + text: string; + idx: number; + numLines: number; +} { + let maxLines = 1; + let maxLinesIdx = -1; + let maxLinesText = ''; + + // TODO: consider changing how we store this, using a record by column key instead of an array + for (let i = 0; i < colWidths.length; i++) { + if (wrappedColIdxs[i]) { + const field = fields[i]; + // special case: for the header, provide `-1` as the row index. + const cellTextRaw = rowIdx === -1 ? getDisplayName(field) : field.values[rowIdx]; + + if (cellTextRaw != null) { + const cellText = String(cellTextRaw); + + if (spaceRegex.test(cellText)) { + const charsPerLine = colWidths[i] / avgCharWidth; + const approxLines = cellText.length / charsPerLine; + + if (approxLines > maxLines) { + maxLines = approxLines; + maxLinesIdx = i; + maxLinesText = cellText; + } + } + } + } + } + + return { text: maxLinesText, idx: maxLinesIdx, numLines: maxLines }; +} + /** * @internal * Returns true if text overflow handling should be applied to the cell. */ -export function shouldTextOverflow( - fieldType: FieldType, - cellType: TableCellDisplayMode, - textWrap: boolean, - cellInspect: boolean -): boolean { - // Tech debt: Technically image cells are of type string, which is misleading (kinda?) - // so we need to ensure we don't apply overflow hover states fo type image - return fieldType === FieldType.string && cellType !== TableCellDisplayMode.Image && !textWrap && !cellInspect; +export function shouldTextOverflow(field: Field): boolean { + let type = getCellOptions(field).type; + + return ( + field.type === FieldType.string && + // Tech debt: Technically image cells are of type string, which is misleading (kinda?) + // so we need to ensure we don't apply overflow hover states for type image + type !== TableCellDisplayMode.Image && + type !== TableCellDisplayMode.Pill && + !shouldTextWrap(field) && + !isCellInspectEnabled(field) + ); } /** @@ -118,6 +175,7 @@ const DEFAULT_CELL_OPTIONS = { type: TableCellDisplayMode.Auto } as const; /** * @internal * Returns the cell options for a field, migrating from legacy displayMode if necessary. + * TODO: remove live migration in favor of doing it in dashboard or panel migrator */ export function getCellOptions(field: Field): TableCellOptions { if (field.config.custom?.displayMode) { @@ -172,7 +230,6 @@ export function getAlignmentFactor( /* ------------------------- Cell color calculation ------------------------- */ const CELL_COLOR_DARKENING_MULTIPLIER = 10; -const CELL_GRADIENT_DARKENING_MULTIPLIER = 15; const CELL_GRADIENT_HUE_ROTATION_DEGREES = 5; /** @@ -190,7 +247,7 @@ export function getCellColors( // Setup color variables let textColor: string | undefined = undefined; let bgColor: string | undefined = undefined; - let bgHoverColor: string | undefined = undefined; + // let bgHoverColor: string | undefined = undefined; if (cellOptions.type === TableCellDisplayMode.ColorText) { textColor = displayValue.color; @@ -200,23 +257,23 @@ export function getCellColors( if (mode === TableCellBackgroundDisplayMode.Basic) { textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark); bgColor = tinycolor(displayValue.color).toRgbString(); - bgHoverColor = tinycolor(displayValue.color) - .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor) - .toRgbString(); + // bgHoverColor = tinycolor(displayValue.color) + // .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor) + // .toRgbString(); } else if (mode === TableCellBackgroundDisplayMode.Gradient) { - const hoverColor = tinycolor(displayValue.color) - .darken(CELL_GRADIENT_DARKENING_MULTIPLIER * darkeningFactor) - .toRgbString(); + // const hoverColor = tinycolor(displayValue.color) + // .darken(CELL_GRADIENT_DARKENING_MULTIPLIER * darkeningFactor) + // .toRgbString(); const bgColor2 = tinycolor(displayValue.color) .darken(CELL_COLOR_DARKENING_MULTIPLIER * darkeningFactor) .spin(CELL_GRADIENT_HUE_ROTATION_DEGREES); textColor = getTextColorForAlphaBackground(displayValue.color!, theme.isDark); bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; - bgHoverColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${hoverColor})`; + // bgHoverColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${hoverColor})`; } } - return { textColor, bgColor, bgHoverColor }; + return { textColor, bgColor }; } /** @@ -559,3 +616,12 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI } } } + +/** @internal */ +export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDisplayMode) { + return ( + cellType !== TableCellDisplayMode.DataLinks && + cellType !== TableCellDisplayMode.Actions && + (field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1 + ); +} diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index e2c5b5b8099..1d029ae7099 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -57,7 +57,7 @@ export interface TableCellProps extends CellProps { onCellFilterAdded?: TableFilterActionCallback; innerWidth: number; frame: DataFrame; - actions?: ActionModel[]; + actions?: ActionModel[]; // unused in NG setInspectCell?: TableInspectCellCallback; } diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index f4c9b072b16..77dc0cec078 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -196,6 +196,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field return DataLinksCell; case TableCellDisplayMode.Actions: return ActionsCell; + case TableCellDisplayMode.Pill: + return DefaultCell; // Legacy table doesn't support pill cells, fallback to default } if (field.type === FieldType.geo) { @@ -764,10 +766,16 @@ export function guessLongestField(fieldConfig: FieldConfigSource, data: DataFram return longestField; } -export type DataLinksActionsTooltipCoords = { +export interface DataLinksActionsTooltipState { + coords: DataLinksActionsTooltipCoords; + links?: LinkModel[]; + actions?: ActionModel[]; +} + +export interface DataLinksActionsTooltipCoords { clientX: number; clientY: number; -}; +} export const getDataLinksActionsTooltipUtils = (links: LinkModel[], actions?: ActionModel[]) => { const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length); diff --git a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.internal.story.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx similarity index 92% rename from packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.internal.story.tsx rename to packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx index ccac99b8644..124dfc46af9 100644 --- a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.internal.story.tsx +++ b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx @@ -6,7 +6,7 @@ import { DataFrame } from '@grafana/data'; import { TableInputCSV } from './TableInputCSV'; const meta: Meta = { - title: 'Forms/TableInputCSV', + title: 'Inputs/Deprecated/TableInputCSV', component: TableInputCSV, }; diff --git a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx index f4a3e11e755..2a705aac90d 100644 --- a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx +++ b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.tsx @@ -101,6 +101,7 @@ export class UnThemedTableInputCSV extends PureComponent { } } +/** @deprecated */ export const TableInputCSV = withTheme2(UnThemedTableInputCSV); TableInputCSV.displayName = 'TableInputCSV'; diff --git a/packages/grafana-ui/src/components/Tabs/Tabs.story.tsx b/packages/grafana-ui/src/components/Tabs/Tabs.story.tsx index b6189591bd1..8f630d37346 100644 --- a/packages/grafana-ui/src/components/Tabs/Tabs.story.tsx +++ b/packages/grafana-ui/src/components/Tabs/Tabs.story.tsx @@ -10,7 +10,7 @@ import { TabsBar } from './TabsBar'; import mdx from './TabsBar.mdx'; const meta: Meta = { - title: 'Layout/Tabs', + title: 'Navigation/Tabs', parameters: { docs: { page: mdx, diff --git a/packages/grafana-ui/src/components/Tags/Tag.story.tsx b/packages/grafana-ui/src/components/Tags/Tag.story.tsx index 3610fa381fe..b9aa5a61632 100644 --- a/packages/grafana-ui/src/components/Tags/Tag.story.tsx +++ b/packages/grafana-ui/src/components/Tags/Tag.story.tsx @@ -5,7 +5,7 @@ import { Tag } from './Tag'; import mdx from './Tag.mdx'; const meta: Meta = { - title: 'Forms/Tags/Tag', + title: 'Information/Tag', component: Tag, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/Tags/TagList.story.tsx b/packages/grafana-ui/src/components/Tags/TagList.story.tsx index 56acb2e3f39..7d8c6094de3 100644 --- a/packages/grafana-ui/src/components/Tags/TagList.story.tsx +++ b/packages/grafana-ui/src/components/Tags/TagList.story.tsx @@ -5,7 +5,7 @@ import { TagList } from './TagList'; import mdx from './TagList.mdx'; const meta: Meta = { - title: 'Forms/Tags/TagList', + title: 'Information/TagList', component: TagList, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx index 295357be5e3..b02f40c649a 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx @@ -7,7 +7,7 @@ import { TagsInput } from './TagsInput'; import mdx from './TagsInput.mdx'; const meta: Meta = { - title: 'Forms/TagsInput', + title: 'Inputs/TagsInput', component: TagsInput, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx index 808e5245ae0..7bcdbb6852a 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx @@ -3,7 +3,7 @@ import { useCallback, useState, forwardRef } from 'react'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; import { Button } from '../Button/Button'; @@ -32,7 +32,7 @@ export interface Props { export const TagsInput = forwardRef( ( { - placeholder = 'New tag (enter key to add)', + placeholder: placeholderProp, tags = [], onChange, width, @@ -45,6 +45,7 @@ export const TagsInput = forwardRef( }, ref ) => { + const placeholder = placeholderProp ?? t('grafana-ui.tags-input.placeholder-new-tag', 'New tag (enter key to add)'); const [newTagName, setNewTagName] = useState(''); const styles = useStyles2(getStyles); const theme = useTheme2(); diff --git a/packages/grafana-ui/src/components/Text/Text.story.tsx b/packages/grafana-ui/src/components/Text/Text.story.tsx index 07071f64b02..f23056df839 100644 --- a/packages/grafana-ui/src/components/Text/Text.story.tsx +++ b/packages/grafana-ui/src/components/Text/Text.story.tsx @@ -7,7 +7,7 @@ import { Text } from './Text'; import mdx from './Text.mdx'; const meta: Meta = { - title: 'General/Text', + title: 'Foundations/Text', component: Text, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/TextArea/TextArea.story.tsx b/packages/grafana-ui/src/components/TextArea/TextArea.story.tsx index d977c73361a..337984c93dd 100644 --- a/packages/grafana-ui/src/components/TextArea/TextArea.story.tsx +++ b/packages/grafana-ui/src/components/TextArea/TextArea.story.tsx @@ -4,7 +4,7 @@ import { TextArea } from './TextArea'; import mdx from './TextArea.mdx'; const meta: Meta = { - title: 'Forms/TextArea', + title: 'Inputs/TextArea', component: TextArea, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.story.tsx similarity index 95% rename from packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx rename to packages/grafana-ui/src/components/ThemeDemos/BorderRadius.story.tsx index c938199176c..92b35ccf142 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.story.tsx @@ -3,7 +3,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { BorderRadiusContainer } from './BorderRadius'; const meta: Meta = { - title: 'Docs Overview/Theme', + title: 'Developers/Border radius', component: BorderRadiusContainer, decorators: [], parameters: { diff --git a/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.story.tsx new file mode 100644 index 00000000000..29cf50f7d6c --- /dev/null +++ b/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.story.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryFn } from '@storybook/react'; + +import { EmotionPerfTest } from './EmotionPerfTest'; + +const meta: Meta = { + title: 'Developers/Emotion perf test', + decorators: [], + parameters: { + options: { + showPanel: false, + }, + docs: {}, + }, +}; + +export const PerfTest: StoryFn = () => { + return ; +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx index edb28e35fcf..32fa7f40835 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx @@ -1,10 +1,9 @@ import { Meta, StoryFn } from '@storybook/react'; -import { EmotionPerfTest } from './EmotionPerfTest'; import { ThemeDemo as NewThemeDemoComponent } from './ThemeDemo'; const meta: Meta = { - title: 'Docs Overview/Theme', + title: 'Foundations/Theme', component: NewThemeDemoComponent, decorators: [], parameters: { @@ -19,8 +18,4 @@ export const ThemeDemo: StoryFn = () => { return ; }; -export const PerfTest: StoryFn = () => { - return ; -}; - export default meta; diff --git a/packages/grafana-ui/src/components/Text/Typography.internal.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx similarity index 97% rename from packages/grafana-ui/src/components/Text/Typography.internal.story.tsx rename to packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx index d9c347a4bf7..0bc75786461 100644 --- a/packages/grafana-ui/src/components/Text/Typography.internal.story.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx @@ -5,11 +5,10 @@ import * as React from 'react'; import { Divider } from '../Divider/Divider'; import { Field } from '../Forms/Field'; import { Stack } from '../Layout/Stack/Stack'; - -import { Text } from './Text'; +import { Text } from '../Text/Text'; const meta: Meta = { - title: 'General/Text', + title: 'Developers/Typography', }; const FONT_WEIGHTS = [/*100, 200, 300, */ 400, 500 /*600, 700, 800, 900*/]; diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx index 18234f1b0d0..04c66996a26 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx @@ -9,7 +9,7 @@ import mdx from './ToolbarButton.mdx'; import { ToolbarButtonRow } from './ToolbarButtonRow'; const meta: Meta = { - title: 'Buttons/ToolbarButton', + title: 'Navigation/ToolbarButton', component: ToolbarButton, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.story.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.story.tsx index 45bd93b0d60..144424b9c00 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.story.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButtonRow.story.tsx @@ -7,7 +7,7 @@ import { ToolbarButtonRow } from './ToolbarButtonRow'; import mdx from './ToolbarButtonRow.mdx'; const meta: Meta = { - title: 'Buttons/ToolbarButton/ToolbarButtonRow', + title: 'Navigation/ToolbarButtonRow', component: ToolbarButtonRow, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.story.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.story.tsx index bfd795724f4..beb4a467507 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.story.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.story.tsx @@ -4,7 +4,7 @@ import { Meta, StoryFn } from '@storybook/react'; import { UnitPicker, UnitPickerProps } from './UnitPicker'; const meta: Meta = { - title: 'Pickers and Editors/UnitPicker', + title: 'Pickers/UnitPicker', component: UnitPicker, parameters: { controls: { diff --git a/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx b/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx index 7352e846434..f20b4b809b5 100644 --- a/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx +++ b/packages/grafana-ui/src/components/UsersIndicator/Avatar.story.tsx @@ -4,7 +4,7 @@ import { Avatar } from './Avatar'; import mdx from './Avatar.mdx'; const meta: Meta = { - title: 'General/UsersIndicator/Avatar', + title: 'Iconography/Avatar', component: Avatar, parameters: { docs: { page: mdx }, diff --git a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx index c8e2ebd55f5..b17dad6fb32 100644 --- a/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx +++ b/packages/grafana-ui/src/components/UsersIndicator/UserIcon.story.tsx @@ -4,7 +4,7 @@ import { UserIcon } from './UserIcon'; import mdx from './UserIcon.mdx'; const meta: Meta = { - title: 'General/UsersIndicator/UserIcon', + title: 'Iconography/UserIcon', component: UserIcon, argTypes: {}, parameters: { diff --git a/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.story.tsx b/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.story.tsx index 00a16d4c304..a7b0a6b9b97 100644 --- a/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.story.tsx +++ b/packages/grafana-ui/src/components/UsersIndicator/UsersIndicator.story.tsx @@ -4,7 +4,7 @@ import { UsersIndicator } from './UsersIndicator'; import mdx from './UsersIndicator.mdx'; const meta: Meta = { - title: 'General/UsersIndicator', + title: 'Iconography/UsersIndicator', component: UsersIndicator, argTypes: { limit: { control: { type: 'number', min: 1 } } }, parameters: { diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.story.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.story.tsx index 86d4df02e8d..ab83e8bcd6e 100644 --- a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.story.tsx +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.story.tsx @@ -7,7 +7,7 @@ import { ValuePicker } from './ValuePicker'; import mdx from './ValuePicker.mdx'; const meta: Meta = { - title: 'Pickers and Editors/ValuePicker', + title: 'Pickers/ValuePicker', component: ValuePicker, parameters: { docs: { diff --git a/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx b/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx index 7e8e93a5a9c..939329c821f 100644 --- a/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx +++ b/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { VizLayout } from './VizLayout'; const meta: Meta = { - title: 'Visualizations/VizLayout', + title: 'Plugins/VizLayout', component: VizLayout, parameters: { docs: {}, diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx index f6a38b71937..b9af4d23fc4 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx @@ -10,7 +10,7 @@ import { VizLegend } from './VizLegend'; import { VizLegendItem } from './types'; const meta: Meta = { - title: 'Visualizations/VizLegend', + title: 'Plugins/VizLegend', component: VizLegend, args: { containerWidth: '100%', diff --git a/packages/grafana-ui/src/components/VizTooltip/SeriesTable.story.tsx b/packages/grafana-ui/src/components/VizTooltip/SeriesTable.story.tsx index 9b5b1d30699..17b9408051a 100644 --- a/packages/grafana-ui/src/components/VizTooltip/SeriesTable.story.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/SeriesTable.story.tsx @@ -3,7 +3,7 @@ import { StoryFn, Meta } from '@storybook/react'; import { SeriesTable } from './SeriesTable'; const meta: Meta = { - title: 'Visualizations/VizTooltip/SeriesTable', + title: 'Plugins/SeriesTable', component: SeriesTable, argTypes: { timestamp: { diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx deleted file mode 100644 index cb613aa228f..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { StoryFn } from '@storybook/react'; - -import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId } from '@grafana/data'; -import { LegendDisplayMode } from '@grafana/schema'; - -import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend'; - -export default { - title: 'Visualizations/Graph/GraphWithLegend', - component: GraphWithLegend, - parameters: { - controls: { - exclude: ['className', 'ariaLabel', 'legendDisplayMode', 'series'], - }, - }, - argTypes: { - displayMode: { control: { type: 'radio' }, options: ['table', 'list', 'hidden'] }, - placement: { control: { type: 'radio' }, options: ['bottom', 'right'] }, - rightAxisSeries: { name: 'Right y-axis series, i.e. A,C' }, - timeZone: { control: { type: 'radio' }, options: ['browser', 'utc'] }, - width: { control: { type: 'range', min: 200, max: 800 } }, - height: { control: { type: 'range', min: 1700, step: 300 } }, - lineWidth: { control: { type: 'range', min: 1, max: 10 } }, - }, -}; - -const series: GraphSeriesXY[] = [ - { - data: [ - [1546372800000, 10], - [1546376400000, 20], - [1546380000000, 10], - ], - color: 'red', - isVisible: true, - label: 'A-series', - seriesIndex: 0, - timeField: { - type: FieldType.time, - name: 'time', - values: [1546372800000, 1546376400000, 1546380000000], - config: {}, - }, - valueField: { - type: FieldType.number, - name: 'a-series', - values: [10, 20, 10], - config: { - color: { - mode: FieldColorModeId.Fixed, - fixedColor: 'red', - }, - }, - }, - timeStep: 3600000, - yAxis: { - index: 1, - }, - }, - { - data: [ - [1546372800000, 20], - [1546376400000, 30], - [1546380000000, 40], - ], - color: 'blue', - isVisible: true, - label: 'B-series', - seriesIndex: 1, - timeField: { - type: FieldType.time, - name: 'time', - values: [1546372800000, 1546376400000, 1546380000000], - config: {}, - }, - valueField: { - type: FieldType.number, - name: 'b-series', - values: [20, 30, 40], - config: { - color: { - mode: FieldColorModeId.Fixed, - fixedColor: 'blue', - }, - }, - }, - timeStep: 3600000, - yAxis: { - index: 1, - }, - }, -]; - -interface StoryProps extends GraphWithLegendProps { - rightAxisSeries: string; - displayMode: string; -} - -export const WithLegend: StoryFn = ({ rightAxisSeries, displayMode, legendDisplayMode, ...args }) => { - const props: Partial = { - series: series.map((s) => { - if ( - rightAxisSeries - .split(',') - .map((s) => s.trim()) - .indexOf(s.label.split('-')[0]) > -1 - ) { - s.yAxis = { index: 2 }; - } else { - s.yAxis = { index: 1 }; - } - return s; - }), - }; - - return ( - - ); -}; -WithLegend.args = { - rightAxisSeries: '', - displayMode: 'list', - onToggleSort: () => {}, - timeRange: { - from: dateTime(1546372800000), - to: dateTime(1546380000000), - raw: { - from: dateTime(1546372800000), - to: dateTime(1546380000000), - }, - }, - timeZone: 'browser', - width: 600, - height: 300, - placement: 'bottom', -}; diff --git a/packages/grafana-ui/src/utils/useDelayedSwitch.internal.story.tsx b/packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx similarity index 90% rename from packages/grafana-ui/src/utils/useDelayedSwitch.internal.story.tsx rename to packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx index c00cdc86e11..23e3b22d1c6 100644 --- a/packages/grafana-ui/src/utils/useDelayedSwitch.internal.story.tsx +++ b/packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx @@ -3,7 +3,7 @@ import { StoryFn } from '@storybook/react'; import { useDelayedSwitch } from './useDelayedSwitch'; export default { - title: 'useDelayedSwitch', + title: 'Utilities/useDelayedSwitch', }; export const Basic: StoryFn = ({ value, delay, duration }) => { diff --git a/pkg/aggregator/README.md b/pkg/aggregator/README.md index 5bae5b94698..eea0234e819 100644 --- a/pkg/aggregator/README.md +++ b/pkg/aggregator/README.md @@ -1,6 +1,100 @@ ## Data Plane Aggregator -### Testing queries +The data plane aggregator is a reverse proxy similar to `kube-aggregator` that sits first in the request path. It is responsible for reverse proxying any registered data plane (anything that isn't CRUD + List + Watch) routes to the appropriate app handlers. Anything that does not match the registered data planeroutes will be delegated to the next apiserver in the handler chain (e.g. `kube-aggregator`). Currently the aggregator uses the Grafana plugin framework to register and proxy requests to Grafana plugins. The benefit of this approach is that it allows apps to be built, packaged, published, and deployed using the existing Grafana plugin framework. + +![Request Flow](./requestflow.png) + +### Architecture + +The data plane aggregator is built on top of the Kubernetes API server framework. It introduces a new `DataPlaneService` Custom Resource Definition (CRD) to dynamically register and proxy requests to Grafana plugins. + +The main components are: + +* **GrafanaAggregator**: The main server component, located in `pkg/aggregator/apiserver/apiserver.go`. It's responsible for: + * Creating a generic API server. + * Registering the `DataPlaneService` resource. + * Running the `DataPlaneServiceRegistrationController`. +* **DataPlaneServiceRegistrationController**: This controller, located in `pkg/aggregator/apiserver/dataplaneservice_controller.go`, is responsible for: + * Watching for `add`, `update`, and `delete` events on `DataPlaneService` resources. + * Passing the `DataPlaneService` to the `PluginHandler` to add, update, or remove the corresponding proxy handlers. +* **PluginHandler**: This handler, located in `pkg/aggregator/apiserver/plugin/handler.go`, is responsible for: + * Translating between the HTTP request/response and Grafana plugin request/response. + * Adding and removing HTTP handlers to proxy requests to plugins based on the registered `DataPlaneService` resources. +* **DataPlaneService**: Defined in `pkg/aggregator/apis/aggregation/v0alpha1/types.go`, this represents a service provided by a Grafana plugin. + +The following diagram illustrates the request flow: + +```mermaid +sequenceDiagram + autonumber + + participant plugin as disk + participant exe as plugin
binaries + participant wrap as apiserver + participant agg as kube-aggregator + participant data as dataplane-aggregator + participant storage as Unified
Storage + + Note over plugin,storage: Registration + wrap->>plugin: Load Scope app manifest + wrap->>agg: Create Scope APIService + wrap->>plugin: Load SLO app manifest + wrap->>agg: Create SLO APIService + wrap->>plugin: Load Dashboard app manifest + wrap->>agg: Create Dashboard APIService + wrap->>data: Create Dashboard DataPlaneService + + Note over plugin,storage: Storage Access + data->>storage: DataPlaneServices + agg->>storage: APIServices + wrap->>storage: Custom resources + + Note over plugin,storage: Create, Update, or Delete Hooks + data->>agg: Local delegation to kube-aggregator + agg->>wrap: HTTP reverse proxy incoming request + wrap->>exe: gRPC admission mutation hook + wrap->>exe: gRPC admission validation hook + + Note over plugin,storage: Get, List, or Watch Hooks + data->>agg: Local delegation to kube-aggregator + agg->>wrap: HTTP reverse proxy incoming request + wrap->>exe: gRPC conversion hook + + Note over plugin,storage: Subresource & Custom Routes + data->>exe: gRPC plugin resource calls + data->>exe: gRPC plugin query data + data->>exe: gRPC plugin stream + data->>exe: gRPC plugin health + data->>exe: gRPC plugin metrics + + Note over plugin,storage: Async (controllers) + exe->>data: Watch + data->>agg: Local delegation to kube-aggregator + agg->>wrap: HTTP reverse proxy incoming request + wrap->>storage: Watch +``` + +## DataPlaneService + +A `DataPlaneService` is a non-namespaced resource that represents a service exposed by a Grafana plugin. Here is an example: + +```yaml +apiVersion: aggregation.grafana.app/v0alpha1 +kind: DataPlaneService +metadata: + name: +spec: + pluginID: + pluginType: # app or datasource + group: + version: + services: + - type: # admission, conversion, query, stream, route, datasource-proxy + method: + path: +``` + +### Query Example 1. `custom.ini` changes: ```ini @@ -27,4 +121,9 @@ dataplaneservice.aggregation.grafana.app/v0alpha1.prometheus.grafana.app created 5. execute query (replace `example` with the UID of a prometheus data source): ```bash curl 'http://admin:admin@localhost:3000/apis/prometheus.grafana.app/v0alpha1/namespaces/default/connections/example/query' -X POST -d '@pkg/aggregator/examples/datasource-query.json' -``` \ No newline at end of file +``` + +### Research/PoC's + +* [PoC Alternate HTTP backend](https://github.com/grafana/grafana/pull/100697) +* [PoC Plugin SDK <--> App SDK Adapter](https://github.com/grafana/grafana-app-sdk/pull/493) \ No newline at end of file diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index f95dfbfffca..42ed8747550 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -10,11 +10,11 @@ require ( github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.36.0 - k8s.io/api v0.33.1 - k8s.io/apimachinery v0.33.1 - k8s.io/apiserver v0.33.1 - k8s.io/client-go v0.33.1 - k8s.io/component-base v0.33.1 + k8s.io/api v0.33.2 + k8s.io/apimachinery v0.33.2 + k8s.io/apiserver v0.33.2 + k8s.io/client-go v0.33.2 + k8s.io/component-base v0.33.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff sigs.k8s.io/structured-merge-diff/v4 v4.6.0 @@ -148,9 +148,9 @@ require ( golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.34.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index d7688a0f0ea..f3daf37f19e 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -482,12 +482,12 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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= @@ -520,16 +520,16 @@ 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/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/pkg/aggregator/requestflow.png b/pkg/aggregator/requestflow.png new file mode 100644 index 00000000000..c6c9d5f7da5 Binary files /dev/null and b/pkg/aggregator/requestflow.png differ diff --git a/pkg/api/apierrors/folder.go b/pkg/api/apierrors/folder.go index 18ab865426d..7ed21c04fb5 100644 --- a/pkg/api/apierrors/folder.go +++ b/pkg/api/apierrors/folder.go @@ -41,7 +41,8 @@ func ToFolderErrorResponse(err error) response.Response { return response.Error(http.StatusConflict, err.Error(), nil) } - if errors.Is(err, dashboards.ErrFolderVersionMismatch) { + if errors.Is(err, dashboards.ErrFolderVersionMismatch) || + k8sErrors.IsAlreadyExists(err) { return response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}) } diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index c9798e9a767..68bb5ba74c8 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -74,7 +74,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Response { userID, err := identity.UserIdentifier(c.GetID()) if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to update user preferences", err) + return response.Error(http.StatusUnauthorized, "Not a valid identity", err) } return prefapi.GetPreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, hs.Features, c.GetOrgID(), userID, 0) diff --git a/pkg/apimachinery/apis/common/v0alpha1/secure_values.go b/pkg/apimachinery/apis/common/v0alpha1/secure_values.go new file mode 100644 index 00000000000..3413753cee4 --- /dev/null +++ b/pkg/apimachinery/apis/common/v0alpha1/secure_values.go @@ -0,0 +1,94 @@ +package v0alpha1 + +import ( + "encoding/json" + "fmt" + "strconv" + + "gopkg.in/yaml.v3" +) + +const redacted = "[REDACTED]" + +// RawSecureValue contains the raw decrypted secure value. +type RawSecureValue string + +var ( + _ fmt.Stringer = (*RawSecureValue)(nil) + _ fmt.Formatter = (*RawSecureValue)(nil) + _ fmt.GoStringer = (*RawSecureValue)(nil) + _ json.Marshaler = (*RawSecureValue)(nil) + _ yaml.Marshaler = (*RawSecureValue)(nil) +) + +// Allow access to a secure value inside +// +k8s:openapi-gen=true +type InlineSecureValue struct { + // Create a secure value -- this is only used for POST/PUT + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=24576 + Create RawSecureValue `json:"create,omitempty"` + + // Name in the secret service (reference) + Name string `json:"name,omitempty"` + + // Remove this value from the secure value map + // Values owned by this resource will be deleted if necessary + Remove bool `json:"remove,omitempty,omitzero"` +} + +func (v InlineSecureValue) IsZero() bool { + return v.Create.IsZero() && v.Name == "" && !v.Remove +} + +// Collection of secure values +// +k8s:openapi-gen=true +type InlineSecureValues = map[string]InlineSecureValue + +// NewSecretValue creates a new exposed secure value wrapper. +func NewSecretValue(v string) RawSecureValue { + return RawSecureValue(v) +} + +// DangerouslyExposeAndConsumeValue will move the decrypted secure value out of the wrapper and return it. +// Further attempts to call this method will panic. +// The function name is intentionally kept long and weird because this is a dangerous operation and should be used carefully! +func (s *RawSecureValue) DangerouslyExposeAndConsumeValue() string { + if *s == "" { + panic("underlying value is empty or was consumed") + } + + tmp := *s + *s = "" + + return string(tmp) +} + +func (s RawSecureValue) IsZero() bool { + return s == "" // exclude from JSON +} + +// String must not return the exposed secure value. +func (s RawSecureValue) String() string { + return redacted +} + +// Format must not return the exposed secure value. +func (s RawSecureValue) Format(f fmt.State, _verb rune) { + _, _ = fmt.Fprint(f, redacted) +} + +// GoString must not return the exposed secure value. +func (s RawSecureValue) GoString() string { + return redacted +} + +// MarshalJSON must not return the exposed secure value. +func (s RawSecureValue) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(redacted)), nil +} + +// MarshalYAML must not return the exposed secure value. +func (s RawSecureValue) MarshalYAML() (any, error) { + return redacted, nil +} diff --git a/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go b/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go new file mode 100644 index 00000000000..53f180753a9 --- /dev/null +++ b/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go @@ -0,0 +1,47 @@ +package v0alpha1 + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestSecureValues(t *testing.T) { + expected := "[REDACTED]" + + rawValue := "a-password" + esv := NewSecretValue(rawValue) + + // String must not return the exposed secure value. + require.Equal(t, expected, esv.String()) + + // Format/GoString must not return the exposed secure value. + require.Equal(t, expected, fmt.Sprintf("%+#v", esv)) + require.Equal(t, expected, fmt.Sprintf("%v", esv)) + require.Equal(t, expected, fmt.Sprintf("%s", esv)) + + buf := new(bytes.Buffer) + _, err := fmt.Fprintf(buf, "%#v", esv) + require.NoError(t, err) + require.Equal(t, expected, buf.String()) + + // MarshalJSON must not return the exposed secure value. + bytes, err := json.Marshal(esv) + require.NoError(t, err) + require.Equal(t, `"`+expected+`"`, string(bytes)) + + // MarshalYAML must not return the exposed secure value. + bytes, err = yaml.Marshal(esv) + require.NoError(t, err) + require.Equal(t, "'"+expected+"'\n", string(bytes)) + + // DangerouslyExposeAndConsumeValue returns the raw value. + require.Equal(t, rawValue, esv.DangerouslyExposeAndConsumeValue()) + + // Further calls to DangerouslyExposeAndConsumeValue will panic. + require.Panics(t, func() { esv.DangerouslyExposeAndConsumeValue() }) +} diff --git a/pkg/apimachinery/apis/common/v0alpha1/types.go b/pkg/apimachinery/apis/common/v0alpha1/types.go index 8ccc08c62a0..34b21adaa3e 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/types.go +++ b/pkg/apimachinery/apis/common/v0alpha1/types.go @@ -1,23 +1,46 @@ package v0alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) // Similar to // https://dev-k8sref-io.web.app/docs/common-definitions/objectreference-/ // ObjectReference contains enough information to let you inspect or modify the referred object. type ObjectReference struct { - Resource string `json:"resource,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` - // APIGroup is the name of the API group that contains the referred object. // The empty string represents the core API group. APIGroup string `json:"apiGroup,omitempty"` // APIVersion is the version of the API group that contains the referred object. APIVersion string `json:"apiVersion,omitempty"` + + // See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#types-kinds + Kind string `json:"kind,omitempty"` + + // Tenant isolation + Namespace string `json:"namespace,omitempty"` + + // Explicit resource identifier + Name string `json:"name,omitempty"` + + // May contain a valid JSON/Go field access statement + FieldPath string `json:"fieldPath,omitempty"` + + // Sepcific deployment of an object + UID types.UID `json:"uid,omitempty"` +} + +func (r ObjectReference) ToOwnerReference() metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: fmt.Sprintf("%s/%s", r.APIGroup, r.APIVersion), + Kind: r.Kind, + Name: r.Name, + UID: r.UID, + } } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go index 17417fd85fa..db4c2e73302 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go @@ -13,9 +13,9 @@ import ( // Unstructured allows objects that do not have Golang structs registered to be manipulated // generically. type Unstructured struct { - // Object is a JSON compatible map with string, float, int, bool, []interface{}, - // or map[string]interface{} children. - Object map[string]interface{} + // Object is a JSON compatible map with string, float, int, bool, []any, + // or map[string]any children. + Object map[string]any } // Produce an API definition that represents map[string]any @@ -27,7 +27,7 @@ func (u Unstructured) OpenAPIDefinition() openapi.OpenAPIDefinition { AdditionalProperties: &spec.SchemaOrBool{Allows: true}, }, VendorExtensible: spec.VendorExtensible{ - Extensions: map[string]interface{}{ + Extensions: map[string]any{ "x-kubernetes-preserve-unknown-fields": true, }, }, @@ -35,14 +35,18 @@ func (u Unstructured) OpenAPIDefinition() openapi.OpenAPIDefinition { } } -func (u *Unstructured) UnstructuredContent() map[string]interface{} { +func (u *Unstructured) IsZero() bool { + return len(u.Object) == 0 +} + +func (u *Unstructured) UnstructuredContent() map[string]any { if u.Object == nil { - return make(map[string]interface{}) + return make(map[string]any) } return u.Object } -func (u *Unstructured) SetUnstructuredContent(content map[string]interface{}) { +func (u *Unstructured) SetUnstructuredContent(content map[string]any) { u.Object = content } @@ -64,7 +68,7 @@ func (u *Unstructured) DeepCopy() *Unstructured { } out := new(Unstructured) *out = *u - out.Object = deepCopyJSONValue(u.Object).(map[string]interface{}) + out.Object = deepCopyJSONValue(u.Object).(map[string]any) return out } @@ -78,24 +82,24 @@ func (u *Unstructured) DeepCopyInto(out *Unstructured) { // runtime.DeepCopyJSON(u.Object) // // BUT this avoids panic on int -func deepCopyJSONValue(x interface{}) interface{} { +func deepCopyJSONValue(x any) any { switch x := x.(type) { - case map[string]interface{}: + case map[string]any: if x == nil { - // Typed nil - an interface{} that contains a type map[string]interface{} with a value of nil + // Typed nil - an any that contains a type map[string]any with a value of nil return x } - clone := make(map[string]interface{}, len(x)) + clone := make(map[string]any, len(x)) for k, v := range x { clone[k] = deepCopyJSONValue(v) } return clone - case []interface{}: + case []any: if x == nil { - // Typed nil - an interface{} that contains a type []interface{} with a value of nil + // Typed nil - an any that contains a type []any with a value of nil return x } - clone := make([]interface{}, len(x)) + clone := make([]any, len(x)) for i, v := range x { clone[i] = deepCopyJSONValue(v) } @@ -124,23 +128,23 @@ func deepCopyJSONValue(x interface{}) interface{} { } } -func (u *Unstructured) Set(field string, value interface{}) { +func (u *Unstructured) Set(field string, value any) { if u.Object == nil { - u.Object = make(map[string]interface{}) + u.Object = make(map[string]any) } _ = unstructured.SetNestedField(u.Object, value, field) } func (u *Unstructured) Remove(fields ...string) { if u.Object == nil { - u.Object = make(map[string]interface{}) + u.Object = make(map[string]any) } unstructured.RemoveNestedField(u.Object, fields...) } -func (u *Unstructured) SetNestedField(value interface{}, fields ...string) { +func (u *Unstructured) SetNestedField(value any, fields ...string) { if u.Object == nil { - u.Object = make(map[string]interface{}) + u.Object = make(map[string]any) } _ = unstructured.SetNestedField(u.Object, value, fields...) } diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go index 1698bd081c0..296212398bd 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go @@ -10,13 +10,13 @@ import ( func TestDeepCopyJSON(t *testing.T) { obj := &Unstructured{ - Object: map[string]interface{}{ + Object: map[string]any{ "int": int(2), "int16": int16(2), "int32": int32(2), "uint64": uint64(2), "ref": &ObjectReference{ - Resource: "x", + Kind: "x", }, "array": []any{ int(1), int64(2), "hello", @@ -25,7 +25,7 @@ func TestDeepCopyJSON(t *testing.T) { "bool": true, "map": map[string]any{ "x": &ObjectReference{ - Resource: "x", + Kind: "x", }, }, "object": &v1.APIGroup{ diff --git a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go index 8a3334e3634..d3f8e64dbff 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.deepcopy.go @@ -11,6 +11,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InlineSecureValue) DeepCopyInto(out *InlineSecureValue) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InlineSecureValue. +func (in *InlineSecureValue) DeepCopy() *InlineSecureValue { + if in == nil { + return nil + } + out := new(InlineSecureValue) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { *out = *in diff --git a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go index 2064fa5fac8..2f92840b5e6 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go +++ b/pkg/apimachinery/apis/common/v0alpha1/zz_generated.openapi.go @@ -11,68 +11,106 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" common "k8s.io/kube-openapi/pkg/common" spec "k8s.io/kube-openapi/pkg/validation/spec" + ptr "k8s.io/utils/ptr" ) func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference": schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Scope": schema_apimachinery_apis_common_v0alpha1_Scope(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter": schema_apimachinery_apis_common_v0alpha1_ScopeFilter(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec": schema_apimachinery_apis_common_v0alpha1_ScopeSpec(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldSelectorRequirement": schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), - "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), - "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), - "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), - "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.InlineSecureValue": schema_apimachinery_apis_common_v0alpha1_InlineSecureValue(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ObjectReference": schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Scope": schema_apimachinery_apis_common_v0alpha1_Scope(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeFilter": schema_apimachinery_apis_common_v0alpha1_ScopeFilter(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.ScopeSpec": schema_apimachinery_apis_common_v0alpha1_ScopeSpec(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.FieldSelectorRequirement": schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), + "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), + "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), + "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), + "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), + "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), + } +} + +func schema_apimachinery_apis_common_v0alpha1_InlineSecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Allow access to a secure value inside", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "create": { + SchemaProps: spec.SchemaProps{ + Description: "Create a secure value -- this is only used for POST/PUT", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](24576), + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name in the secret service (reference)", + Type: []string{"string"}, + Format: "", + }, + }, + "remove": { + SchemaProps: spec.SchemaProps{ + Description: "Remove this value from the secure value map Values owned by this resource will be deleted if necessary", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, } } @@ -83,24 +121,6 @@ func schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref common.Referen Description: "Similar to https://dev-k8sref-io.web.app/docs/common-definitions/objectreference-/ ObjectReference contains enough information to let you inspect or modify the referred object.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "resource": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "namespace": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "name": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, "apiGroup": { SchemaProps: spec.SchemaProps{ Description: "APIGroup is the name of the API group that contains the referred object. The empty string represents the core API group.", @@ -115,6 +135,41 @@ func schema_apimachinery_apis_common_v0alpha1_ObjectReference(ref common.Referen Format: "", }, }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Tenant isolation", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Explicit resource identifier", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldPath": { + SchemaProps: spec.SchemaProps{ + Description: "May contain a valid JSON/Go field access statement", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "Sepcific deployment of an object", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 2155cda748e..8a1c0902f6a 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -4,12 +4,14 @@ go 1.24.4 require ( github.com/go-jose/go-jose/v3 v3.0.4 // @grafana/identity-access-team - github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // @grafana/identity-access-team - github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 // @grafana/identity-access-team + github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 - k8s.io/apimachinery v0.33.1 - k8s.io/apiserver v0.33.1 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.33.2 + k8s.io/apiserver v0.33.2 k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 ) require ( @@ -44,13 +46,11 @@ require ( golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 0c3fcf26b46..9ea794ad760 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -31,10 +31,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed h1:k5Ng33zE9fCawqfEVybOasXY7/FQD5Qg2J92ePneeVM= -github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= -github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= -github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -152,8 +152,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -166,10 +166,10 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index be5d7fe83fa..f98738db58d 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -36,8 +36,22 @@ const ( serviceNameForProvisioning = "provisioning" ) -func newInternalIdentity(name string, namespace string, orgID int64) Requester { - return &StaticRequester{ +type IdentityOpts func(*StaticRequester) + +// WithServiceIdentityName sets the `StaticRequester.AccessTokenClaims.Rest.ServiceIdentity` field to the provided name. +// This is so far only used by Secrets Manager to identify and gate the service decrypting a secret. +func WithServiceIdentityName(name string) IdentityOpts { + return func(r *StaticRequester) { + r.AccessTokenClaims.Rest.ServiceIdentity = name + } +} + +func newInternalIdentity(name string, namespace string, orgID int64, opts ...IdentityOpts) Requester { + // Create a copy of the ServiceIdentityClaims to avoid modifying the global one. + // Some of the options might mutate it. + claimsCopy := *ServiceIdentityClaims + + staticRequester := &StaticRequester{ Type: types.TypeAccessPolicy, Name: name, UserUID: name, @@ -50,37 +64,43 @@ func newInternalIdentity(name string, namespace string, orgID int64) Requester { Permissions: map[int64]map[string][]string{ orgID: serviceIdentityPermissions, }, - AccessTokenClaims: ServiceIdentityClaims, + AccessTokenClaims: &claimsCopy, } + + for _, opt := range opts { + opt(staticRequester) + } + + return staticRequester } // WithServiceIdentity sets an identity representing the service itself in provided org and store it in context. // This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with // static permissions so it can be used in legacy code paths. -func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) { - r := newInternalIdentity(serviceName, "*", orgID) +func WithServiceIdentity(ctx context.Context, orgID int64, opts ...IdentityOpts) (context.Context, Requester) { + r := newInternalIdentity(serviceName, "*", orgID, opts...) return WithRequester(ctx, r), r } -func WithProvisioningIdentity(ctx context.Context, namespace string) (context.Context, Requester, error) { +func WithProvisioningIdentity(ctx context.Context, namespace string, opts ...IdentityOpts) (context.Context, Requester, error) { ns, err := types.ParseNamespace(namespace) if err != nil { return nil, nil, err } - r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID) + r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID, opts...) return WithRequester(ctx, r), r, nil } // WithServiceIdentityContext sets an identity representing the service itself in context. -func WithServiceIdentityContext(ctx context.Context, orgID int64) context.Context { - ctx, _ = WithServiceIdentity(ctx, orgID) +func WithServiceIdentityContext(ctx context.Context, orgID int64, opts ...IdentityOpts) context.Context { + ctx, _ = WithServiceIdentity(ctx, orgID, opts...) return ctx } // WithServiceIdentityFN calls provided closure with an context contaning the identity of the service. -func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error)) (T, error) { - return fn(WithServiceIdentityContext(ctx, orgID)) +func WithServiceIdentityFn[T any](ctx context.Context, orgID int64, fn func(ctx context.Context) (T, error), opts ...IdentityOpts) (T, error) { + return fn(WithServiceIdentityContext(ctx, orgID, opts...)) } func getWildcardPermissions(actions ...string) map[string][]string { @@ -91,14 +111,6 @@ func getWildcardPermissions(actions ...string) map[string][]string { return permissions } -func getTokenPermissions(groups ...string) []string { - out := make([]string, 0, len(groups)) - for _, group := range groups { - out = append(out, group+":*") - } - return out -} - // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( @@ -115,18 +127,22 @@ var serviceIdentityPermissions = getWildcardPermissions( "datasources:delete", "alert.provisioning:write", "alert.provisioning.secrets:read", - "users:read", // accesscontrol.ActionUsersRead, - "org.users:read", // accesscontrol.ActionOrgUsersRead, - "teams:read", // accesscontrol.ActionTeamsRead, + "users:read", // accesscontrol.ActionUsersRead, + "org.users:read", // accesscontrol.ActionOrgUsersRead, + "teams:read", // accesscontrol.ActionTeamsRead, + "serviceaccounts:read", // serviceaccounts.ActionRead, ) -var serviceIdentityTokenPermissions = getTokenPermissions( - "folder.grafana.app", - "dashboard.grafana.app", - "secret.grafana.app", - "query.grafana.app", - "iam.grafana.app", -) +var serviceIdentityTokenPermissions = []string{ + "folder.grafana.app:*", + "dashboard.grafana.app:*", + "secret.grafana.app:*", + "query.grafana.app:*", + "iam.grafana.app:*", + + // Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions. + "secret.grafana.app/securevalues:decrypt", +} var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{ Rest: authn.AccessTokenClaims{ diff --git a/pkg/apimachinery/identity/context_test.go b/pkg/apimachinery/identity/context_test.go index 87f31a17763..10c043517d7 100644 --- a/pkg/apimachinery/identity/context_test.go +++ b/pkg/apimachinery/identity/context_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/grafana/authlib/authn" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -24,3 +25,48 @@ func TestRequesterFromContext(t *testing.T) { require.Equal(t, expected.GetUID(), actual.GetUID()) }) } + +func TestWithServiceIdentity(t *testing.T) { + t.Run("with a custom service identity name", func(t *testing.T) { + customName := "custom-service" + orgID := int64(1) + ctx, requester := identity.WithServiceIdentity(context.Background(), orgID, identity.WithServiceIdentityName(customName)) + require.NotNil(t, requester) + require.Equal(t, orgID, requester.GetOrgID()) + require.Equal(t, customName, requester.GetExtra()[string(authn.ServiceIdentityKey)][0]) + require.Contains(t, requester.GetTokenPermissions(), "secret.grafana.app/securevalues:decrypt") + + fromCtx, err := identity.GetRequester(ctx) + require.NoError(t, err) + require.Equal(t, customName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0]) + + // Reuse the context but create another identity on top with a different name and org ID + anotherCustomName := "another-custom-service" + anotherOrgID := int64(2) + ctx2 := identity.WithServiceIdentityContext(ctx, anotherOrgID, identity.WithServiceIdentityName(anotherCustomName)) + + fromCtx, err = identity.GetRequester(ctx2) + require.NoError(t, err) + require.Equal(t, anotherOrgID, fromCtx.GetOrgID()) + require.Equal(t, anotherCustomName, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)][0]) + + // Reuse the context but create another identity without a custom name + ctx3, requester := identity.WithServiceIdentity(ctx2, 1) + require.NotNil(t, requester) + require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)]) + + fromCtx, err = identity.GetRequester(ctx3) + require.NoError(t, err) + require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)]) + }) + + t.Run("without a custom service identity name", func(t *testing.T) { + ctx, requester := identity.WithServiceIdentity(context.Background(), 1) + require.NotNil(t, requester) + require.Empty(t, requester.GetExtra()[string(authn.ServiceIdentityKey)]) + + fromCtx, err := identity.GetRequester(ctx) + require.NoError(t, err) + require.Empty(t, fromCtx.GetExtra()[string(authn.ServiceIdentityKey)]) + }) +} diff --git a/pkg/apimachinery/utils/blob.go b/pkg/apimachinery/utils/blob.go new file mode 100644 index 00000000000..ca023560cc9 --- /dev/null +++ b/pkg/apimachinery/utils/blob.go @@ -0,0 +1,88 @@ +package utils + +import ( + "bytes" + "fmt" + "mime" + "strconv" + "strings" +) + +type BlobInfo struct { + UID string `json:"uid"` + Size int64 `json:"size,omitempty"` + Hash string `json:"hash,omitempty"` + MimeType string `json:"mime,omitempty"` + Charset string `json:"charset,omitempty"` // content type = mime+charset +} + +// Content type is mime + charset +func (b *BlobInfo) SetContentType(v string) { + var params map[string]string + var err error + + b.Charset = "" + b.MimeType, params, err = mime.ParseMediaType(v) + if err != nil { + return + } + b.Charset = params["charset"] +} + +// Content type is mime + charset +func (b *BlobInfo) ContentType() string { + sb := bytes.NewBufferString(b.MimeType) + if b.Charset != "" { + sb.WriteString("; charset=") + sb.WriteString(b.Charset) + } + return sb.String() +} + +func (b *BlobInfo) String() string { + sb := bytes.NewBufferString(b.UID) + if b.Size > 0 { + fmt.Fprintf(sb, "; size=%d", b.Size) + } + if b.Hash != "" { + sb.WriteString("; hash=") + sb.WriteString(b.Hash) + } + if b.MimeType != "" { + sb.WriteString("; mime=") + sb.WriteString(b.MimeType) + } + if b.Charset != "" { + sb.WriteString("; charset=") + sb.WriteString(b.Charset) + } + return sb.String() +} + +func ParseBlobInfo(v string) *BlobInfo { + if v == "" { + return nil + } + info := &BlobInfo{} + for i, part := range strings.Split(v, ";") { + if i == 0 { + info.UID = part + continue + } + kv := strings.Split(strings.TrimSpace(part), "=") + if len(kv) == 2 { + val := kv[1] + switch kv[0] { + case "size": + info.Size, _ = strconv.ParseInt(val, 10, 64) + case "hash": + info.Hash = val + case "mime": + info.MimeType = val + case "charset": + info.Charset = val + } + } + } + return info +} diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index 2907897851b..508263b87f4 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -1,12 +1,9 @@ package utils import ( - "bytes" "fmt" - "mime" "reflect" "strconv" - "strings" "time" "k8s.io/apimachinery/pkg/api/meta" @@ -15,6 +12,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" ) // LabelKeyGetHistory is used to select object history for an given resource @@ -149,6 +148,13 @@ type GrafanaMetaAccessor interface { // SetSourceProperties sets the source properties of the resource. SetSourceProperties(SourceProperties) + + // GetSecureValues reads the "secure" property on a resource + GetSecureValues() (common.InlineSecureValues, error) + + // SetSourceProperties sets the source properties of the resource. + // For write commands, this may include inline secrets; read will only have references + SetSecureValues(common.InlineSecureValues) error } var _ GrafanaMetaAccessor = (*grafanaMetaAccessor)(nil) @@ -825,81 +831,96 @@ func (m *grafanaMetaAccessor) SetSourceProperties(v SourceProperties) { m.obj.SetAnnotations(annot) } -type BlobInfo struct { - UID string `json:"uid"` - Size int64 `json:"size,omitempty"` - Hash string `json:"hash,omitempty"` - MimeType string `json:"mime,omitempty"` - Charset string `json:"charset,omitempty"` // content type = mime+charset +// GetSecureValues implements GrafanaMetaAccessor. +func (m *grafanaMetaAccessor) GetSecureValues() (vals common.InlineSecureValues, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("error reading spec") + } + }() + + var property any // may be map or struct + + f := m.r.FieldByName("Secure") + if f.IsValid() { + property = f.Interface() + } else { + // Unstructured + u, ok := m.raw.(*unstructured.Unstructured) + if ok { + property = u.Object["secure"] + } + } + + // Not found (and no error) + if property == nil { + return nil, nil + } + + // Try directly casting the property + vals, ok := property.(common.InlineSecureValues) + if ok { + return vals, nil + } + + // Generic map + u, ok := property.(map[string]any) + if ok { + vals = make(common.InlineSecureValues, len(u)) + for k, v := range u { + sv, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("unsupported nested secure value: %t", v) + } + inline := common.InlineSecureValue{} + inline.Name, _, _ = unstructured.NestedString(sv, "name") + inline.Remove, _, _ = unstructured.NestedBool(sv, "remove") + create, _, _ := unstructured.NestedString(sv, "create") + if create != "" { + inline.Create = common.NewSecretValue(create) + } + vals[k] = inline + } + return vals, nil + } + + fmt.Printf("TODO PROPERTY: (%t) %+v\n", property, property) + + return nil, fmt.Errorf("support: %t", property) } -// Content type is mime + charset -func (b *BlobInfo) SetContentType(v string) { - var params map[string]string - var err error +// SetSecureValues implements GrafanaMetaAccessor. +func (m *grafanaMetaAccessor) SetSecureValues(vals common.InlineSecureValues) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("error setting spec") + } + }() - b.Charset = "" - b.MimeType, params, err = mime.ParseMediaType(v) - if err != nil { + f := m.r.FieldByName("Secure") + if f.IsValid() && f.CanSet() { + f.Set(reflect.ValueOf(vals)) return } - b.Charset = params["charset"] + + // Unstructured object + u, ok := m.raw.(*unstructured.Unstructured) + if ok { + u.Object["secure"] = vals + return + } + + return fmt.Errorf("unable to set secure values on (%T)", m.raw) } -// Content type is mime + charset -func (b *BlobInfo) ContentType() string { - sb := bytes.NewBufferString(b.MimeType) - if b.Charset != "" { - sb.WriteString("; charset=") - sb.WriteString(b.Charset) +func ToObjectReference(obj GrafanaMetaAccessor) common.ObjectReference { + gvk := obj.GetGroupVersionKind() + return common.ObjectReference{ + APIGroup: gvk.Group, + APIVersion: gvk.Version, + Kind: gvk.Kind, + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + UID: obj.GetUID(), } - return sb.String() -} - -func (b *BlobInfo) String() string { - sb := bytes.NewBufferString(b.UID) - if b.Size > 0 { - fmt.Fprintf(sb, "; size=%d", b.Size) - } - if b.Hash != "" { - sb.WriteString("; hash=") - sb.WriteString(b.Hash) - } - if b.MimeType != "" { - sb.WriteString("; mime=") - sb.WriteString(b.MimeType) - } - if b.Charset != "" { - sb.WriteString("; charset=") - sb.WriteString(b.Charset) - } - return sb.String() -} - -func ParseBlobInfo(v string) *BlobInfo { - if v == "" { - return nil - } - info := &BlobInfo{} - for i, part := range strings.Split(v, ";") { - if i == 0 { - info.UID = part - continue - } - kv := strings.Split(strings.TrimSpace(part), "=") - if len(kv) == 2 { - val := kv[1] - switch kv[0] { - case "size": - info.Size, _ = strconv.ParseInt(val, 10, 64) - case "hash": - info.Hash = val - case "mime": - info.MimeType = val - case "charset": - info.Charset = val - } - } - } - return info } diff --git a/pkg/apimachinery/utils/meta_mock.go b/pkg/apimachinery/utils/meta_mock.go index d73107b1bd6..4a0815a1b08 100644 --- a/pkg/apimachinery/utils/meta_mock.go +++ b/pkg/apimachinery/utils/meta_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package utils @@ -12,6 +12,8 @@ import ( types "k8s.io/apimachinery/pkg/types" + v0alpha1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -1248,6 +1250,63 @@ func (_c *MockGrafanaMetaAccessor_GetRuntimeObject_Call) RunAndReturn(run func() return _c } +// GetSecureValues provides a mock function with no fields +func (_m *MockGrafanaMetaAccessor) GetSecureValues() (map[string]v0alpha1.InlineSecureValue, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetSecureValues") + } + + var r0 map[string]v0alpha1.InlineSecureValue + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]v0alpha1.InlineSecureValue, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]v0alpha1.InlineSecureValue); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]v0alpha1.InlineSecureValue) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGrafanaMetaAccessor_GetSecureValues_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSecureValues' +type MockGrafanaMetaAccessor_GetSecureValues_Call struct { + *mock.Call +} + +// GetSecureValues is a helper method to define mock.On call +func (_e *MockGrafanaMetaAccessor_Expecter) GetSecureValues() *MockGrafanaMetaAccessor_GetSecureValues_Call { + return &MockGrafanaMetaAccessor_GetSecureValues_Call{Call: _e.mock.On("GetSecureValues")} +} + +func (_c *MockGrafanaMetaAccessor_GetSecureValues_Call) Run(run func()) *MockGrafanaMetaAccessor_GetSecureValues_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockGrafanaMetaAccessor_GetSecureValues_Call) Return(_a0 map[string]v0alpha1.InlineSecureValue, _a1 error) *MockGrafanaMetaAccessor_GetSecureValues_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGrafanaMetaAccessor_GetSecureValues_Call) RunAndReturn(run func() (map[string]v0alpha1.InlineSecureValue, error)) *MockGrafanaMetaAccessor_GetSecureValues_Call { + _c.Call.Return(run) + return _c +} + // GetSelfLink provides a mock function with no fields func (_m *MockGrafanaMetaAccessor) GetSelfLink() string { ret := _m.Called() @@ -2369,6 +2428,52 @@ func (_c *MockGrafanaMetaAccessor_SetResourceVersionInt64_Call) RunAndReturn(run return _c } +// SetSecureValues provides a mock function with given fields: _a0 +func (_m *MockGrafanaMetaAccessor) SetSecureValues(_a0 map[string]v0alpha1.InlineSecureValue) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SetSecureValues") + } + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]v0alpha1.InlineSecureValue) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockGrafanaMetaAccessor_SetSecureValues_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetSecureValues' +type MockGrafanaMetaAccessor_SetSecureValues_Call struct { + *mock.Call +} + +// SetSecureValues is a helper method to define mock.On call +// - _a0 map[string]v0alpha1.InlineSecureValue +func (_e *MockGrafanaMetaAccessor_Expecter) SetSecureValues(_a0 interface{}) *MockGrafanaMetaAccessor_SetSecureValues_Call { + return &MockGrafanaMetaAccessor_SetSecureValues_Call{Call: _e.mock.On("SetSecureValues", _a0)} +} + +func (_c *MockGrafanaMetaAccessor_SetSecureValues_Call) Run(run func(_a0 map[string]v0alpha1.InlineSecureValue)) *MockGrafanaMetaAccessor_SetSecureValues_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]v0alpha1.InlineSecureValue)) + }) + return _c +} + +func (_c *MockGrafanaMetaAccessor_SetSecureValues_Call) Return(_a0 error) *MockGrafanaMetaAccessor_SetSecureValues_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockGrafanaMetaAccessor_SetSecureValues_Call) RunAndReturn(run func(map[string]v0alpha1.InlineSecureValue) error) *MockGrafanaMetaAccessor_SetSecureValues_Call { + _c.Call.Return(run) + return _c +} + // SetSelfLink provides a mock function with given fields: selfLink func (_m *MockGrafanaMetaAccessor) SetSelfLink(selfLink string) { _m.Called(selfLink) diff --git a/pkg/apimachinery/utils/meta_test.go b/pkg/apimachinery/utils/meta_test.go index 0cc7d423d8f..fa58489b768 100644 --- a/pkg/apimachinery/utils/meta_test.go +++ b/pkg/apimachinery/utils/meta_test.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -24,6 +25,9 @@ type TestResource struct { // Read/write raw status Status Spec `json:"status,omitempty"` + + // Secure values as map + Secure common.InlineSecureValues `json:"secure,omitempty"` } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -84,6 +88,9 @@ type TestResource2 struct { // Exercise read/write pointer status Status *Spec `json:"status,omitempty"` + + // This time defined with a strict struct + SecureValues ExplictSecureValues `json:"secure,omitempty"` } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -112,6 +119,12 @@ func (in *TestResource2) DeepCopyObject() runtime.Object { return nil } +// Spec defines model for Spec. +type ExplictSecureValues struct { + // Sample token value + Prop common.InlineSecureValue `json:"token,omitempty"` +} + // Spec defines model for Spec. type Spec2 struct{} @@ -247,6 +260,15 @@ func TestMetaAccessor(t *testing.T) { status, err = meta.GetStatus() require.NoError(t, err) require.Equal(t, res.Object["status"], status) + + // Check write/read on unstructured object + err = meta.SetSecureValues(common.InlineSecureValues{ + "a": {Name: "bbbb"}, + }) + require.NoError(t, err) + secure, err := meta.GetSecureValues() + require.NoError(t, err) + require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true)) }) t.Run("get and set grafana metadata (TestResource)", func(t *testing.T) { @@ -254,6 +276,11 @@ func TestMetaAccessor(t *testing.T) { Spec: Spec{ Title: "test", }, + Secure: common.InlineSecureValues{ + "x": common.InlineSecureValue{ + Create: "hello", + }, + }, // Status is empty, but not nil! } meta, err := utils.MetaAccessor(res) @@ -302,6 +329,19 @@ func TestMetaAccessor(t *testing.T) { require.Equal(t, res.Status, status) require.Equal(t, "111", res.Status.Title) require.Equal(t, `{"title":"111"}`, asJSON(status, false)) + + // Check read/write secure values + secure, err := meta.GetSecureValues() + require.NoError(t, err) + require.JSONEq(t, `{"x": {"create": "[REDACTED]"}}`, asJSON(secure, true)) + + err = meta.SetSecureValues(common.InlineSecureValues{ + "a": {Name: "bbbb"}, + }) + require.NoError(t, err) + secure, err = meta.GetSecureValues() + require.NoError(t, err) + require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true)) }) t.Run("get and set grafana metadata (TestResource2)", func(t *testing.T) { diff --git a/pkg/apis/iam/v0alpha1/register.go b/pkg/apis/iam/v0alpha1/register.go index 7cec76a3b77..879cb3837cc 100644 --- a/pkg/apis/iam/v0alpha1/register.go +++ b/pkg/apis/iam/v0alpha1/register.go @@ -2,8 +2,10 @@ package v0alpha1 import ( "fmt" + "strings" "time" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -16,10 +18,11 @@ const ( APIVERSION = GROUP + "/" + VERSION ) -var UserResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "users", "user", "User", - func() runtime.Object { return &User{} }, - func() runtime.Object { return &UserList{} }, +var userKind = iamv0alpha1.UserKind() +var UserResourceInfo = utils.NewResourceInfo(userKind.Group(), userKind.Version(), + userKind.GroupVersionResource().Resource, strings.ToLower(userKind.Kind()), userKind.Kind(), + func() runtime.Object { return userKind.ZeroValue() }, + func() runtime.Object { return userKind.ZeroListValue() }, utils.TableColumns{ Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, @@ -28,7 +31,7 @@ var UserResourceInfo = utils.NewResourceInfo(GROUP, VERSION, {Name: "Created At", Type: "date"}, }, Reader: func(obj any) ([]interface{}, error) { - u, ok := obj.(*User) + u, ok := obj.(*iamv0alpha1.User) if ok { return []interface{}{ u.Name, @@ -42,10 +45,11 @@ var UserResourceInfo = utils.NewResourceInfo(GROUP, VERSION, }, ) -var TeamResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "teams", "team", "Team", - func() runtime.Object { return &Team{} }, - func() runtime.Object { return &TeamList{} }, +var teamKind = iamv0alpha1.TeamKind() +var TeamResourceInfo = utils.NewResourceInfo(teamKind.Group(), teamKind.Version(), + teamKind.GroupVersionResource().Resource, strings.ToLower(teamKind.Kind()), teamKind.Kind(), + func() runtime.Object { return teamKind.ZeroValue() }, + func() runtime.Object { return teamKind.ZeroListValue() }, utils.TableColumns{ Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, @@ -54,7 +58,7 @@ var TeamResourceInfo = utils.NewResourceInfo(GROUP, VERSION, {Name: "Created At", Type: "date"}, }, Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*Team) + m, ok := obj.(*iamv0alpha1.Team) if !ok { return nil, fmt.Errorf("expected team") } @@ -68,10 +72,11 @@ var TeamResourceInfo = utils.NewResourceInfo(GROUP, VERSION, }, ) -var ServiceAccountResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "serviceaccounts", "serviceaccount", "ServiceAccount", - func() runtime.Object { return &ServiceAccount{} }, - func() runtime.Object { return &ServiceAccountList{} }, +var serviceAccountKind = iamv0alpha1.ServiceAccountKind() +var ServiceAccountResourceInfo = utils.NewResourceInfo(serviceAccountKind.Group(), serviceAccountKind.Version(), + serviceAccountKind.GroupVersionResource().Resource, strings.ToLower(serviceAccountKind.Kind()), serviceAccountKind.Kind(), + func() runtime.Object { return serviceAccountKind.ZeroValue() }, + func() runtime.Object { return serviceAccountKind.ZeroListValue() }, utils.TableColumns{ Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, @@ -80,7 +85,7 @@ var ServiceAccountResourceInfo = utils.NewResourceInfo(GROUP, VERSION, {Name: "Created At", Type: "date"}, }, Reader: func(obj any) ([]interface{}, error) { - sa, ok := obj.(*ServiceAccount) + sa, ok := obj.(*iamv0alpha1.ServiceAccount) if ok { return []interface{}{ sa.Name, @@ -120,10 +125,13 @@ var SSOSettingResourceInfo = utils.NewResourceInfo( }, ) +var teamBindingKind = iamv0alpha1.TeamBindingKind() var TeamBindingResourceInfo = utils.NewResourceInfo( - GROUP, VERSION, "teambindings", "teambinding", "TeamBinding", - func() runtime.Object { return &TeamBinding{} }, - func() runtime.Object { return &TeamBindingList{} }, + teamBindingKind.Group(), teamBindingKind.Version(), + teamBindingKind.GroupVersionResource().Resource, + strings.ToLower(teamBindingKind.Kind()), teamBindingKind.Kind(), + func() runtime.Object { return teamBindingKind.ZeroValue() }, + func() runtime.Object { return teamBindingKind.ZeroListValue() }, utils.TableColumns{ Definition: []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name"}, @@ -131,13 +139,13 @@ var TeamBindingResourceInfo = utils.NewResourceInfo( {Name: "Created At", Type: "string", Format: "date"}, }, Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*TeamBinding) + m, ok := obj.(*iamv0alpha1.TeamBinding) if !ok { return nil, fmt.Errorf("expected team binding") } return []interface{}{ m.Name, - m.Spec.Team.Name, + m.Spec.TeamRef.Name, m.CreationTimestamp.UTC().Format(time.RFC3339), }, nil }, @@ -158,19 +166,19 @@ var ( func AddKnownTypes(scheme *runtime.Scheme, version string) { scheme.AddKnownTypes( schema.GroupVersion{Group: GROUP, Version: version}, - &User{}, - &UserList{}, + &iamv0alpha1.User{}, + &iamv0alpha1.UserList{}, &UserTeamList{}, - &ServiceAccount{}, - &ServiceAccountList{}, + &iamv0alpha1.ServiceAccount{}, + &iamv0alpha1.ServiceAccountList{}, &ServiceAccountTokenList{}, - &Team{}, - &TeamList{}, + &iamv0alpha1.Team{}, + &iamv0alpha1.TeamList{}, &DisplayList{}, &SSOSetting{}, &SSOSettingList{}, - &TeamBinding{}, - &TeamBindingList{}, + &iamv0alpha1.TeamBinding{}, + &iamv0alpha1.TeamBindingList{}, &TeamMemberList{}, ) } diff --git a/pkg/apis/iam/v0alpha1/types_service_account.go b/pkg/apis/iam/v0alpha1/types_service_account.go new file mode 100644 index 00000000000..25688929ebf --- /dev/null +++ b/pkg/apis/iam/v0alpha1/types_service_account.go @@ -0,0 +1,19 @@ +package v0alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ServiceAccountTokenList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ServiceAccountToken `json:"items"` +} + +type ServiceAccountToken struct { + Name string `json:"name,omitempty"` + Revoked bool `json:"revoked,omitempty"` + Expires *metav1.Time `json:"expires,omitempty"` + LastUsed *metav1.Time `json:"lastUsed,omitempty"` + Created metav1.Time `json:"created"` +} diff --git a/pkg/apis/iam/v0alpha1/types_servier_account.go b/pkg/apis/iam/v0alpha1/types_servier_account.go deleted file mode 100644 index 3f07ca76b41..00000000000 --- a/pkg/apis/iam/v0alpha1/types_servier_account.go +++ /dev/null @@ -1,50 +0,0 @@ -package v0alpha1 - -import ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ServiceAccount struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ServiceAccountSpec `json:"spec,omitempty"` -} - -func (s ServiceAccount) AuthID() string { - return fmt.Sprintf("%d", s.Spec.InternalID) -} - -type ServiceAccountSpec struct { - Title string `json:"title,omitempty"` - Disabled bool `json:"disabled,omitempty"` - // This is currently used for authorization checks but we don't want to expose it - InternalID int64 `json:"-"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ServiceAccountList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []ServiceAccount `json:"items"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ServiceAccountTokenList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []ServiceAccountToken `json:"items"` -} - -type ServiceAccountToken struct { - Name string `json:"name,omitempty"` - Revoked bool `json:"revoked,omitempty"` - Expires *metav1.Time `json:"expires,omitempty"` - LastUsed *metav1.Time `json:"lastUsed,omitempty"` - Created metav1.Time `json:"created"` -} diff --git a/pkg/apis/iam/v0alpha1/types_team.go b/pkg/apis/iam/v0alpha1/types_team.go index f7a2cfe1762..de8d81543f2 100644 --- a/pkg/apis/iam/v0alpha1/types_team.go +++ b/pkg/apis/iam/v0alpha1/types_team.go @@ -1,60 +1,9 @@ package v0alpha1 import ( - "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type Team struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec TeamSpec `json:"spec,omitempty"` -} - -type TeamSpec struct { - Title string `json:"title,omitempty"` - Email string `json:"email,omitempty"` - - // This is currently used for authorization checks but we don't want to expose it - InternalID int64 `json:"-"` -} - -func (t Team) AuthID() string { - return fmt.Sprintf("%d", t.Spec.InternalID) -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type TeamList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []Team `json:"items"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type TeamBinding struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec TeamBindingSpec `json:"spec,omitempty"` -} - -type TeamBindingSpec struct { - Subjects []TeamSubject `json:"subjects,omitempty"` - Team TeamRef `json:"team,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type TeamBindingList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []TeamBinding `json:"items"` -} - type TeamSubject struct { // Identity is a reference to the identity of this subject. Identity IdentityRef `json:"identity"` diff --git a/pkg/apis/iam/v0alpha1/types_user.go b/pkg/apis/iam/v0alpha1/types_user.go index f9d97095a3e..b62bbf6fc9d 100644 --- a/pkg/apis/iam/v0alpha1/types_user.go +++ b/pkg/apis/iam/v0alpha1/types_user.go @@ -1,40 +1,6 @@ package v0alpha1 -import ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type User struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec UserSpec `json:"spec,omitempty"` -} - -func (u User) AuthID() string { - return fmt.Sprintf("%d", u.Spec.InternalID) -} - -type UserSpec struct { - Name string `json:"name,omitempty"` - Login string `json:"login,omitempty"` - Email string `json:"email,omitempty"` - EmailVerified bool `json:"emailVerified,omitempty"` - Disabled bool `json:"disabled,omitempty"` - // This is currently used for authorization checks but we don't want to expose it - InternalID int64 `json:"-"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type UserList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []User `json:"items"` -} +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type UserTeamList struct { diff --git a/pkg/apis/iam/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/iam/v0alpha1/zz_generated.deepcopy.go index 20556acfa96..64d2f72c358 100644 --- a/pkg/apis/iam/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/iam/v0alpha1/zz_generated.deepcopy.go @@ -162,82 +162,6 @@ func (in *SSOSettingSpec) DeepCopy() *SSOSettingSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceAccount) DeepCopyInto(out *ServiceAccount) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccount. -func (in *ServiceAccount) DeepCopy() *ServiceAccount { - if in == nil { - return nil - } - out := new(ServiceAccount) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ServiceAccount) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceAccountList) DeepCopyInto(out *ServiceAccountList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ServiceAccount, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountList. -func (in *ServiceAccountList) DeepCopy() *ServiceAccountList { - if in == nil { - return nil - } - out := new(ServiceAccountList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ServiceAccountList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ServiceAccountSpec) DeepCopyInto(out *ServiceAccountSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountSpec. -func (in *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { - if in == nil { - return nil - } - out := new(ServiceAccountSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceAccountToken) DeepCopyInto(out *ServiceAccountToken) { *out = *in @@ -296,148 +220,6 @@ func (in *ServiceAccountTokenList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Team) DeepCopyInto(out *Team) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Team. -func (in *Team) DeepCopy() *Team { - if in == nil { - return nil - } - out := new(Team) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Team) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TeamBinding) DeepCopyInto(out *TeamBinding) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBinding. -func (in *TeamBinding) DeepCopy() *TeamBinding { - if in == nil { - return nil - } - out := new(TeamBinding) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *TeamBinding) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TeamBindingList) DeepCopyInto(out *TeamBindingList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]TeamBinding, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBindingList. -func (in *TeamBindingList) DeepCopy() *TeamBindingList { - if in == nil { - return nil - } - out := new(TeamBindingList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *TeamBindingList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TeamBindingSpec) DeepCopyInto(out *TeamBindingSpec) { - *out = *in - if in.Subjects != nil { - in, out := &in.Subjects, &out.Subjects - *out = make([]TeamSubject, len(*in)) - copy(*out, *in) - } - out.Team = in.Team - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamBindingSpec. -func (in *TeamBindingSpec) DeepCopy() *TeamBindingSpec { - if in == nil { - return nil - } - out := new(TeamBindingSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TeamList) DeepCopyInto(out *TeamList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Team, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamList. -func (in *TeamList) DeepCopy() *TeamList { - if in == nil { - return nil - } - out := new(TeamList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *TeamList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TeamMember) DeepCopyInto(out *TeamMember) { *out = *in @@ -502,22 +284,6 @@ func (in *TeamRef) DeepCopy() *TeamRef { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TeamSpec) DeepCopyInto(out *TeamSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamSpec. -func (in *TeamSpec) DeepCopy() *TeamSpec { - if in == nil { - return nil - } - out := new(TeamSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TeamSubject) DeepCopyInto(out *TeamSubject) { *out = *in @@ -535,82 +301,6 @@ func (in *TeamSubject) DeepCopy() *TeamSubject { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *User) DeepCopyInto(out *User) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. -func (in *User) DeepCopy() *User { - if in == nil { - return nil - } - out := new(User) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *User) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *UserList) DeepCopyInto(out *UserList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]User, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserList. -func (in *UserList) DeepCopy() *UserList { - if in == nil { - return nil - } - out := new(UserList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *UserList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *UserSpec) DeepCopyInto(out *UserSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec. -func (in *UserSpec) DeepCopy() *UserSpec { - if in == nil { - return nil - } - out := new(UserSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserTeam) DeepCopyInto(out *UserTeam) { *out = *in diff --git a/pkg/apis/iam/v0alpha1/zz_generated.openapi.go b/pkg/apis/iam/v0alpha1/zz_generated.openapi.go index 826b6dfb60d..c345dd23444 100644 --- a/pkg/apis/iam/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/iam/v0alpha1/zz_generated.openapi.go @@ -20,24 +20,12 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.SSOSetting": schema_pkg_apis_iam_v0alpha1_SSOSetting(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.SSOSettingList": schema_pkg_apis_iam_v0alpha1_SSOSettingList(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.SSOSettingSpec": schema_pkg_apis_iam_v0alpha1_SSOSettingSpec(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccount": schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountList": schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountSpec": schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountToken": schema_pkg_apis_iam_v0alpha1_ServiceAccountToken(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountTokenList": schema_pkg_apis_iam_v0alpha1_ServiceAccountTokenList(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Team": schema_pkg_apis_iam_v0alpha1_Team(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBinding": schema_pkg_apis_iam_v0alpha1_TeamBinding(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingList": schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingSpec": schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamMember": schema_pkg_apis_iam_v0alpha1_TeamMember(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamMemberList": schema_pkg_apis_iam_v0alpha1_TeamMemberList(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamRef": schema_pkg_apis_iam_v0alpha1_TeamRef(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSubject": schema_pkg_apis_iam_v0alpha1_TeamSubject(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.User": schema_pkg_apis_iam_v0alpha1_User(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserList": schema_pkg_apis_iam_v0alpha1_UserList(ref), - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserSpec": schema_pkg_apis_iam_v0alpha1_UserSpec(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserTeam": schema_pkg_apis_iam_v0alpha1_UserTeam(ref), "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserTeamList": schema_pkg_apis_iam_v0alpha1_UserTeamList(ref), } @@ -328,118 +316,6 @@ func schema_pkg_apis_iam_v0alpha1_SSOSettingSpec(ref common.ReferenceCallback) c } } -func schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountSpec"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccountSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_ServiceAccountList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccount"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.ServiceAccount", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_ServiceAccountSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "disabled": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - }, - }, - } -} - func schema_pkg_apis_iam_v0alpha1_ServiceAccountToken(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -530,215 +406,6 @@ func schema_pkg_apis_iam_v0alpha1_ServiceAccountTokenList(ref common.ReferenceCa } } -func schema_pkg_apis_iam_v0alpha1_Team(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSpec"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_TeamBinding(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingSpec"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_TeamBindingList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBinding"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamBinding", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_TeamBindingSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "subjects": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSubject"), - }, - }, - }, - }, - }, - "team": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamRef"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamRef", "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.TeamSubject"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_TeamList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Team"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.Team", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - func schema_pkg_apis_iam_v0alpha1_TeamMember(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -864,30 +531,6 @@ func schema_pkg_apis_iam_v0alpha1_TeamRef(ref common.ReferenceCallback) common.O } } -func schema_pkg_apis_iam_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "email": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - } -} - func schema_pkg_apis_iam_v0alpha1_TeamSubject(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -918,136 +561,6 @@ func schema_pkg_apis_iam_v0alpha1_TeamSubject(ref common.ReferenceCallback) comm } } -func schema_pkg_apis_iam_v0alpha1_User(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserSpec"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.UserSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_UserList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/iam/v0alpha1.User"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/iam/v0alpha1.User", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_iam_v0alpha1_UserSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "name": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "login": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "email": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "emailVerified": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "disabled": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - }, - }, - } -} - func schema_pkg_apis_iam_v0alpha1_UserTeam(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/iam/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/iam/v0alpha1/zz_generated.openapi_violation_exceptions.list index 4d214e16a1a..b2e716970f2 100644 --- a/pkg/apis/iam/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/iam/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,4 +1,3 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,DisplayList,Items -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,TeamBindingSpec,Subjects API rule violation: names_match,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,Display,InternalID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/iam/v0alpha1,DisplayList,Items diff --git a/pkg/apis/provisioning/v0alpha1/register.go b/pkg/apis/provisioning/v0alpha1/register.go index d1a9d4d9fdf..c7db279034c 100644 --- a/pkg/apis/provisioning/v0alpha1/register.go +++ b/pkg/apis/provisioning/v0alpha1/register.go @@ -43,6 +43,10 @@ var RepositoryResourceInfo = utils.NewResourceInfo(GROUP, VERSION, target = m.Spec.GitHub.URL case GitRepositoryType: target = m.Spec.Git.URL + case BitbucketRepositoryType: + target = m.Spec.Bitbucket.URL + case GitLabRepositoryType: + target = m.Spec.GitLab.URL } return []interface{}{ @@ -119,6 +123,7 @@ func AddKnownTypes(gv schema.GroupVersion, scheme *runtime.Scheme) error { &ResourceStats{}, &Job{}, &JobList{}, + &RefList{}, ) return nil } diff --git a/pkg/apis/provisioning/v0alpha1/settings.go b/pkg/apis/provisioning/v0alpha1/settings.go index 85a7e8e4150..58441e85f96 100644 --- a/pkg/apis/provisioning/v0alpha1/settings.go +++ b/pkg/apis/provisioning/v0alpha1/settings.go @@ -14,6 +14,9 @@ type RepositoryViewList struct { // The UI should force the onboarding workflow when this is true LegacyStorage bool `json:"legacyStorage,omitempty"` + // AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc) + AvailableRepositoryTypes []RepositoryType `json:"availableRepositoryTypes,omitempty"` + // +mapType=atomic Items []RepositoryView `json:"items"` } diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go index 561528f0020..fc18abdcaf7 100644 --- a/pkg/apis/provisioning/v0alpha1/types.go +++ b/pkg/apis/provisioning/v0alpha1/types.go @@ -77,20 +77,58 @@ type GitRepositoryConfig struct { Path string `json:"path,omitempty"` } +type BitbucketRepositoryConfig struct { + // The repository URL (e.g. `https://bitbucket.org/example/test`). + URL string `json:"url,omitempty"` + // The branch to use in the repository. + Branch string `json:"branch"` + // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. + Token string `json:"token,omitempty"` + // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. + // +listType=atomic + EncryptedToken []byte `json:"encryptedToken,omitempty"` + // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. + // This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. + // The path is relative to the root of the repository, regardless of the leading slash. + // + // When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. + Path string `json:"path,omitempty"` +} + +type GitLabRepositoryConfig struct { + // The repository URL (e.g. `https://gitlab.com/example/test`). + URL string `json:"url,omitempty"` + // The branch to use in the repository. + Branch string `json:"branch"` + // Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. + Token string `json:"token,omitempty"` + // Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. + // +listType=atomic + EncryptedToken []byte `json:"encryptedToken,omitempty"` + // Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. + // This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. + // The path is relative to the root of the repository, regardless of the leading slash. + // + // When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. + Path string `json:"path,omitempty"` +} + // RepositoryType defines the types of Repository // +enum type RepositoryType string // RepositoryType values const ( - LocalRepositoryType RepositoryType = "local" - GitHubRepositoryType RepositoryType = "github" - GitRepositoryType RepositoryType = "git" + LocalRepositoryType RepositoryType = "local" + GitHubRepositoryType RepositoryType = "github" + GitRepositoryType RepositoryType = "git" + BitbucketRepositoryType RepositoryType = "bitbucket" + GitLabRepositoryType RepositoryType = "gitlab" ) // IsGit returns true if the repository type is git or github func (r RepositoryType) IsGit() bool { - return r == GitRepositoryType || r == GitHubRepositoryType + return r == GitRepositoryType || r == GitHubRepositoryType || r == BitbucketRepositoryType || r == GitLabRepositoryType } type RepositorySpec struct { @@ -122,6 +160,14 @@ type RepositorySpec struct { // The repository on Git. // Mutually exclusive with local | github | git. Git *GitRepositoryConfig `json:"git,omitempty"` + + // The repository on Bitbucket. + // Mutually exclusive with local | github | git. + Bitbucket *BitbucketRepositoryConfig `json:"bitbucket,omitempty"` + + // The repository on GitLab. + // Mutually exclusive with local | github | git. + GitLab *GitLabRepositoryConfig `json:"gitlab,omitempty"` } // SyncTargetType defines where we want all values to resolve @@ -454,3 +500,21 @@ type HistoryItem struct { Authors []Author `json:"authors"` CreatedAt int64 `json:"createdAt"` } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type RefList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + // +listType=atomic + Items []RefItem `json:"items"` +} + +type RefItem struct { + // The name of the reference (branch or tag) + Name string `json:"name"` + // The SHA hash of the commit this ref points to + Hash string `json:"hash,omitempty"` + // The URL to the reference (branch or tag) + RefURL string `json:"refURL,omitempty"` +} diff --git a/pkg/apis/provisioning/v0alpha1/types_test.go b/pkg/apis/provisioning/v0alpha1/types_test.go index 915bc2d942c..ef8643e2a1e 100644 --- a/pkg/apis/provisioning/v0alpha1/types_test.go +++ b/pkg/apis/provisioning/v0alpha1/types_test.go @@ -22,6 +22,16 @@ func TestRepositoryType_IsGit(t *testing.T) { repoType: v0alpha1.GitHubRepositoryType, want: true, }, + { + name: "bitbucket", + repoType: v0alpha1.BitbucketRepositoryType, + want: true, + }, + { + name: "gitlab", + repoType: v0alpha1.GitLabRepositoryType, + want: true, + }, { name: "local", repoType: v0alpha1.LocalRepositoryType, diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index f9aa9f64eef..30fd560a597 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -27,6 +27,27 @@ func (in *Author) DeepCopy() *Author { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BitbucketRepositoryConfig) DeepCopyInto(out *BitbucketRepositoryConfig) { + *out = *in + if in.EncryptedToken != nil { + in, out := &in.EncryptedToken, &out.EncryptedToken + *out = make([]byte, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitbucketRepositoryConfig. +func (in *BitbucketRepositoryConfig) DeepCopy() *BitbucketRepositoryConfig { + if in == nil { + return nil + } + out := new(BitbucketRepositoryConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ErrorDetails) DeepCopyInto(out *ErrorDetails) { *out = *in @@ -127,6 +148,27 @@ func (in *GitHubRepositoryConfig) DeepCopy() *GitHubRepositoryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitLabRepositoryConfig) DeepCopyInto(out *GitLabRepositoryConfig) { + *out = *in + if in.EncryptedToken != nil { + in, out := &in.EncryptedToken, &out.EncryptedToken + *out = make([]byte, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitLabRepositoryConfig. +func (in *GitLabRepositoryConfig) DeepCopy() *GitLabRepositoryConfig { + if in == nil { + return nil + } + out := new(GitLabRepositoryConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitRepositoryConfig) DeepCopyInto(out *GitRepositoryConfig) { *out = *in @@ -442,6 +484,53 @@ func (in *PullRequestJobOptions) DeepCopy() *PullRequestJobOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RefItem) DeepCopyInto(out *RefItem) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefItem. +func (in *RefItem) DeepCopy() *RefItem { + if in == nil { + return nil + } + out := new(RefItem) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RefList) DeepCopyInto(out *RefList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RefItem, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefList. +func (in *RefList) DeepCopy() *RefList { + if in == nil { + return nil + } + out := new(RefList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RefList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Repository) DeepCopyInto(out *Repository) { *out = *in @@ -527,6 +616,16 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) { *out = new(GitRepositoryConfig) (*in).DeepCopyInto(*out) } + if in.Bitbucket != nil { + in, out := &in.Bitbucket, &out.Bitbucket + *out = new(BitbucketRepositoryConfig) + (*in).DeepCopyInto(*out) + } + if in.GitLab != nil { + in, out := &in.GitLab, &out.GitLab + *out = new(GitLabRepositoryConfig) + (*in).DeepCopyInto(*out) + } return } @@ -593,6 +692,11 @@ func (in *RepositoryView) DeepCopy() *RepositoryView { func (in *RepositoryViewList) DeepCopyInto(out *RepositoryViewList) { *out = *in out.TypeMeta = in.TypeMeta + if in.AvailableRepositoryTypes != nil { + in, out := &in.AvailableRepositoryTypes, &out.AvailableRepositoryTypes + *out = make([]RepositoryType, len(*in)) + copy(*out, *in) + } if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]RepositoryView, len(*in)) diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 5a80c0ea498..9505205943f 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -14,46 +14,50 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author": schema_pkg_apis_provisioning_v0alpha1_Author(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HealthStatus": schema_pkg_apis_provisioning_v0alpha1_HealthStatus(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem": schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryList": schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Job": schema_pkg_apis_provisioning_v0alpha1_Job(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobList": schema_pkg_apis_provisioning_v0alpha1_JobList(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobResourceSummary": schema_pkg_apis_provisioning_v0alpha1_JobResourceSummary(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobSpec": schema_pkg_apis_provisioning_v0alpha1_JobSpec(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobStatus": schema_pkg_apis_provisioning_v0alpha1_JobStatus(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_LocalRepositoryConfig(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ManagerStats": schema_pkg_apis_provisioning_v0alpha1_ManagerStats(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.MigrateJobOptions": schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.PullRequestJobOptions": schema_pkg_apis_provisioning_v0alpha1_PullRequestJobOptions(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Repository": schema_pkg_apis_provisioning_v0alpha1_Repository(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryList": schema_pkg_apis_provisioning_v0alpha1_RepositoryList(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositorySpec": schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryStatus": schema_pkg_apis_provisioning_v0alpha1_RepositoryStatus(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryView": schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryViewList": schema_pkg_apis_provisioning_v0alpha1_RepositoryViewList(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceCount": schema_pkg_apis_provisioning_v0alpha1_ResourceCount(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceList": schema_pkg_apis_provisioning_v0alpha1_ResourceList(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceListItem": schema_pkg_apis_provisioning_v0alpha1_ResourceListItem(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects": schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRepositoryInfo": schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceStats": schema_pkg_apis_provisioning_v0alpha1_ResourceStats(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceType": schema_pkg_apis_provisioning_v0alpha1_ResourceType(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceURLs": schema_pkg_apis_provisioning_v0alpha1_ResourceURLs(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceWrapper": schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncJobOptions": schema_pkg_apis_provisioning_v0alpha1_SyncJobOptions(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncOptions": schema_pkg_apis_provisioning_v0alpha1_SyncOptions(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncStatus": schema_pkg_apis_provisioning_v0alpha1_SyncStatus(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.TestResults": schema_pkg_apis_provisioning_v0alpha1_TestResults(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.WebhookResponse": schema_pkg_apis_provisioning_v0alpha1_WebhookResponse(ref), - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.WebhookStatus": schema_pkg_apis_provisioning_v0alpha1_WebhookStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author": schema_pkg_apis_provisioning_v0alpha1_Author(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.BitbucketRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitLabRepositoryConfig(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HealthStatus": schema_pkg_apis_provisioning_v0alpha1_HealthStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryItem": schema_pkg_apis_provisioning_v0alpha1_HistoryItem(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.HistoryList": schema_pkg_apis_provisioning_v0alpha1_HistoryList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Job": schema_pkg_apis_provisioning_v0alpha1_Job(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobList": schema_pkg_apis_provisioning_v0alpha1_JobList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobResourceSummary": schema_pkg_apis_provisioning_v0alpha1_JobResourceSummary(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobSpec": schema_pkg_apis_provisioning_v0alpha1_JobSpec(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.JobStatus": schema_pkg_apis_provisioning_v0alpha1_JobStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_LocalRepositoryConfig(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ManagerStats": schema_pkg_apis_provisioning_v0alpha1_ManagerStats(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.MigrateJobOptions": schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.PullRequestJobOptions": schema_pkg_apis_provisioning_v0alpha1_PullRequestJobOptions(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefItem": schema_pkg_apis_provisioning_v0alpha1_RefItem(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefList": schema_pkg_apis_provisioning_v0alpha1_RefList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Repository": schema_pkg_apis_provisioning_v0alpha1_Repository(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryList": schema_pkg_apis_provisioning_v0alpha1_RepositoryList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositorySpec": schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryStatus": schema_pkg_apis_provisioning_v0alpha1_RepositoryStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryView": schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryViewList": schema_pkg_apis_provisioning_v0alpha1_RepositoryViewList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceCount": schema_pkg_apis_provisioning_v0alpha1_ResourceCount(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceList": schema_pkg_apis_provisioning_v0alpha1_ResourceList(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceListItem": schema_pkg_apis_provisioning_v0alpha1_ResourceListItem(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects": schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRepositoryInfo": schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceStats": schema_pkg_apis_provisioning_v0alpha1_ResourceStats(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceType": schema_pkg_apis_provisioning_v0alpha1_ResourceType(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceURLs": schema_pkg_apis_provisioning_v0alpha1_ResourceURLs(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceWrapper": schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncJobOptions": schema_pkg_apis_provisioning_v0alpha1_SyncJobOptions(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncOptions": schema_pkg_apis_provisioning_v0alpha1_SyncOptions(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncStatus": schema_pkg_apis_provisioning_v0alpha1_SyncStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.TestResults": schema_pkg_apis_provisioning_v0alpha1_TestResults(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.WebhookResponse": schema_pkg_apis_provisioning_v0alpha1_WebhookResponse(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.WebhookStatus": schema_pkg_apis_provisioning_v0alpha1_WebhookStatus(ref), } } @@ -90,6 +94,60 @@ func schema_pkg_apis_provisioning_v0alpha1_Author(ref common.ReferenceCallback) } } +func schema_pkg_apis_provisioning_v0alpha1_BitbucketRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "The repository URL (e.g. `https://bitbucket.org/example/test`).", + Type: []string{"string"}, + Format: "", + }, + }, + "branch": { + SchemaProps: spec.SchemaProps{ + Description: "The branch to use in the repository.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "token": { + SchemaProps: spec.SchemaProps{ + Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", + Type: []string{"string"}, + Format: "", + }, + }, + "encryptedToken": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", + Type: []string{"string"}, + Format: "byte", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"branch"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -314,6 +372,60 @@ func schema_pkg_apis_provisioning_v0alpha1_GitHubRepositoryConfig(ref common.Ref } } +func schema_pkg_apis_provisioning_v0alpha1_GitLabRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "The repository URL (e.g. `https://gitlab.com/example/test`).", + Type: []string{"string"}, + Format: "", + }, + }, + "branch": { + SchemaProps: spec.SchemaProps{ + Description: "The branch to use in the repository.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "token": { + SchemaProps: spec.SchemaProps{ + Description: "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", + Type: []string{"string"}, + Format: "", + }, + }, + "encryptedToken": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", + Type: []string{"string"}, + Format: "byte", + }, + }, + "path": { + SchemaProps: spec.SchemaProps{ + Description: "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"branch"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_GitRepositoryConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -950,6 +1062,94 @@ func schema_pkg_apis_provisioning_v0alpha1_PullRequestJobOptions(ref common.Refe } } +func schema_pkg_apis_provisioning_v0alpha1_RefItem(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "The name of the reference (branch or tag)", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "hash": { + SchemaProps: spec.SchemaProps{ + Description: "The SHA hash of the commit this ref points to", + Type: []string{"string"}, + Format: "", + }, + }, + "refURL": { + SchemaProps: spec.SchemaProps{ + Description: "The URL to the reference (branch or tag)", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + +func schema_pkg_apis_provisioning_v0alpha1_RefList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefItem"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RefItem", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + func schema_pkg_apis_provisioning_v0alpha1_Repository(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1096,11 +1296,11 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref common.ReferenceCa }, "type": { SchemaProps: spec.SchemaProps{ - Description: "The repository type. When selected oneOf the values below should be non-nil\n\nPossible enum values:\n - `\"git\"`\n - `\"github\"`\n - `\"local\"`", + Description: "The repository type. When selected oneOf the values below should be non-nil\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"git\"`\n - `\"github\"`\n - `\"gitlab\"`\n - `\"local\"`", Default: "", Type: []string{"string"}, Format: "", - Enum: []interface{}{"git", "github", "local"}, + Enum: []interface{}{"bitbucket", "git", "github", "gitlab", "local"}, }, }, "local": { @@ -1121,12 +1321,24 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref common.ReferenceCa Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig"), }, }, + "bitbucket": { + SchemaProps: spec.SchemaProps{ + Description: "The repository on Bitbucket. Mutually exclusive with local | github | git.", + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.BitbucketRepositoryConfig"), + }, + }, + "gitlab": { + SchemaProps: spec.SchemaProps{ + Description: "The repository on GitLab. Mutually exclusive with local | github | git.", + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig"), + }, + }, }, Required: []string{"title", "workflows", "sync", "type"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncOptions"}, + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.BitbucketRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitHubRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitLabRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.GitRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.LocalRepositoryConfig", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncOptions"}, } } @@ -1217,11 +1429,11 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref common.ReferenceCa }, "type": { SchemaProps: spec.SchemaProps{ - Description: "The repository type\n\nPossible enum values:\n - `\"git\"`\n - `\"github\"`\n - `\"local\"`", + Description: "The repository type\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"git\"`\n - `\"github\"`\n - `\"gitlab\"`\n - `\"local\"`", Default: "", Type: []string{"string"}, Format: "", - Enum: []interface{}{"git", "github", "local"}, + Enum: []interface{}{"bitbucket", "git", "github", "gitlab", "local"}, }, }, "target": { @@ -1291,6 +1503,22 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryViewList(ref common.Referen Format: "", }, }, + "availableRepositoryTypes": { + SchemaProps: spec.SchemaProps{ + Description: "AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc)", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"bitbucket", "git", "github", "gitlab", "local"}, + }, + }, + }, + }, + }, "items": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -1535,11 +1763,11 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref common.Ref Properties: map[string]spec.Schema{ "type": { SchemaProps: spec.SchemaProps{ - Description: "The repository type\n\nPossible enum values:\n - `\"git\"`\n - `\"github\"`\n - `\"local\"`", + Description: "The repository type\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"git\"`\n - `\"github\"`\n - `\"gitlab\"`\n - `\"local\"`", Default: "", Type: []string{"string"}, Format: "", - Enum: []interface{}{"git", "github", "local"}, + Enum: []interface{}{"bitbucket", "git", "github", "gitlab", "local"}, }, }, "title": { diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list index 06726d0b167..6c41753b403 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -4,9 +4,11 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provis API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Errors API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobStatus,Summary API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ManagerStats,Stats +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RefList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,Workflows API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryView,Workflows +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryViewList,AvailableRepositoryTypes API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryViewList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceList,Items API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,TestResults,Errors @@ -14,6 +16,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provis API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobSpec,PullRequest API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ManagerStats,Identity API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,GitHub +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,GitLab API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceWrapper,URLs API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,SyncStatus,JobID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,WebhookResponse,Message diff --git a/pkg/apis/secret/README.md b/pkg/apis/secret/README.md deleted file mode 100644 index a375b20df06..00000000000 --- a/pkg/apis/secret/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# pkg/apis/secret - -General documentation on the Secrets API for App Platform. - -## Regenerating codegen files - -Currently the generated files are not using `grafana-app-sdk` and does not have a CUE schema definition. - -In order to regenerate the codegen files (those prefixed by `zz_`), you can run: -```sh -./hack/update-codegen.sh secret -``` - -More details [here](https://github.com/grafana/grafana/tree/main/hack#kubernetes-hack-alert). - -## Regenerating Protobuf files - -```sh -make protobuf -``` diff --git a/pkg/apis/secret/go.mod b/pkg/apis/secret/go.mod deleted file mode 100644 index 78c2b9a78fa..00000000000 --- a/pkg/apis/secret/go.mod +++ /dev/null @@ -1,98 +0,0 @@ -module github.com/grafana/grafana/pkg/apis/secret - -go 1.24.4 - -require ( - github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e - github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.73.0 - google.golang.org/protobuf v1.36.6 - gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.33.1 - k8s.io/apiserver v0.33.1 - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 -) - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // 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-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/jonboulle/clockwork v0.5.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/onsi/gomega v1.36.2 // 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.22.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/x448/float16 v0.8.4 // indirect - go.etcd.io/bbolt v1.4.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/v3 v3.5.21 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.34.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/client-go v0.33.1 // indirect - k8s.io/component-base v0.33.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) diff --git a/pkg/apis/secret/go.sum b/pkg/apis/secret/go.sum deleted file mode 100644 index 6ff9117cba7..00000000000 --- a/pkg/apis/secret/go.sum +++ /dev/null @@ -1,348 +0,0 @@ -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/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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -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/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/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-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 v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -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/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= -github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= -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 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.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -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.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -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.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/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= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= -go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= -go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= -go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= -go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= -go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA= -go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= -go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= -go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= -go.etcd.io/etcd/pkg/v3 v3.5.21 h1:jUItxeKyrDuVuWhdh0HtjUANwyuzcb7/FAeUfABmQsk= -go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU= -go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk= -go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= -go.etcd.io/etcd/server/v3 v3.5.21 h1:9w0/k12majtgarGmlMVuhwXRI2ob3/d1Ik3X5TKo0yU= -go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= -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= -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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -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/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/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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -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/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= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -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-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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/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/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-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -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/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -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.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.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/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/apis/secret/v0alpha1/doc.go b/pkg/apis/secret/v0alpha1/doc.go deleted file mode 100644 index f986c4ac24c..00000000000 --- a/pkg/apis/secret/v0alpha1/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=secret.grafana.app - -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" diff --git a/pkg/apis/secret/v0alpha1/keeper.go b/pkg/apis/secret/v0alpha1/keeper.go deleted file mode 100644 index 348df2ff914..00000000000 --- a/pkg/apis/secret/v0alpha1/keeper.go +++ /dev/null @@ -1,176 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type Keeper struct { - metav1.TypeMeta `json:",inline"` - - // Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // This is the actual keeper schema. - // +patchStrategy=replace - // +patchMergeKey=name - Spec KeeperSpec `json:"spec" patchStrategy:"replace" patchMergeKey:"name"` -} - -// KeeperType represents the type of a Keeper. -type KeeperType string - -const ( - AWSKeeperType KeeperType = "aws" - AzureKeeperType KeeperType = "azure" - GCPKeeperType KeeperType = "gcp" - HashiCorpKeeperType KeeperType = "hashicorp" -) - -func (kt KeeperType) String() string { - return string(kt) -} - -// KeeperConfig is an interface that all keeper config types must implement. -type KeeperConfig interface { - Type() KeeperType -} - -type KeeperSpec struct { - // Short description for the Keeper. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=253 - Description string `json:"description"` - - // AWS Keeper Configuration. - // +structType=atomic - // +optional - AWS *AWSKeeperConfig `json:"aws,omitempty"` - - // Azure Keeper Configuration. - // +structType=atomic - // +optional - Azure *AzureKeeperConfig `json:"azurekeyvault,omitempty"` - - // GCP Keeper Configuration. - // +structType=atomic - // +optional - GCP *GCPKeeperConfig `json:"gcp,omitempty"` - - // HashiCorp Vault Keeper Configuration. - // +structType=atomic - // +optional - HashiCorp *HashiCorpKeeperConfig `json:"hashivault,omitempty"` -} - -func (s *KeeperSpec) GetType() KeeperType { - if s.AWS != nil { - return AWSKeeperType - } - if s.Azure != nil { - return AzureKeeperType - } - if s.GCP != nil { - return GCPKeeperType - } - if s.HashiCorp != nil { - return HashiCorpKeeperType - } - return "" -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type KeeperList struct { - metav1.TypeMeta `json:",inline"` - - // Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata - // +optional - metav1.ListMeta `json:"metadata,omitempty"` - - // Slice containing all keepers. - Items []Keeper `json:"items"` -} - -// Credentials of remote keepers. -type AWSCredentials struct { - AccessKeyID CredentialValue `json:"accessKeyId"` - SecretAccessKey CredentialValue `json:"secretAccessKey"` - KMSKeyID string `json:"kmsKeyId,omitempty"` -} - -type AzureCredentials struct { - KeyVaultName string `json:"keyVaultName"` - TenantID string `json:"tenantId"` - ClientID string `json:"clientId"` - ClientSecret CredentialValue `json:"clientSecret"` -} - -type GCPCredentials struct { - ProjectID string `json:"projectId"` - CredentialsFile string `json:"credentialsFile"` -} - -type HashiCorpCredentials struct { - Address string `json:"address"` - Token CredentialValue `json:"token"` -} - -// Holds the way credentials are obtained. -// +union -type CredentialValue struct { - // The name of the secure value that holds the actual value. - // +optional - SecureValueName string `json:"secureValueName,omitempty"` - - // The value is taken from the environment variable. - // +optional - ValueFromEnv string `json:"valueFromEnv,omitempty"` - - // The value is taken from the Grafana config file. - // TODO: how do we explain that this is a path to the config file? - // +optional - ValueFromConfig string `json:"valueFromConfig,omitempty"` -} - -// System Keeper. -type SystemKeeperConfig struct{} - -func (s *SystemKeeperConfig) Type() KeeperType { - return "system" -} - -// Remote Keepers. -type AWSKeeperConfig struct { - AWSCredentials `json:",inline"` -} - -type AzureKeeperConfig struct { - AzureCredentials `json:",inline"` -} - -type GCPKeeperConfig struct { - GCPCredentials `json:",inline"` -} - -type HashiCorpKeeperConfig struct { - HashiCorpCredentials `json:",inline"` -} - -func (s *AWSKeeperConfig) Type() KeeperType { - return AWSKeeperType -} - -func (s *AzureKeeperConfig) Type() KeeperType { - return AzureKeeperType -} - -func (s *GCPKeeperConfig) Type() KeeperType { - return GCPKeeperType -} - -func (s *HashiCorpKeeperConfig) Type() KeeperType { - return HashiCorpKeeperType -} diff --git a/pkg/apis/secret/v0alpha1/register.go b/pkg/apis/secret/v0alpha1/register.go deleted file mode 100644 index f31ab882212..00000000000 --- a/pkg/apis/secret/v0alpha1/register.go +++ /dev/null @@ -1,131 +0,0 @@ -package v0alpha1 - -import ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/registry/generic" - - "github.com/grafana/grafana/pkg/apimachinery/utils" -) - -const ( - GROUP = "secret.grafana.app" - VERSION = "v0alpha1" -) - -// SecureValuesResourceInfo is used when registering the API service. -var SecureValuesResourceInfo = utils.NewResourceInfo( - GROUP, - VERSION, - "securevalues", // resource name (e.g. `kubectl get securevalues`). - "securevalue", // singular name. Used when creating a resource (e.g. `securevalue-xxx`). - "SecureValue", // kind. - func() runtime.Object { return &SecureValue{} }, // constructor for single object. This is used by the rest storage layer `Create` method. - func() runtime.Object { return &SecureValueList{} }, // constructor for list object. This is used by the rest storage layer `List` method. - utils.TableColumns{ - // This defines the fields we view in `kubectl get`. Not related with the storage layer. - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"}, - {Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"}, - {Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"}, - {Name: "Status", Type: "string", Format: "string", Description: "The status of the secure value"}, - }, - // Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`. - Reader: func(obj any) ([]interface{}, error) { - r, ok := obj.(*SecureValue) - if ok { - return []interface{}{ - r.Name, - r.Spec.Description, - r.Spec.Keeper, - r.Spec.Ref, - r.Status.Phase, - }, nil - } - - return nil, fmt.Errorf("expected SecureValue but got %T", obj) - }, - }, -) - -var KeeperResourceInfo = utils.NewResourceInfo( - GROUP, - VERSION, - "keepers", // resource name (e.g. `kubectl get keepers`). - "keeper", // singular name. Used when creating a resource (e.g. `keeper-xxx`). - "Keeper", // kind. - func() runtime.Object { return &Keeper{} }, // constructor for single object. This is used by the rest storage layer `Create` method. - func() runtime.Object { return &KeeperList{} }, // constructor for list object. This is used by the rest storage layer `List` method. - utils.TableColumns{ - // This defines the fields we view in `kubectl get`. Not related with the storage layer. - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Description", Type: "string", Format: "string", Description: "Short description for the Keeper"}, - }, - // Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`. - Reader: func(obj any) ([]interface{}, error) { - r, ok := obj.(*Keeper) - if ok { - return []interface{}{ - r.Name, - r.Spec.Description, - }, nil - } - - return nil, fmt.Errorf("expected Keeper but got %T", obj) - }, - }, -) - -var ( - // SchemeGroupVersion is group version used to register these objects. - SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} - - // SchemaBuilder is used by standard codegen, this is not used in the code otherwise. - SchemeBuilder runtime.SchemeBuilder - localSchemeBuilder = &SchemeBuilder - AddToScheme = localSchemeBuilder.AddToScheme -) - -// Adds the status phase to the selectable fields, besides the generic metadata name and namespace. -func SelectableSecureValueFields(obj *SecureValue) fields.Set { - return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{ - "status.phase": string(obj.Status.Phase), - }) -} - -// Adds the list of known types to the given scheme. -func AddKnownTypes(scheme *runtime.Scheme, version string) error { - // TODO: do we need a type for the secure value decrypt? - // Since it is a subresource, it could be interesting to not use `SecureValue`, but rather something distinct like `DecryptedSecureValue`? - scheme.AddKnownTypes( - schema.GroupVersion{Group: GROUP, Version: version}, - &SecureValue{}, - &SecureValueList{}, - &Keeper{}, - &KeeperList{}, - // &secretV0.SecureValueActivityList{}, - ) - - err := scheme.AddFieldLabelConversionFunc( - SecureValuesResourceInfo.GroupVersionKind(), - func(label, value string) (string, string, error) { - fieldSet := SelectableSecureValueFields(&SecureValue{}) - for key := range fieldSet { - if label == key { - return label, value, nil - } - } - return "", "", fmt.Errorf("field label not supported for %s: %s", SecureValuesResourceInfo.GroupVersionKind(), label) - }, - ) - if err != nil { - return err - } - return nil -} diff --git a/pkg/apis/secret/v0alpha1/secure_value.go b/pkg/apis/secret/v0alpha1/secure_value.go deleted file mode 100644 index 26a68263568..00000000000 --- a/pkg/apis/secret/v0alpha1/secure_value.go +++ /dev/null @@ -1,101 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type SecureValue struct { - metav1.TypeMeta `json:",inline"` - - // Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // This is the actual secure value schema. - Spec SecureValueSpec `json:"spec"` - - // Read-only observed status of the `SecureValue`. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status - Status SecureValueStatus `json:"status,omitempty"` -} - -// +enum -type SecureValuePhase string - -const ( - // When the `SecureValue` is created, it will start in `Pending` phase to create the underlying secret asynchronously. - SecureValuePhasePending SecureValuePhase = "Pending" - - // If the creation of the secret is successful, it will move to the `Succeeded` phase. - SecureValuePhaseSucceeded SecureValuePhase = "Succeeded" - - // If the creation of the secret fails, it will move to the `Failed` phase. - // Check the additional `status` fields for more information on what caused the failure. - // This state is unrecoverable. - SecureValuePhaseFailed SecureValuePhase = "Failed" -) - -type SecureValueStatus struct { - // High-level summary of where the `SecureValue` is in its lifecycle. - // One of: `Pending`, `Succeeded` or `Failed`. - Phase SecureValuePhase `json:"phase"` - - // A human readable message indicating details about why the `SecureValue` is in this phase. - // Only applicable if the `phase=Failed`. - // +optional - Message string `json:"message,omitempty"` - - // +optional - ExternalID string `json:"externalId,omitempty"` -} - -type SecureValueSpec struct { - // Short description that explains the purpose of this SecureValue. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=253 - Description string `json:"description"` - - // The raw value is only valid for write. Read/List will always be empty. - // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. - // Minimum and maximum lengths in bytes. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=24576 - Value ExposedSecureValue `json:"value,omitempty"` - - // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. - // This should not contain sensitive information. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=1024 - // +optional - Ref *string `json:"ref,omitempty"` - - // Name of the keeper, being the actual storage of the secure value. - // If not specified, the default keeper for the namespace will be used. - // +k8s:validation:minLength=1 - // +k8s:validation:maxLength=253 - // +optional - Keeper *string `json:"keeper,omitempty"` - - // The Decrypters that are allowed to decrypt this secret. - // An empty list means no service can decrypt it. - // +k8s:validation:maxItems=64 - // +k8s:validation:uniqueItems=true - // +listType=atomic - // +optional - Decrypters []string `json:"decrypters"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type SecureValueList struct { - metav1.TypeMeta `json:",inline"` - - // Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. - // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata - // +optional - metav1.ListMeta `json:"metadata,omitempty"` - - // Slice containing all secure values. This will NOT output decrypted values. - Items []SecureValue `json:"items"` -} diff --git a/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 727f1e43034..00000000000 --- a/pkg/apis/secret/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,384 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AWSCredentials) DeepCopyInto(out *AWSCredentials) { - *out = *in - out.AccessKeyID = in.AccessKeyID - out.SecretAccessKey = in.SecretAccessKey - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSCredentials. -func (in *AWSCredentials) DeepCopy() *AWSCredentials { - if in == nil { - return nil - } - out := new(AWSCredentials) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AWSKeeperConfig) DeepCopyInto(out *AWSKeeperConfig) { - *out = *in - out.AWSCredentials = in.AWSCredentials - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSKeeperConfig. -func (in *AWSKeeperConfig) DeepCopy() *AWSKeeperConfig { - if in == nil { - return nil - } - out := new(AWSKeeperConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AzureCredentials) DeepCopyInto(out *AzureCredentials) { - *out = *in - out.ClientSecret = in.ClientSecret - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureCredentials. -func (in *AzureCredentials) DeepCopy() *AzureCredentials { - if in == nil { - return nil - } - out := new(AzureCredentials) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AzureKeeperConfig) DeepCopyInto(out *AzureKeeperConfig) { - *out = *in - out.AzureCredentials = in.AzureCredentials - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKeeperConfig. -func (in *AzureKeeperConfig) DeepCopy() *AzureKeeperConfig { - if in == nil { - return nil - } - out := new(AzureKeeperConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CredentialValue) DeepCopyInto(out *CredentialValue) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialValue. -func (in *CredentialValue) DeepCopy() *CredentialValue { - if in == nil { - return nil - } - out := new(CredentialValue) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPCredentials) DeepCopyInto(out *GCPCredentials) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPCredentials. -func (in *GCPCredentials) DeepCopy() *GCPCredentials { - if in == nil { - return nil - } - out := new(GCPCredentials) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPKeeperConfig) DeepCopyInto(out *GCPKeeperConfig) { - *out = *in - out.GCPCredentials = in.GCPCredentials - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPKeeperConfig. -func (in *GCPKeeperConfig) DeepCopy() *GCPKeeperConfig { - if in == nil { - return nil - } - out := new(GCPKeeperConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HashiCorpCredentials) DeepCopyInto(out *HashiCorpCredentials) { - *out = *in - out.Token = in.Token - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HashiCorpCredentials. -func (in *HashiCorpCredentials) DeepCopy() *HashiCorpCredentials { - if in == nil { - return nil - } - out := new(HashiCorpCredentials) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HashiCorpKeeperConfig) DeepCopyInto(out *HashiCorpKeeperConfig) { - *out = *in - out.HashiCorpCredentials = in.HashiCorpCredentials - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HashiCorpKeeperConfig. -func (in *HashiCorpKeeperConfig) DeepCopy() *HashiCorpKeeperConfig { - if in == nil { - return nil - } - out := new(HashiCorpKeeperConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Keeper) DeepCopyInto(out *Keeper) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Keeper. -func (in *Keeper) DeepCopy() *Keeper { - if in == nil { - return nil - } - out := new(Keeper) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Keeper) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *KeeperList) DeepCopyInto(out *KeeperList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Keeper, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeeperList. -func (in *KeeperList) DeepCopy() *KeeperList { - if in == nil { - return nil - } - out := new(KeeperList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *KeeperList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *KeeperSpec) DeepCopyInto(out *KeeperSpec) { - *out = *in - if in.AWS != nil { - in, out := &in.AWS, &out.AWS - *out = new(AWSKeeperConfig) - **out = **in - } - if in.Azure != nil { - in, out := &in.Azure, &out.Azure - *out = new(AzureKeeperConfig) - **out = **in - } - if in.GCP != nil { - in, out := &in.GCP, &out.GCP - *out = new(GCPKeeperConfig) - **out = **in - } - if in.HashiCorp != nil { - in, out := &in.HashiCorp, &out.HashiCorp - *out = new(HashiCorpKeeperConfig) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeeperSpec. -func (in *KeeperSpec) DeepCopy() *KeeperSpec { - if in == nil { - return nil - } - out := new(KeeperSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecureValue) DeepCopyInto(out *SecureValue) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecureValue. -func (in *SecureValue) DeepCopy() *SecureValue { - if in == nil { - return nil - } - out := new(SecureValue) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SecureValue) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecureValueList) DeepCopyInto(out *SecureValueList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]SecureValue, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecureValueList. -func (in *SecureValueList) DeepCopy() *SecureValueList { - if in == nil { - return nil - } - out := new(SecureValueList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SecureValueList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecureValueSpec) DeepCopyInto(out *SecureValueSpec) { - *out = *in - if in.Ref != nil { - in, out := &in.Ref, &out.Ref - *out = new(string) - **out = **in - } - if in.Keeper != nil { - in, out := &in.Keeper, &out.Keeper - *out = new(string) - **out = **in - } - if in.Decrypters != nil { - in, out := &in.Decrypters, &out.Decrypters - *out = make([]string, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecureValueSpec. -func (in *SecureValueSpec) DeepCopy() *SecureValueSpec { - if in == nil { - return nil - } - out := new(SecureValueSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecureValueStatus) DeepCopyInto(out *SecureValueStatus) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecureValueStatus. -func (in *SecureValueStatus) DeepCopy() *SecureValueStatus { - if in == nil { - return nil - } - out := new(SecureValueStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SystemKeeperConfig) DeepCopyInto(out *SystemKeeperConfig) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SystemKeeperConfig. -func (in *SystemKeeperConfig) DeepCopy() *SystemKeeperConfig { - if in == nil { - return nil - } - out := new(SystemKeeperConfig) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 679e6938ae0..00000000000 --- a/pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1,8 +0,0 @@ -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AWSCredentials,AccessKeyID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AWSCredentials,KMSKeyID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,ClientID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,TenantID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,GCPCredentials,ProjectID -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index b8a14bc172b..dc92b567d4b 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -4,17 +4,17 @@ go 1.24.4 require ( github.com/google/go-cmp v0.7.0 - github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d - github.com/grafana/grafana-app-sdk/logging v0.38.2 + github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 + github.com/grafana/grafana-app-sdk/logging v0.39.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.22.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/propagators/jaeger v1.36.0 go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/otel/trace v1.36.0 - k8s.io/apimachinery v0.33.1 - k8s.io/apiserver v0.33.1 - k8s.io/component-base v0.33.1 + k8s.io/apimachinery v0.33.2 + k8s.io/apiserver v0.33.2 + k8s.io/component-base v0.33.2 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/structured-merge-diff/v4 v4.6.0 @@ -43,7 +43,7 @@ require ( github.com/google/gnostic-models v0.6.9 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed // indirect + github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 // indirect github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 // 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 @@ -90,16 +90,16 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.34.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/api v0.33.2 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 3771d878851..8006ceabbca 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -78,14 +78,14 @@ 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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed h1:k5Ng33zE9fCawqfEVybOasXY7/FQD5Qg2J92ePneeVM= -github.com/grafana/authlib v0.0.0-20250618124654-54543efcfeed/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= -github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= -github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43 h1:vVPT0i5Y1vI6qzecYStV2yk7cHKrC3Pc7AgvwT5KydQ= +github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43 h1:NlkGMnVi/oUn6Cr90QbJYpQJ4FnjyAIG9Ex5GtTZIzw= +github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914 h1:qcSGhr691f1mmPHwg2svGyO40Ex92G02aOyHzP6XHCE= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= -github.com/grafana/grafana-app-sdk/logging v0.38.2 h1:EdQTRxbbH72zdqJ09Z76zcSjfALJXkpPLgvKEPPnloc= -github.com/grafana/grafana-app-sdk/logging v0.38.2/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= @@ -329,12 +329,12 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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= @@ -364,16 +364,16 @@ 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/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/pkg/build/a11y/main.go b/pkg/build/a11y/main.go index 444e59213d2..dd85c2c3328 100644 --- a/pkg/build/a11y/main.go +++ b/pkg/build/a11y/main.go @@ -91,7 +91,7 @@ func run(ctx context.Context, cmd *cli.Command) error { hostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ Include: []string{ "./devenv", - "./e2e/test-plugins", // Directory is included so provisioning works, but they're not actually build + "./e2e-playwright/test-plugins", // Directory is included so provisioning works, but they're not actually build "./scripts/grafana-server/custom.ini", "./scripts/grafana-server/start-server", "./scripts/grafana-server/kill-server", diff --git a/pkg/build/a11y/service.go b/pkg/build/a11y/service.go index c866bc60520..98bcb5cf20b 100644 --- a/pkg/build/a11y/service.go +++ b/pkg/build/a11y/service.go @@ -22,7 +22,7 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"mkdir", "-p", "/src/grafana"}). WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). - WithDirectory("/src/grafana/e2e/test-plugins", opts.HostSrc.Directory("./e2e/test-plugins")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", opts.HostSrc.Directory("./e2e-playwright/test-plugins")). WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). WithWorkdir("/src/grafana"). WithEnvVariable("GF_APP_MODE", "development"). diff --git a/pkg/build/daggerbuild/artifacts/frontend.go b/pkg/build/daggerbuild/artifacts/frontend.go index 403c515b4e4..298f990a642 100644 --- a/pkg/build/daggerbuild/artifacts/frontend.go +++ b/pkg/build/daggerbuild/artifacts/frontend.go @@ -47,7 +47,7 @@ func (f *Frontend) BuildFile(ctx context.Context, builder *dagger.Container, opt } func (f *Frontend) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) { - return frontend.Build(builder), nil + return frontend.Build(builder, f.Version), nil } func (f *Frontend) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) { diff --git a/pkg/build/daggerbuild/artifacts/package_targz.go b/pkg/build/daggerbuild/artifacts/package_targz.go index 5e35c553042..93e09112ceb 100644 --- a/pkg/build/daggerbuild/artifacts/package_targz.go +++ b/pkg/build/daggerbuild/artifacts/package_targz.go @@ -164,7 +164,7 @@ func NewTarball( if err != nil { return nil, err } - frontendArtifact, err := NewFrontend(ctx, log, version, artifact, enterprise, src, cache) + frontendArtifact, err := NewFrontend(ctx, log, artifact, version, enterprise, src, cache) if err != nil { return nil, err } diff --git a/pkg/build/daggerbuild/frontend/build.go b/pkg/build/daggerbuild/frontend/build.go index b04cdc01767..9aec71e37d9 100644 --- a/pkg/build/daggerbuild/frontend/build.go +++ b/pkg/build/daggerbuild/frontend/build.go @@ -1,11 +1,14 @@ package frontend import ( + "fmt" + "dagger.io/dagger" ) -func Build(builder *dagger.Container) *dagger.Directory { +func Build(builder *dagger.Container, version string) *dagger.Directory { public := builder. + WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("yarn lerna version %s --exact --no-git-tag-version --no-push --force-publish -y", version)}). WithExec([]string{"yarn", "run", "build"}). WithExec([]string{"/bin/sh", "-c", "find /src/public -type d -name node_modules -print0 | xargs -0 rm -rf"}). Directory("/src/public") diff --git a/pkg/build/e2e-playwright/README.md b/pkg/build/e2e-playwright/README.md new file mode 100644 index 00000000000..d5fc311d696 --- /dev/null +++ b/pkg/build/e2e-playwright/README.md @@ -0,0 +1,22 @@ +# Pa11y accessability tests + +We use pa11y to run some automated simple accessability tests. They're ran with dagger to help orchestrate starting server + tests in a reproducable manner. + +To run the tests locally: + +1. Install dagger locally https://docs.dagger.io/install/ +2. Grab the grafana.tar.gz artifact by either + 1. Downloading it from the Github Action artifact from your PR + 1. Build it locally with: + ```sh + dagger run go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="$PWD" > dist/files.txt + cat dist/files.txt # Will output the path to the grafana.tar.gz + ``` +3. Run the dagger pipeline with: + ```sh + dagger -v run go run ./pkg/build/a11y --package=(full path to .tar.gz) --results=./pa11y-ci-results.json + ``` + The JSON results file will be saved to the file from the `--results` arg +4. If they fail and you want to see the full output + 1. Run the dagger command with `dagger -vE [...]` + 2. At the end, arrow up to the exec pa11y-ci segment and hit Enter \ No newline at end of file diff --git a/pkg/build/e2e-playwright/e2e.go b/pkg/build/e2e-playwright/e2e.go new file mode 100644 index 00000000000..35579e8dd2f --- /dev/null +++ b/pkg/build/e2e-playwright/e2e.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "dagger.io/dagger" +) + +var ( + // Locations in the container to write results to + testResultsDir = "/playwright-test-results" + htmlResultsDir = "/playwright-html-report" + blobResultsDir = "/playwright-blob-report" +) + +type RunTestOpts struct { + GrafanaService *dagger.Service + FrontendContainer *dagger.Container + HostSrc *dagger.Directory + Shard string + HTMLReportExportDir string + BlobReportExportDir string + TestResultsExportDir string +} + +func RunTest( + ctx context.Context, + d *dagger.Client, + opts RunTestOpts, +) (*dagger.Container, error) { + playwrightCommand := buildPlaywrightCommand(opts) + + e2eContainer := opts.FrontendContainer. + WithWorkdir("/src"). + WithDirectory("/src", opts.HostSrc). + WithMountedCache(".nx", d.CacheVolume("nx-cache")). + WithEnvVariable("HOST", grafanaHost). + WithEnvVariable("PORT", fmt.Sprint(grafanaPort)). + WithServiceBinding(grafanaHost, opts.GrafanaService). + WithEnvVariable("bustcache", "1"). + WithEnvVariable("PLAYWRIGHT_HTML_OPEN", "never"). + WithEnvVariable("PLAYWRIGHT_HTML_OUTPUT_DIR", htmlResultsDir). + WithEnvVariable("PLAYWRIGHT_BLOB_OUTPUT_DIR", blobResultsDir). + WithExec(playwrightCommand, dagger.ContainerWithExecOpts{ + Expect: dagger.ReturnTypeAny, + }) + + if opts.TestResultsExportDir != "" { + _, err := e2eContainer.Directory(testResultsDir).Export(ctx, opts.TestResultsExportDir) + if err != nil { + return nil, err + } + } + + if opts.HTMLReportExportDir != "" { + _, err := e2eContainer.Directory(htmlResultsDir).Export(ctx, opts.HTMLReportExportDir) + if err != nil { + return nil, err + } + } + + if opts.BlobReportExportDir != "" { + _, err := e2eContainer.Directory(blobResultsDir).Export(ctx, opts.BlobReportExportDir) + if err != nil { + return nil, err + } + } + + return e2eContainer, nil +} + +func buildPlaywrightCommand(opts RunTestOpts) []string { + playwrightReporters := []string{ + "dot", // minimal output in shards + } + + if opts.HTMLReportExportDir != "" { + playwrightReporters = append(playwrightReporters, "html") + } + + if opts.BlobReportExportDir != "" { + playwrightReporters = append(playwrightReporters, "blob") + } + + playwrightCommand := []string{ + "yarn", + "e2e:playwright", + "--reporter", + strings.Join(playwrightReporters, ","), + "--output", + testResultsDir, + } + + if opts.Shard != "" { + playwrightCommand = append(playwrightCommand, "--shard", opts.Shard) + } + + return playwrightCommand +} diff --git a/pkg/build/e2e-playwright/frontend.go b/pkg/build/e2e-playwright/frontend.go new file mode 100644 index 00000000000..625ea155ae4 --- /dev/null +++ b/pkg/build/e2e-playwright/frontend.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "dagger.io/dagger" +) + +type Deps struct { + NodeVersion string + PlaywrightVersion string +} + +// Create a container with frontend dependencies installed, ready to build plugins +// or run e2e tests. +// Theoretically we would setup Playwright in e2e.go, but to optimise layer caching +// we want it to happen before yarn install. +func WithFrontendContainer(ctx context.Context, d *dagger.Client, yarnHostSrc *dagger.Directory) (*dagger.Container, error) { + deps, err := GetVersions(ctx, yarnHostSrc) + if err != nil { + return nil, err + } + + nodeBase := WithNode(d, deps.NodeVersion) + playwrightBase := WithPlaywright(d, nodeBase, deps.PlaywrightVersion) + + return WithYarnInstall(d, playwrightBase, yarnHostSrc), nil +} + +func GetVersions(ctx context.Context, src *dagger.Directory) (Deps, error) { + nvmrc, err := src.File(".nvmrc").Contents(ctx) + if err != nil { + return Deps{}, err + } + pkgJSON, err := src.File("package.json").Contents(ctx) + if err != nil { + return Deps{}, err + } + + // parse package.json + var pkgJson struct { + DevDependencies map[string]string `json:"devDependencies"` + } + if err := json.Unmarshal([]byte(pkgJSON), &pkgJson); err != nil { + return Deps{}, err + } + + return Deps{ + NodeVersion: strings.TrimSpace(strings.TrimPrefix(nvmrc, "v")), + PlaywrightVersion: strings.TrimSpace(pkgJson.DevDependencies["@playwright/test"]), + }, nil +} + +func WithNode(d *dagger.Client, version string) *dagger.Container { + nodeImage := fmt.Sprintf("node:%s-slim", strings.TrimPrefix(version, "v")) + return d.Container().From(nodeImage) +} + +func WithPlaywright(d *dagger.Client, base *dagger.Container, version string) *dagger.Container { + brCache := d.CacheVolume("playwright-browsers") + return base. + WithEnvVariable("PLAYWRIGHT_BROWSERS_PATH", "/playwright-cache"). + WithMountedCache("/playwright-cache", brCache). + WithExec([]string{"npx", "-y", "playwright@" + version, "install", "--with-deps"}) +} + +func WithYarnInstall(d *dagger.Client, base *dagger.Container, yarnHostSrc *dagger.Directory) *dagger.Container { + yarnCache := d.CacheVolume("yarn-cache") + + return base. + WithWorkdir("/src"). + WithMountedCache("/.yarn", yarnCache). + WithEnvVariable("YARN_CACHE_FOLDER", "/.yarn"). + WithEnvVariable("CYPRESS_INSTALL_BINARY", "0"). // Don't download Cypress binaries + + // It's important to copy all files here because the whole src directory is then copied into the test runner container + WithDirectory("/src", yarnHostSrc). + WithExec([]string{"corepack", "enable"}). + WithExec([]string{"corepack", "install"}). + WithExec([]string{"yarn", "install", "--immutable"}) +} diff --git a/pkg/build/e2e-playwright/main.go b/pkg/build/e2e-playwright/main.go new file mode 100644 index 00000000000..09384c8614b --- /dev/null +++ b/pkg/build/e2e-playwright/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + + "dagger.io/dagger" + "github.com/urfave/cli/v3" +) + +var ( + grafanaHost = "grafana" + grafanaPort = 3001 +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := NewApp().Run(ctx, os.Args); err != nil { + cancel() + fmt.Println(err) + os.Exit(1) + } +} + +func NewApp() *cli.Command { + return &cli.Command{ + Name: "a11y", + Usage: "Run Grafana playwright e2e tests", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "grafana-dir", + Usage: "Path to the grafana/grafana clone directory", + Value: ".", + Validator: mustBeDir("grafana-dir", false, false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "package", + Usage: "Path to the grafana tar.gz package", + Value: "grafana.tar.gz", + Validator: mustBeFile("package", false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "license", + Usage: "Path to the Grafana Enterprise license file (optional)", + Validator: mustBeFile("license", true), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "shard", + Usage: "Test shard to run. See Playwright docs", + }, + &cli.StringFlag{ + Name: "results-dir", + Usage: "Path to the directory to export the playwright test results to (optional)", + Validator: mustBeDir("results-dir", true, true), + }, + &cli.StringFlag{ + Name: "html-dir", + Usage: "Enables the HTML reporter, exported to this directory (optional)", + Validator: mustBeDir("html-dir", true, true), + }, + &cli.StringFlag{ + Name: "blob-dir", + Usage: "Enables the blob reporter, exported to this directory. Useful with --shard (optional)", + Validator: mustBeDir("blob-dir", true, true), + }, + }, + Action: run, + } +} + +func run(ctx context.Context, cmd *cli.Command) error { + grafanaDir := cmd.String("grafana-dir") + targzPath := cmd.String("package") + licensePath := cmd.String("license") + pwShard := cmd.String("shard") + resultsDir := cmd.String("results-dir") + htmlDir := cmd.String("html-dir") + blobDir := cmd.String("blob-dir") + // pa11yConfigPath := cmd.String("config") + // pa11yResultsPath := cmd.String("results") + // noThresholdFail := cmd.Bool("no-threshold-fail") + + d, err := dagger.Connect(ctx) + if err != nil { + return fmt.Errorf("failed to connect to Dagger: %w", err) + } + + // Explicitly only the files used by the grafana-server service + grafanaHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Include: []string{ + "./devenv", + + // Must build test plugins to run e2e tests + "./e2e-playwright/test-plugins", + "./packages/grafana-plugin-configs", + + "./scripts/grafana-server/custom.ini", + "./scripts/grafana-server/start-server", + "./scripts/grafana-server/kill-server", + "./scripts/grafana-server/variables", + }, + }) + + // Minimal files needed to run yarn install + yarnHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Include: []string{ + "package.json", + "yarn.lock", + ".yarnrc.yml", + ".yarn", + "packages/*/package.json", + "packages/grafana-plugin-configs", + "public/app/plugins/*/*/package.json", + "e2e-playwright/test-plugins/*/package.json", + ".nvmrc", + }, + }) + + // Files needed to run e2e tests. yarnHostSrc will be copied into the test runner container as well. + e2eHostSrc := d.Host().Directory(".", dagger.HostDirectoryOpts{ + Include: []string{ + "public/app/types/*.d.ts", + "public/app/core/icons/cached.json", + + // packages we use in playwright tests + "packages", // TODO: do we need all of this? + "public/app/plugins", // TODO: do we need all of this? + + // e2e files + "e2e-playwright", + "e2e-playwright/test-plugins", + "playwright.config.ts", + }, + Exclude: []string{ + "**/dist", + }, + }) + + frontendContainer, err := WithFrontendContainer(ctx, d, yarnHostSrc) + if err != nil { + return fmt.Errorf("failed to create frontend container: %w", err) + } + + targz := d.Host().File(targzPath) + + var license *dagger.File + if licensePath != "" { + license = d.Host().File(licensePath) + } + + svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ + HostSrc: grafanaHostSrc, + FrontendContainer: frontendContainer, + GrafanaTarGz: targz, + License: license, + }) + if err != nil { + return fmt.Errorf("failed to create Grafana service: %w", err) + } + + runOpts := RunTestOpts{ + GrafanaService: svc, + FrontendContainer: frontendContainer, + HostSrc: e2eHostSrc, + Shard: pwShard, + TestResultsExportDir: resultsDir, + HTMLReportExportDir: htmlDir, + BlobReportExportDir: blobDir, + } + + c, runErr := RunTest(ctx, d, runOpts) + if runErr != nil { + return fmt.Errorf("failed to run e2e test suite: %w", runErr) + } + + c, syncErr := c.Sync(ctx) + if syncErr != nil { + return fmt.Errorf("failed to sync e2e test suite: %w", syncErr) + } + + code, codeErr := c.ExitCode(ctx) + if codeErr != nil { + return fmt.Errorf("failed to get exit code of e2e test suite: %w", codeErr) + } + + if code == 0 { + log.Printf("e2e tests passed with exit code %d", code) + } else { + return fmt.Errorf("e2e tests failed with exit code %d", code) + } + + log.Println("e2e tests completed successfully") + return nil +} + +func mustBeFile(arg string, emptyOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if stat.IsDir() { + return cli.Exit(arg+" must be a file, not a directory: "+s, 1) + } + return nil + } +} + +func mustBeDir(arg string, emptyOk bool, notExistOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + if notExistOk { + return nil + } + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if !stat.IsDir() { + return cli.Exit(arg+" must be a directory: "+s, 1) + } + return nil + } +} diff --git a/pkg/build/e2e-playwright/service.go b/pkg/build/e2e-playwright/service.go new file mode 100644 index 00000000000..d162451ef70 --- /dev/null +++ b/pkg/build/e2e-playwright/service.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "dagger.io/dagger" +) + +type GrafanaServiceOpts struct { + HostSrc *dagger.Directory + FrontendContainer *dagger.Container + GrafanaTarGz *dagger.File + License *dagger.File +} + +func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) { + testPlugins := opts.FrontendContainer. + WithDirectory("e2e-playwright/test-plugins", opts.HostSrc.Directory("./e2e-playwright/test-plugins")). + WithDirectory("packages/grafana-plugin-configs", opts.HostSrc.Directory("./packages/grafana-plugin-configs")). + WithExec([]string{"yarn", "e2e:plugin:build"}) + + container := d.Container().From("alpine:3"). + WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd", "util-linux"}). + WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz). + WithExec([]string{"mkdir", "-p", "/src/grafana"}). + WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). + WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", testPlugins.Directory("./e2e-playwright/test-plugins")). + WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). + WithWorkdir("/src/grafana"). + // Only set config variables here that are specific to the dagger/GHA runner. + // Prefer to configure scripts/grafana-server/custom.ini instead so they're also used + // when running locally. + WithEnvVariable("GF_SERVER_HTTP_PORT", fmt.Sprint(grafanaPort)). + WithExposedPort(grafanaPort) + + var licenseArg string + if opts.License != nil { + licenseArg = "/src/license.jwt" + container = container.WithMountedFile(licenseArg, opts.License) + } + + // We add all GF_ environment variables to allow for overriding Grafana configuration. + // It is unlikely the runner has any such otherwise. + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GF_") { + parts := strings.SplitN(env, "=", 2) + container = container.WithEnvVariable(parts[0], parts[1]) + } + } + + svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "scripts/grafana-server/start-server", licenseArg}}) + + return svc, nil +} diff --git a/pkg/build/e2e/service.go b/pkg/build/e2e/service.go index 95de093ec18..5c6eb0c4e78 100644 --- a/pkg/build/e2e/service.go +++ b/pkg/build/e2e/service.go @@ -82,6 +82,7 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). WithDirectory("/src/grafana/devenv", src.Directory("/src/devenv")). WithDirectory("/src/grafana/e2e", src.Directory("/src/e2e")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", src.Directory("/src/e2e-playwright/test-plugins")). WithDirectory("/src/grafana/scripts", src.Directory("/src/scripts")). WithDirectory("/src/grafana/tools", src.Directory("/src/tools")). WithWorkdir("/src/grafana"). diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 514f105fb0b..b1c6fdea7f2 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -8,7 +8,7 @@ go 1.24.4 replace github.com/docker/docker => github.com/moby/moby v27.5.1+incompatible require ( - cloud.google.com/go/storage v1.52.0 // @grafana/grafana-backend-group + cloud.google.com/go/storage v1.55.0 // @grafana/grafana-backend-group github.com/Masterminds/semver/v3 v3.3.1 // @grafana/grafana-developer-enablement-squad github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources github.com/docker/docker v28.1.1+incompatible // @grafana/grafana-developer-enablement-squad @@ -16,7 +16,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group - github.com/googleapis/gax-go/v2 v2.14.1 // indirect; @grafana/grafana-backend-group + github.com/googleapis/gax-go/v2 v2.14.2 // indirect; @grafana/grafana-backend-group github.com/jmespath/go-jmespath v0.4.0 // indirect; @grafana/grafana-backend-group github.com/stretchr/testify v1.10.0 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.16 // @grafana/grafana-backend-group @@ -31,18 +31,18 @@ require ( golang.org/x/sync v0.15.0 // @grafana/alerting-backend golang.org/x/text v0.26.0 // indirect; @grafana/grafana-backend-group golang.org/x/time v0.11.0 // indirect; @grafana/grafana-backend-group - google.golang.org/api v0.233.0 // @grafana/grafana-backend-group + google.golang.org/api v0.235.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.73.0 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.6 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) require ( - cloud.google.com/go v0.120.0 // indirect + cloud.google.com/go v0.121.1 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.5.0 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/buildkite/yaml v2.1.0+incompatible // indirect @@ -71,9 +71,9 @@ require ( go.opentelemetry.io/otel/metric v1.36.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/sys v0.33.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect; @grafana/grafana-backend-group - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect; @grafana/grafana-backend-group + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -87,7 +87,7 @@ require ( require ( cel.dev/expr v0.23.1 // indirect - cloud.google.com/go/monitoring v1.24.0 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect github.com/99designs/gqlgen v0.17.73 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect @@ -115,7 +115,7 @@ require ( github.com/vektah/gqlparser/v2 v2.5.27 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect @@ -123,7 +123,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect go.opentelemetry.io/otel/log v0.12.2 // indirect go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 8c8edfafedd..fa9044c8415 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -1,26 +1,26 @@ cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs= -cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= -cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= -cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= -cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= -cloud.google.com/go/storage v1.52.0 h1:ROpzMW/IwipKtatA69ikxibdzQSiXJrY9f6IgBa9AlA= -cloud.google.com/go/storage v1.52.0/go.mod h1:4wrBAbAYUvYkbrf19ahGm4I5kDQhESSqN3CGEkMGvOY= -cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= -cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dagger.io/dagger v0.18.8 h1:k3+DvD93Fy5SKijuPqFGvnQIBdJQJdfZtrGp4rqU1Xg= dagger.io/dagger v0.18.8/go.mod h1:FWhniTblKFaUK6emdtL229v9GUOgC5rqIWIzABIdJIc= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= @@ -168,8 +168,8 @@ 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/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= @@ -263,8 +263,8 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= -go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= @@ -370,19 +370,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI= -google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c= +google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= 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/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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 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= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index d7ad56620cf..5e0af708098 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -6,7 +6,7 @@ require ( cuelang.org/go v0.11.1 github.com/dave/dst v0.27.3 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.35 + github.com/grafana/cog v0.0.36 github.com/grafana/cuetsy v0.1.11 github.com/matryer/is v1.4.1 golang.org/x/tools v0.34.0 diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index f63bc60ac46..8196c0310c9 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -31,8 +31,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.35 h1:ZrGca+X17lczqhuXVCxHbLyGszP8VHGh1eAQLxbplkY= -github.com/grafana/cog v0.0.35/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= +github.com/grafana/cog v0.0.36 h1:5kijtkHRzabhCG+ck4sCAEtYp2luaBK8v1CrUHcQ0Es= +github.com/grafana/cog v0.0.36/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index de5a2e46eba..111b2b8c04c 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -28,6 +28,7 @@ import ( _ "github.com/russellhaering/goxmldsig" _ "github.com/spf13/cobra" // used by the standalone apiserver cli _ "github.com/stretchr/testify/require" + _ "gocloud.dev/secrets/hashivault" _ "golang.org/x/time/rate" _ "k8s.io/api" _ "k8s.io/apimachinery/pkg/util/httpstream/spdy" diff --git a/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go b/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go new file mode 100644 index 00000000000..3a1e2c1c423 --- /dev/null +++ b/pkg/generated/applyconfiguration/provisioning/v0alpha1/bitbucketrepositoryconfig.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// BitbucketRepositoryConfigApplyConfiguration represents a declarative configuration of the BitbucketRepositoryConfig type for use +// with apply. +type BitbucketRepositoryConfigApplyConfiguration struct { + URL *string `json:"url,omitempty"` + Branch *string `json:"branch,omitempty"` + Token *string `json:"token,omitempty"` + EncryptedToken []byte `json:"encryptedToken,omitempty"` + Path *string `json:"path,omitempty"` +} + +// BitbucketRepositoryConfigApplyConfiguration constructs a declarative configuration of the BitbucketRepositoryConfig type for use with +// apply. +func BitbucketRepositoryConfig() *BitbucketRepositoryConfigApplyConfiguration { + return &BitbucketRepositoryConfigApplyConfiguration{} +} + +// WithURL sets the URL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the URL field is set to the value of the last call. +func (b *BitbucketRepositoryConfigApplyConfiguration) WithURL(value string) *BitbucketRepositoryConfigApplyConfiguration { + b.URL = &value + return b +} + +// WithBranch sets the Branch field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Branch field is set to the value of the last call. +func (b *BitbucketRepositoryConfigApplyConfiguration) WithBranch(value string) *BitbucketRepositoryConfigApplyConfiguration { + b.Branch = &value + return b +} + +// WithToken sets the Token field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Token field is set to the value of the last call. +func (b *BitbucketRepositoryConfigApplyConfiguration) WithToken(value string) *BitbucketRepositoryConfigApplyConfiguration { + b.Token = &value + return b +} + +// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the EncryptedToken field. +func (b *BitbucketRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *BitbucketRepositoryConfigApplyConfiguration { + for i := range values { + b.EncryptedToken = append(b.EncryptedToken, values[i]) + } + return b +} + +// WithPath sets the Path field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Path field is set to the value of the last call. +func (b *BitbucketRepositoryConfigApplyConfiguration) WithPath(value string) *BitbucketRepositoryConfigApplyConfiguration { + b.Path = &value + return b +} diff --git a/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go b/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go new file mode 100644 index 00000000000..dfa6d7f9620 --- /dev/null +++ b/pkg/generated/applyconfiguration/provisioning/v0alpha1/gitlabrepositoryconfig.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// GitLabRepositoryConfigApplyConfiguration represents a declarative configuration of the GitLabRepositoryConfig type for use +// with apply. +type GitLabRepositoryConfigApplyConfiguration struct { + URL *string `json:"url,omitempty"` + Branch *string `json:"branch,omitempty"` + Token *string `json:"token,omitempty"` + EncryptedToken []byte `json:"encryptedToken,omitempty"` + Path *string `json:"path,omitempty"` +} + +// GitLabRepositoryConfigApplyConfiguration constructs a declarative configuration of the GitLabRepositoryConfig type for use with +// apply. +func GitLabRepositoryConfig() *GitLabRepositoryConfigApplyConfiguration { + return &GitLabRepositoryConfigApplyConfiguration{} +} + +// WithURL sets the URL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the URL field is set to the value of the last call. +func (b *GitLabRepositoryConfigApplyConfiguration) WithURL(value string) *GitLabRepositoryConfigApplyConfiguration { + b.URL = &value + return b +} + +// WithBranch sets the Branch field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Branch field is set to the value of the last call. +func (b *GitLabRepositoryConfigApplyConfiguration) WithBranch(value string) *GitLabRepositoryConfigApplyConfiguration { + b.Branch = &value + return b +} + +// WithToken sets the Token field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Token field is set to the value of the last call. +func (b *GitLabRepositoryConfigApplyConfiguration) WithToken(value string) *GitLabRepositoryConfigApplyConfiguration { + b.Token = &value + return b +} + +// WithEncryptedToken adds the given value to the EncryptedToken field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the EncryptedToken field. +func (b *GitLabRepositoryConfigApplyConfiguration) WithEncryptedToken(values ...byte) *GitLabRepositoryConfigApplyConfiguration { + for i := range values { + b.EncryptedToken = append(b.EncryptedToken, values[i]) + } + return b +} + +// WithPath sets the Path field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Path field is set to the value of the last call. +func (b *GitLabRepositoryConfigApplyConfiguration) WithPath(value string) *GitLabRepositoryConfigApplyConfiguration { + b.Path = &value + return b +} diff --git a/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go b/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go index 9f8a6247bb7..13d9e45d582 100644 --- a/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go +++ b/pkg/generated/applyconfiguration/provisioning/v0alpha1/repositoryspec.go @@ -11,14 +11,16 @@ import ( // RepositorySpecApplyConfiguration represents a declarative configuration of the RepositorySpec type for use // with apply. type RepositorySpecApplyConfiguration struct { - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - Workflows []provisioningv0alpha1.Workflow `json:"workflows,omitempty"` - Sync *SyncOptionsApplyConfiguration `json:"sync,omitempty"` - Type *provisioningv0alpha1.RepositoryType `json:"type,omitempty"` - Local *LocalRepositoryConfigApplyConfiguration `json:"local,omitempty"` - GitHub *GitHubRepositoryConfigApplyConfiguration `json:"github,omitempty"` - Git *GitRepositoryConfigApplyConfiguration `json:"git,omitempty"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Workflows []provisioningv0alpha1.Workflow `json:"workflows,omitempty"` + Sync *SyncOptionsApplyConfiguration `json:"sync,omitempty"` + Type *provisioningv0alpha1.RepositoryType `json:"type,omitempty"` + Local *LocalRepositoryConfigApplyConfiguration `json:"local,omitempty"` + GitHub *GitHubRepositoryConfigApplyConfiguration `json:"github,omitempty"` + Git *GitRepositoryConfigApplyConfiguration `json:"git,omitempty"` + Bitbucket *BitbucketRepositoryConfigApplyConfiguration `json:"bitbucket,omitempty"` + GitLab *GitLabRepositoryConfigApplyConfiguration `json:"gitlab,omitempty"` } // RepositorySpecApplyConfiguration constructs a declarative configuration of the RepositorySpec type for use with @@ -92,3 +94,19 @@ func (b *RepositorySpecApplyConfiguration) WithGit(value *GitRepositoryConfigApp b.Git = value return b } + +// WithBitbucket sets the Bitbucket field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Bitbucket field is set to the value of the last call. +func (b *RepositorySpecApplyConfiguration) WithBitbucket(value *BitbucketRepositoryConfigApplyConfiguration) *RepositorySpecApplyConfiguration { + b.Bitbucket = value + return b +} + +// WithGitLab sets the GitLab field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GitLab field is set to the value of the last call. +func (b *RepositorySpecApplyConfiguration) WithGitLab(value *GitLabRepositoryConfigApplyConfiguration) *RepositorySpecApplyConfiguration { + b.GitLab = value + return b +} diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index c32821294d8..a512116f25c 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -20,8 +20,12 @@ import ( func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { // Group=provisioning.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithKind("BitbucketRepositoryConfig"): + return &provisioningv0alpha1.BitbucketRepositoryConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("GitHubRepositoryConfig"): return &provisioningv0alpha1.GitHubRepositoryConfigApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("GitLabRepositoryConfig"): + return &provisioningv0alpha1.GitLabRepositoryConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("GitRepositoryConfig"): return &provisioningv0alpha1.GitRepositoryConfigApplyConfiguration{} case v0alpha1.SchemeGroupVersion.WithKind("HealthStatus"): diff --git a/pkg/infra/filestorage/file_storage_mock.go b/pkg/infra/filestorage/file_storage_mock.go index daa8eeaaec2..89918dd159d 100644 --- a/pkg/infra/filestorage/file_storage_mock.go +++ b/pkg/infra/filestorage/file_storage_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.6. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package filestorage @@ -17,6 +17,10 @@ type MockFileStorage struct { func (_m *MockFileStorage) CreateFolder(ctx context.Context, path string) error { ret := _m.Called(ctx, path) + if len(ret) == 0 { + panic("no return value specified for CreateFolder") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, path) @@ -31,6 +35,10 @@ func (_m *MockFileStorage) CreateFolder(ctx context.Context, path string) error func (_m *MockFileStorage) Delete(ctx context.Context, path string) error { ret := _m.Called(ctx, path) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, path) @@ -45,6 +53,10 @@ func (_m *MockFileStorage) Delete(ctx context.Context, path string) error { func (_m *MockFileStorage) DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error { ret := _m.Called(ctx, path, options) + if len(ret) == 0 { + panic("no return value specified for DeleteFolder") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, *DeleteFolderOptions) error); ok { r0 = rf(ctx, path, options) @@ -59,7 +71,16 @@ func (_m *MockFileStorage) DeleteFolder(ctx context.Context, path string, option func (_m *MockFileStorage) Get(ctx context.Context, path string, options *GetFileOptions) (*File, bool, error) { ret := _m.Called(ctx, path, options) + if len(ret) == 0 { + panic("no return value specified for Get") + } + var r0 *File + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *GetFileOptions) (*File, bool, error)); ok { + return rf(ctx, path, options) + } if rf, ok := ret.Get(0).(func(context.Context, string, *GetFileOptions) *File); ok { r0 = rf(ctx, path, options) } else { @@ -68,14 +89,12 @@ func (_m *MockFileStorage) Get(ctx context.Context, path string, options *GetFil } } - var r1 bool if rf, ok := ret.Get(1).(func(context.Context, string, *GetFileOptions) bool); ok { r1 = rf(ctx, path, options) } else { r1 = ret.Get(1).(bool) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, string, *GetFileOptions) error); ok { r2 = rf(ctx, path, options) } else { @@ -89,7 +108,15 @@ func (_m *MockFileStorage) Get(ctx context.Context, path string, options *GetFil func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) { ret := _m.Called(ctx, folderPath, paging, options) + if len(ret) == 0 { + panic("no return value specified for List") + } + var r0 *ListResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *Paging, *ListOptions) (*ListResponse, error)); ok { + return rf(ctx, folderPath, paging, options) + } if rf, ok := ret.Get(0).(func(context.Context, string, *Paging, *ListOptions) *ListResponse); ok { r0 = rf(ctx, folderPath, paging, options) } else { @@ -98,7 +125,6 @@ func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging * } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, *Paging, *ListOptions) error); ok { r1 = rf(ctx, folderPath, paging, options) } else { @@ -112,6 +138,10 @@ func (_m *MockFileStorage) List(ctx context.Context, folderPath string, paging * func (_m *MockFileStorage) Upsert(ctx context.Context, command *UpsertFileCommand) error { ret := _m.Called(ctx, command) + if len(ret) == 0 { + panic("no return value specified for Upsert") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *UpsertFileCommand) error); ok { r0 = rf(ctx, command) @@ -122,10 +152,14 @@ func (_m *MockFileStorage) Upsert(ctx context.Context, command *UpsertFileComman return r0 } -// close provides a mock function with given fields: +// close provides a mock function with no fields func (_m *MockFileStorage) close() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for close") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -135,3 +169,17 @@ func (_m *MockFileStorage) close() error { return r0 } + +// NewMockFileStorage creates a new instance of MockFileStorage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockFileStorage(t interface { + mock.TestingT + Cleanup(func()) +}) *MockFileStorage { + mock := &MockFileStorage{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index 2d60aa29221..e952d013ca4 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -725,6 +725,10 @@ type VariableModel struct { // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. Regex *string `json:"regex,omitempty"` + // Additional static options for query variable + StaticOptions []VariableOption `json:"staticOptions,omitempty"` + // Ordering of static options in relation to options returned from data source for query variable + StaticOptionsOrder *VariableModelStaticOptionsOrder `json:"staticOptionsOrder,omitempty"` } // NewVariableModel creates a new VariableModel object. @@ -1045,6 +1049,14 @@ const ( DataTransformerConfigTopicAlertStates DataTransformerConfigTopic = "alertStates" ) +type VariableModelStaticOptionsOrder string + +const ( + VariableModelStaticOptionsOrderBefore VariableModelStaticOptionsOrder = "before" + VariableModelStaticOptionsOrderAfter VariableModelStaticOptionsOrder = "after" + VariableModelStaticOptionsOrderSorted VariableModelStaticOptionsOrder = "sorted" +) + type ValueMapOrRangeMapOrRegexMapOrSpecialValueMap struct { ValueMap *ValueMap `json:"ValueMap,omitempty"` RangeMap *RangeMap `json:"RangeMap,omitempty"` diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index f06373df124..774cdb540ad 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -29,12 +29,16 @@ import ( "github.com/grafana/grafana/pkg/util" ) -const forceUseGraphAPIKey = "force_use_graph_api" // #nosec G101 not a hardcoded credential +const ( + forceUseGraphAPIKey = "force_use_graph_api" // #nosec G101 not a hardcoded credential + domainHintKey = "domain_hint" +) var ( ExtraAzureADSettingKeys = map[string]ExtraKeyInfo{ forceUseGraphAPIKey: {Type: Bool, DefaultValue: false}, allowedOrganizationsKey: {Type: String}, + domainHintKey: {Type: String}, } errAzureADMissingGroups = &SocialError{"either the user does not have any group membership or the groups claim is missing from the token."} ) @@ -288,7 +292,8 @@ func (s *SocialAzureAD) Validate(ctx context.Context, newSettings ssoModels.SSOS validateAllowedGroups, validation.MustBeEmptyValidator(info.ApiUrl, "API URL"), validation.RequiredUrlValidator(info.AuthUrl, "Auth URL"), - validation.RequiredUrlValidator(info.TokenUrl, "Token URL")) + validation.RequiredUrlValidator(info.TokenUrl, "Token URL"), + validation.DomainValidator(info.Extra[domainHintKey], "Domain Hint")) } func validateAllowedGroups(info *social.OAuthInfo, requester identity.Requester) error { @@ -323,6 +328,17 @@ func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client, return claims, nil } +func (s *SocialAzureAD) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + s.reloadMutex.RLock() + defer s.reloadMutex.RUnlock() + + if domainHint, ok := s.info.Extra[domainHintKey]; ok && domainHint != "" { + opts = append(opts, oauth2.SetAuthURLParam("domain_hint", domainHint)) + } + + return s.Config.AuthCodeURL(state, opts...) +} + func (s *SocialAzureAD) validateIDTokenSignature(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) { var claims azureClaims diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index 7d1d5842a47..02b93285e93 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -245,78 +245,136 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, defer s.reloadMutex.RUnlock() s.log.Debug("Getting user info") - toCheck := make([]*UserInfoJson, 0, 2) - if tokenData := s.extractFromToken(token); tokenData != nil { - toCheck = append(toCheck, tokenData) + // 1. Collect user info data from various sources + dataSources := s.collectUserInfoData(ctx, client, token) + + // 2. Build user info from collected data + userInfo, externalOrgs, err := s.buildUserInfo(dataSources) + if err != nil { + return nil, err + } + + // 3. Post-process user info + err = s.postProcessUserInfo(ctx, client, userInfo, externalOrgs) + if err != nil { + return nil, err + } + + // 4. Validate user access + err = s.validateUserAccess(ctx, client, userInfo) + if err != nil { + return nil, err + } + + s.log.Debug("User info result", "result", userInfo) + return userInfo, nil +} + +// collectUserInfoData gathers user information from ID token, API, and access token +func (s *SocialGenericOAuth) collectUserInfoData(ctx context.Context, client *http.Client, token *oauth2.Token) []*UserInfoJson { + dataSources := make([]*UserInfoJson, 0, 3) + + if idTokenData := s.extractFromIDToken(token); idTokenData != nil { + dataSources = append(dataSources, idTokenData) } if apiData := s.extractFromAPI(ctx, client); apiData != nil { - toCheck = append(toCheck, apiData) + dataSources = append(dataSources, apiData) + } + if accessTokenData := s.extractFromAccessToken(token); accessTokenData != nil { + dataSources = append(dataSources, accessTokenData) } + return dataSources +} + +// buildUserInfo constructs BasicUserInfo from collected data sources +func (s *SocialGenericOAuth) buildUserInfo(dataSources []*UserInfoJson) (*social.BasicUserInfo, []string, error) { userInfo := &social.BasicUserInfo{} var externalOrgs []string - for _, data := range toCheck { + + for _, data := range dataSources { s.log.Debug("Processing external user info", "source", data.source, "data", data) - if userInfo.Id == "" { - userInfo.Id = data.Sub + s.extractBasicUserFields(userInfo, data) + + if err := s.extractRoleAndOrgs(userInfo, &externalOrgs, data); err != nil { + return nil, nil, err } - if userInfo.Name == "" { - userInfo.Name = s.extractUserName(data) - } + s.extractUserGroups(userInfo, data) + } - if userInfo.Login == "" { - userInfo.Login = s.extractLogin(data) - } + return userInfo, externalOrgs, nil +} - if userInfo.Email == "" { - userInfo.Email = s.extractEmail(data) - if userInfo.Email != "" { - s.log.Debug("Set user info email from extracted email", "email", userInfo.Email) - } - } +// extractBasicUserFields extracts basic user fields (ID, Name, Login, Email) from data +func (s *SocialGenericOAuth) extractBasicUserFields(userInfo *social.BasicUserInfo, data *UserInfoJson) { + if userInfo.Id == "" { + userInfo.Id = data.Sub + } - if userInfo.Role == "" && !s.info.SkipOrgRoleSync { - role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{}) - if err != nil { - s.log.Warn("Failed to extract role", "err", err) - } else { - userInfo.Role = role - if s.info.AllowAssignGrafanaAdmin { - userInfo.IsGrafanaAdmin = &grafanaAdmin - } - } - } + if userInfo.Name == "" { + userInfo.Name = s.extractUserName(data) + } - if len(externalOrgs) == 0 && !s.info.SkipOrgRoleSync { - var err error - externalOrgs, err = s.extractOrgs(data.rawJSON) - if err != nil { - s.log.Warn("Failed to extract orgs", "err", err) - return nil, err - } - } + if userInfo.Login == "" { + userInfo.Login = s.extractLogin(data) + } - if len(userInfo.Groups) == 0 { - groups, err := s.extractGroups(data) - if err != nil { - s.log.Warn("Failed to extract groups", "err", err) - } else if len(groups) > 0 { - s.log.Debug("Setting user info groups from extracted groups") - userInfo.Groups = groups + if userInfo.Email == "" { + userInfo.Email = s.extractEmail(data) + if userInfo.Email != "" { + s.log.Debug("Set user info email from extracted email", "email", userInfo.Email) + } + } +} + +// extractRoleAndOrgs extracts role and organization information from data +func (s *SocialGenericOAuth) extractRoleAndOrgs(userInfo *social.BasicUserInfo, externalOrgs *[]string, data *UserInfoJson) error { + if userInfo.Role == "" && !s.info.SkipOrgRoleSync { + role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{}) + if err != nil { + s.log.Warn("Failed to extract role", "err", err) + } else { + userInfo.Role = role + if s.info.AllowAssignGrafanaAdmin { + userInfo.IsGrafanaAdmin = &grafanaAdmin } } } + if len(*externalOrgs) == 0 && !s.info.SkipOrgRoleSync { + orgs, err := s.extractOrgs(data.rawJSON) + if err != nil { + s.log.Warn("Failed to extract orgs", "err", err) + return err + } + *externalOrgs = orgs + } + + return nil +} + +// extractUserGroups extracts group information from data +func (s *SocialGenericOAuth) extractUserGroups(userInfo *social.BasicUserInfo, data *UserInfoJson) { + if len(userInfo.Groups) == 0 { + groups, err := s.extractGroups(data) + if err != nil { + s.log.Warn("Failed to extract groups", "err", err) + } else if len(groups) > 0 { + s.log.Debug("Setting user info groups from extracted groups") + userInfo.Groups = groups + } + } +} + +// postProcessUserInfo handles post-processing of user info (org roles, private email, etc.) +func (s *SocialGenericOAuth) postProcessUserInfo(ctx context.Context, client *http.Client, userInfo *social.BasicUserInfo, externalOrgs []string) error { if !s.info.SkipOrgRoleSync { userInfo.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, externalOrgs, userInfo.Role) if s.info.RoleAttributeStrict && len(userInfo.OrgRoles) == 0 { - // If no roles are found and role_attribute_strict is set, return an error. - // The s.info.RoleAttributeStrict is necessary, because there is a case when len(userInfo.OrgRoles) == 0, - // but strict role mapping is not enabled (when getAllOrgs fails). - return nil, errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data") + return errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data") } } @@ -325,11 +383,11 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, } if s.canFetchPrivateEmail(userInfo) { - var err error - userInfo.Email, err = s.fetchPrivateEmail(ctx, client) + email, err := s.fetchPrivateEmail(ctx, client) if err != nil { - return nil, err + return err } + userInfo.Email = email s.log.Debug("Setting email from fetched private email", "email", userInfo.Email) } @@ -338,28 +396,32 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, userInfo.Login = userInfo.Email } + return nil +} + +// validateUserAccess validates user access based on team, organization, and group membership +func (s *SocialGenericOAuth) validateUserAccess(ctx context.Context, client *http.Client, userInfo *social.BasicUserInfo) error { if !s.isTeamMember(ctx, client) { - return nil, &SocialError{"User not a member of one of the required teams"} + return &SocialError{"User not a member of one of the required teams"} } if !s.isOrganizationMember(ctx, client) { - return nil, &SocialError{"User not a member of one of the required organizations"} + return &SocialError{"User not a member of one of the required organizations"} } if !s.isGroupMember(userInfo.Groups) { - return nil, errMissingGroupMembership + return errMissingGroupMembership } - s.log.Debug("User info result", "result", userInfo) - return userInfo, nil + return nil } func (s *SocialGenericOAuth) canFetchPrivateEmail(userinfo *social.BasicUserInfo) bool { return s.info.ApiUrl != "" && userinfo.Email == "" } -func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson { - s.log.Debug("Extracting user info from OAuth token") +func (s *SocialGenericOAuth) extractFromIDToken(token *oauth2.Token) *UserInfoJson { + s.log.Debug("Extracting user info from OAuth ID token") idTokenAttribute := "id_token" if s.idTokenAttributeName != "" { @@ -373,21 +435,44 @@ func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson return nil } - rawJSON, err := s.retrieveRawIDToken(idToken) + rawJSON, err := s.retrieveRawJWTPayload(idToken) if err != nil { - s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", token)) + s.log.Warn("Error retrieving id_token payload", "error", err, "token", fmt.Sprintf("%+v", token)) return nil } + return s.parseUserInfoFromJSON(rawJSON, "id_token") +} + +func (s *SocialGenericOAuth) extractFromAccessToken(token *oauth2.Token) *UserInfoJson { + s.log.Debug("Extracting user info from OAuth access token") + + accessToken := token.AccessToken + if accessToken == "" { + s.log.Debug("No access token found") + return nil + } + + rawJSON, err := s.retrieveRawJWTPayload(accessToken) + if err != nil { + s.log.Warn("Error retrieving access token payload", "error", err) + return nil + } + + return s.parseUserInfoFromJSON(rawJSON, "access_token") +} + +// parseUserInfoFromJSON is a helper method to parse UserInfoJson from raw JSON and source +func (s *SocialGenericOAuth) parseUserInfoFromJSON(rawJSON []byte, source string) *UserInfoJson { var data UserInfoJson if err := json.Unmarshal(rawJSON, &data); err != nil { - s.log.Error("Error decoding id_token JSON", "raw_json", string(rawJSON), "error", err) + s.log.Error("Error decoding user info JSON", "raw_json", string(rawJSON), "error", err, "source", source) return nil } data.rawJSON = rawJSON - data.source = "token" - s.log.Debug("Received id_token", "raw_json", string(data.rawJSON), "data", data.String()) + data.source = source + s.log.Debug("Parsed user info from JSON", "raw_json", string(rawJSON), "data", data.String(), "source", source) return &data } @@ -404,18 +489,7 @@ func (s *SocialGenericOAuth) extractFromAPI(ctx context.Context, client *http.Cl return nil } - rawJSON := rawUserInfoResponse.Body - - var data UserInfoJson - if err := json.Unmarshal(rawJSON, &data); err != nil { - s.log.Error("Error decoding user info response", "raw_json", rawJSON, "error", err) - return nil - } - - data.rawJSON = rawJSON - data.source = "API" - s.log.Debug("Received user info response from API", "raw_json", string(rawJSON), "data", data.String()) - return &data + return s.parseUserInfoFromJSON(rawUserInfoResponse.Body, "API") } func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string { diff --git a/pkg/login/social/connectors/generic_oauth_test.go b/pkg/login/social/connectors/generic_oauth_test.go index 881ed9fd541..b4b7469cc05 100644 --- a/pkg/login/social/connectors/generic_oauth_test.go +++ b/pkg/login/social/connectors/generic_oauth_test.go @@ -31,6 +31,7 @@ func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) { AllowAssignGrafanaAdmin bool ResponseBody any OAuth2Extra any + AccessToken string Setup func(*orgtest.FakeOrgService) RoleAttributePath string RoleAttributeStrict bool @@ -440,6 +441,62 @@ func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) { ExpectedEmail: "john.doe@example.com", ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer}, }, + // Access Token Test Cases + { + Name: "Given a valid access token with role, no ID token, no API response, use access token", + ResponseBody: map[string]any{}, + OAuth2Extra: map[string]any{}, + AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiRWRpdG9yIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20ifQ.oVEMSJVqBwrGXOcwGgXL_8J-CZhgFVPjXXSqzPJQ5JU", // { "role": "Editor", "email": "access.token@example.com" } + RoleAttributePath: "role", + ExpectedEmail: "access.token@example.com", + ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor}, + }, + { + Name: "Given a valid access token with org roles, no ID token, no API response, use access token", + ResponseBody: map[string]any{}, + OAuth2Extra: map[string]any{}, + AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiVmlld2VyIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20iLCJpbmZvIjp7InJvbGVzIjpbImFjY2Vzcy1kZXYiLCJhY2Nlc3Mtb3BzIl19fQ.g8-mNJQDL9CJWgRTFdKBRRKbsHZfFhJrzPYQGXfxGIE", // { "role": "Viewer", "email": "access.token@example.com", "info": { "roles": [ "access-dev", "access-ops" ] }} + RoleAttributePath: "role", + OrgAttributePath: "info.roles", + OrgMapping: []string{"access-dev:org_dev:Admin", "access-ops:org_engineering:Editor"}, + ExpectedEmail: "access.token@example.com", + ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleAdmin, 5: org.RoleEditor}, + }, + { + Name: "Given a valid access token and ID token, prefer ID token", + ResponseBody: map[string]any{}, + OAuth2Extra: map[string]any{ + // { "role": "Admin", "email": "id.token@example.com" } + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImlkLnRva2VuQGV4YW1wbGUuY29tIn0.T8wcoOOPQ_av9VsOFoYJZGNFGJgG0d3LPDvtxvgODkU", + }, + AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiRWRpdG9yIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20ifQ.oVEMSJVqBwrGXOcwGgXL_8J-CZhgFVPjXXSqzPJQ5JU", // { "role": "Editor", "email": "access.token@example.com" } + RoleAttributePath: "role", + ExpectedEmail: "id.token@example.com", + ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin}, + }, + { + Name: "Given a valid access token with no email, ID token with no role, API response with no data, merge", + ResponseBody: map[string]any{}, + OAuth2Extra: map[string]any{ + // { "email": "id.token@example.com" } + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImlkLnRva2VuQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4", + }, + AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiRWRpdG9yIn0.gfnKWZKNFNqrILhHFzabBVEWnJJIZBmQSBwLPCHhLUY", // { "role": "Editor" } + RoleAttributePath: "role", + ExpectedEmail: "id.token@example.com", + ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor}, + }, + { + Name: "Given a valid access token with GrafanaAdmin role and AssignGrafanaAdmin enabled", + AllowAssignGrafanaAdmin: true, + ResponseBody: map[string]any{}, + OAuth2Extra: map[string]any{}, + AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20ifQ.fJPjMgZW9bOYXOLgOUekNQmNrVbUNhU1iqQJwqFWzUY", // { "role": "GrafanaAdmin", "email": "access.token@example.com" } + RoleAttributePath: "role", + ExpectedEmail: "access.token@example.com", + ExpectedGrafanaAdmin: trueBoolPtr(), + ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin}, + }, } cfg := &setting.Cfg{ @@ -479,8 +536,9 @@ func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) { require.NoError(t, err) })) provider.info.ApiUrl = ts.URL + staticToken := oauth2.Token{ - AccessToken: "", + AccessToken: tc.AccessToken, TokenType: "", RefreshToken: "", Expiry: time.Now(), @@ -853,7 +911,7 @@ func TestPayloadCompression(t *testing.T) { } token := staticToken.WithExtra(test.OAuth2Extra) - userInfo := provider.extractFromToken(token) + userInfo := provider.extractFromIDToken(token) if test.ExpectedEmail == "" { require.Nil(t, userInfo, "Testing case %q", test.Name) diff --git a/pkg/login/social/connectors/gitlab_oauth.go b/pkg/login/social/connectors/gitlab_oauth.go index 8545040aa0f..2a2d2b7100b 100644 --- a/pkg/login/social/connectors/gitlab_oauth.go +++ b/pkg/login/social/connectors/gitlab_oauth.go @@ -275,7 +275,7 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client return nil, nil } - rawJSON, err := s.retrieveRawIDToken(idToken) + rawJSON, err := s.retrieveRawJWTPayload(idToken) if err != nil { s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken)) return nil, nil @@ -302,6 +302,8 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client data.Groups = userInfo.Groups } + data.raw = rawJSON + s.log.Debug("Resolved user data", "data", fmt.Sprintf("%+v", data)) return &data, nil } diff --git a/pkg/login/social/connectors/gitlab_oauth_test.go b/pkg/login/social/connectors/gitlab_oauth_test.go index 09a621f645f..60fdc27d6ee 100644 --- a/pkg/login/social/connectors/gitlab_oauth_test.go +++ b/pkg/login/social/connectors/gitlab_oauth_test.go @@ -37,6 +37,8 @@ const ( rootUserRespBody = `{"id":1,"username":"root","name":"Administrator","state":"active","email":"root@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":true,"namespace_id":1}` editorUserRespBody = `{"id":3,"username":"gitlab-editor","name":"Gitlab Editor","state":"active","email":"gitlab-editor@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":false,"namespace_id":1}` + editorUserIDToken = `{"sub":"3","preferred_username":"gitlab-editor","name":"Gitlab Editor","email":"gitlab-editor@example.org","email_verified":true,"groups_direct":["editors", "viewers"]}` // #nosec G101 not a hardcoded credential + adminGroup = `{"id":4,"web_url":"http://grafana-gitlab.local/groups/admins","name":"Admins","path":"admins","project_creation_level":"developer","full_name":"Admins","full_path":"admins","created_at":"2022-09-13T19:38:04.891Z"}` editorGroup = `{"id":5,"web_url":"http://grafana-gitlab.local/groups/editors","name":"Editors","path":"editors","project_creation_level":"developer","full_name":"Editors","full_path":"editors","created_at":"2022-09-13T19:38:15.074Z"}` viewerGroup = `{"id":6,"web_url":"http://grafana-gitlab.local/groups/viewers","name":"Viewers","path":"viewers","project_creation_level":"developer","full_name":"Viewers","full_path":"viewers","created_at":"2022-09-13T19:38:25.777Z"}` @@ -61,6 +63,7 @@ func TestSocialGitlab_UserInfo(t *testing.T) { GroupsRespBody string GroupHeaders map[string]string RoleAttributePath string + IDToken string ExpectedLogin string ExpectedEmail string ExpectedRoles map[int64]org.RoleType @@ -180,6 +183,24 @@ func TestSocialGitlab_UserInfo(t *testing.T) { ExpectedEmail: "gitlab-editor@example.org", ExpectedRoles: map[int64]org.RoleType{4: "Editor", 5: "Viewer"}, }, + { + Name: "Maps roles from ID token attributes if available", + RoleAttributePath: `email=='gitlab-editor@example.org' && 'Editor' || 'Viewer'`, + IDToken: editorUserIDToken, + ExpectedLogin: "gitlab-editor", + ExpectedEmail: "gitlab-editor@example.org", + ExpectedRoles: map[int64]org.RoleType{1: "Editor"}, + ExpectedGrafanaAdmin: nilPointer, + }, + { + Name: "Maps groups from ID token groups if available", + RoleAttributePath: gitlabAttrPath, + IDToken: editorUserIDToken, + ExpectedLogin: "gitlab-editor", + ExpectedEmail: "gitlab-editor@example.org", + ExpectedRoles: map[int64]org.RoleType{1: "Editor"}, + ExpectedGrafanaAdmin: nilPointer, + }, { Name: "Should return error when neither role attribute path nor org mapping evaluates to a role and role attribute strict is enabled", Cfg: conf{RoleAttributeStrict: true, OrgMapping: []string{"other:Org4:Editor"}}, @@ -230,8 +251,17 @@ func TestSocialGitlab_UserInfo(t *testing.T) { require.Fail(t, "unexpected request URI: "+r.RequestURI) } })) + + token := &oauth2.Token{} + if tt.IDToken != "" { + emptyJWTHeader := base64.RawURLEncoding.EncodeToString([]byte("{}")) + JWTBody := base64.RawURLEncoding.EncodeToString([]byte(tt.IDToken)) + idToken := fmt.Sprintf("%s.%s.signature", emptyJWTHeader, JWTBody) + token = token.WithExtra(map[string]any{"id_token": idToken}) + } + provider.info.ApiUrl = ts.URL + apiURI - actualResult, err := provider.UserInfo(context.Background(), ts.Client(), &oauth2.Token{}) + actualResult, err := provider.UserInfo(context.Background(), ts.Client(), token) if tt.ExpectedError != nil { require.ErrorIs(t, err, tt.ExpectedError) return @@ -382,6 +412,9 @@ func TestSocialGitlab_extractFromToken(t *testing.T) { } for _, tc := range testCases { + if tc.wantUser != nil { + tc.wantUser.raw = []byte(tc.payload) + } t.Run(tc.name, func(t *testing.T) { // Create a test client with a dummy token client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"}) diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 2191a2c01e8..4e5d7a3f8f8 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -236,7 +236,7 @@ func (s *SocialGoogle) extractFromToken(_ context.Context, _ *http.Client, token return nil, nil } - rawJSON, err := s.retrieveRawIDToken(idToken) + rawJSON, err := s.retrieveRawJWTPayload(idToken) if err != nil { s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken)) return nil, nil diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index 35bc67004c5..e6bda1c4f81 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -196,21 +196,21 @@ func (s *SocialBase) isGroupMember(groups []string) bool { return false } -func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) { - tokenString, ok := idToken.(string) +func (s *SocialBase) retrieveRawJWTPayload(token any) ([]byte, error) { + tokenString, ok := token.(string) if !ok { - return nil, fmt.Errorf("id_token is not a string: %v", idToken) + return nil, fmt.Errorf("token is not a string: %v", token) } jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$") matched := jwtRegexp.FindStringSubmatch(tokenString) if matched == nil { - return nil, fmt.Errorf("id_token is not in JWT format: %s", tokenString) + return nil, fmt.Errorf("token is not in JWT format: %s", tokenString) } rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2]) if err != nil { - return nil, fmt.Errorf("error base64 decoding id_token: %w", err) + return nil, fmt.Errorf("error base64 decoding token payload: %w", err) } headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1]) diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index 870c13cf8e3..74760f0a3b5 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -36,7 +36,7 @@ func TestIntegrationSocialService_ProvideService(t *testing.T) { expectedGenericOAuthSkipOrgRoleSync bool }{ { - name: "should load all social connectors when ssoSettingsApi is enabled", + name: "should load all social connectors", expectedSocialMapLength: 7, expectedGenericOAuthSkipOrgRoleSync: false, }, diff --git a/pkg/login/social/socialtest/social_connector_mock.go b/pkg/login/social/socialtest/social_connector_mock.go index 17ed902bd01..4150fcb5301 100644 --- a/pkg/login/social/socialtest/social_connector_mock.go +++ b/pkg/login/social/socialtest/social_connector_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.27.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package socialtest @@ -31,6 +31,10 @@ func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCode _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for AuthCodeURL") + } + var r0 string if rf, ok := ret.Get(0).(func(string, ...oauth2.AuthCodeOption) string); ok { r0 = rf(state, opts...) @@ -45,6 +49,10 @@ func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCode func (_m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *http.Client { ret := _m.Called(ctx, t) + if len(ret) == 0 { + panic("no return value specified for Client") + } + var r0 *http.Client if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) *http.Client); ok { r0 = rf(ctx, t) @@ -68,6 +76,10 @@ func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOp _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for Exchange") + } + var r0 *oauth2.Token var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)); ok { @@ -90,10 +102,14 @@ func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOp return r0, r1 } -// GetOAuthInfo provides a mock function with given fields: +// GetOAuthInfo provides a mock function with no fields func (_m *MockSocialConnector) GetOAuthInfo() *social.OAuthInfo { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for GetOAuthInfo") + } + var r0 *social.OAuthInfo if rf, ok := ret.Get(0).(func() *social.OAuthInfo); ok { r0 = rf() @@ -110,6 +126,10 @@ func (_m *MockSocialConnector) GetOAuthInfo() *social.OAuthInfo { func (_m *MockSocialConnector) IsEmailAllowed(email string) bool { ret := _m.Called(email) + if len(ret) == 0 { + panic("no return value specified for IsEmailAllowed") + } + var r0 bool if rf, ok := ret.Get(0).(func(string) bool); ok { r0 = rf(email) @@ -120,10 +140,14 @@ func (_m *MockSocialConnector) IsEmailAllowed(email string) bool { return r0 } -// IsSignupAllowed provides a mock function with given fields: +// IsSignupAllowed provides a mock function with no fields func (_m *MockSocialConnector) IsSignupAllowed() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for IsSignupAllowed") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -138,6 +162,10 @@ func (_m *MockSocialConnector) IsSignupAllowed() bool { func (_m *MockSocialConnector) SupportBundleContent(_a0 *bytes.Buffer) error { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for SupportBundleContent") + } + var r0 error if rf, ok := ret.Get(0).(func(*bytes.Buffer) error); ok { r0 = rf(_a0) @@ -152,6 +180,10 @@ func (_m *MockSocialConnector) SupportBundleContent(_a0 *bytes.Buffer) error { func (_m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource { ret := _m.Called(ctx, t) + if len(ret) == 0 { + panic("no return value specified for TokenSource") + } + var r0 oauth2.TokenSource if rf, ok := ret.Get(0).(func(context.Context, *oauth2.Token) oauth2.TokenSource); ok { r0 = rf(ctx, t) @@ -168,6 +200,10 @@ func (_m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) func (_m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) { ret := _m.Called(ctx, client, token) + if len(ret) == 0 { + panic("no return value specified for UserInfo") + } + var r0 *social.BasicUserInfo var r1 error if rf, ok := ret.Get(0).(func(context.Context, *http.Client, *oauth2.Token) (*social.BasicUserInfo, error)); ok { @@ -190,13 +226,12 @@ func (_m *MockSocialConnector) UserInfo(ctx context.Context, client *http.Client return r0, r1 } -type mockConstructorTestingTNewMockSocialConnector interface { +// NewMockSocialConnector creates a new instance of MockSocialConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSocialConnector(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockSocialConnector creates a new instance of MockSocialConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockSocialConnector(t mockConstructorTestingTNewMockSocialConnector) *MockSocialConnector { +}) *MockSocialConnector { mock := &MockSocialConnector{} mock.Mock.Test(t) diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 61c1a32c6cf..f2f2a420e22 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -7,7 +7,7 @@ replace github.com/grafana/grafana/pkg/codegen => ../../codegen require ( cuelang.org/go v0.11.1 github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d - github.com/grafana/cog v0.0.35 + github.com/grafana/cog v0.0.36 github.com/grafana/cuetsy v0.1.11 github.com/grafana/grafana/pkg/codegen v0.0.0-20250514132646-acbc7b54ed9e ) diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index b4b6d753637..1771a71bf9b 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -30,8 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= -github.com/grafana/cog v0.0.35 h1:ZrGca+X17lczqhuXVCxHbLyGszP8VHGh1eAQLxbplkY= -github.com/grafana/cog v0.0.35/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= +github.com/grafana/cog v0.0.36 h1:5kijtkHRzabhCG+ck4sCAEtYp2luaBK8v1CrUHcQ0Es= +github.com/grafana/cog v0.0.36/go.mod h1:UDstzYqMdgIROmbfkHL8fB9XWQO2lnf5z+4W/eJo4Dc= github.com/grafana/cuetsy v0.1.11 h1:I3IwBhF+UaQxRM79HnImtrAn8REGdb5M3+C4QrYHoWk= github.com/grafana/cuetsy v0.1.11/go.mod h1:Ix97+CPD8ws9oSSxR3/Lf4ahU1I4Np83kjJmDVnLZvc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/pkg/plugins/config/config.go b/pkg/plugins/config/config.go index d3a5392be4a..8a7be65e3bc 100644 --- a/pkg/plugins/config/config.go +++ b/pkg/plugins/config/config.go @@ -35,6 +35,9 @@ type Features struct { SriChecksEnabled bool PluginsCDNSyncLoaderEnabled bool LocalizationForPlugins bool + // Needed only until Tempo Alerting / metrics TraceQL is stable + // https://github.com/grafana/grafana/issues/106888 + TempoAlertingEnabled bool } // NewPluginManagementCfg returns a new PluginManagementCfg. diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index b15f0dd867b..7802040999c 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -46,10 +46,6 @@ func DefaultService(cfg *config.PluginManagementCfg) *Service { // Base returns the base path for the specified plugin. func (s *Service) Base(n PluginInfo) (string, error) { - if n.class == plugins.ClassCore { - baseDir := getBaseDir(n.fs.Base()) - return path.Join("public/app/plugins", string(n.pluginJSON.Type), baseDir), nil - } if n.class == plugins.ClassCDN { return n.fs.Base(), nil } @@ -64,7 +60,6 @@ func (s *Service) Base(n PluginInfo) (string, error) { if s.cdn.PluginSupported(n.parent.pluginJSON.ID) { return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, relPath) } - return path.Join("public/plugins", n.parent.pluginJSON.ID, relPath), nil } return path.Join("public/plugins", n.pluginJSON.ID), nil @@ -73,32 +68,12 @@ func (s *Service) Base(n PluginInfo) (string, error) { // Module returns the module.js path for the specified plugin. func (s *Service) Module(n PluginInfo) (string, error) { if n.class == plugins.ClassCore { - if filepath.Base(n.fs.Base()) == "dist" { - // The core plugin has been built externally, use the module from the dist folder - } else { - baseDir := getBaseDir(n.fs.Base()) - return path.Join("core:plugin", baseDir), nil + if filepath.Base(n.fs.Base()) != "dist" { + return path.Join("core:plugin", filepath.Base(n.fs.Base())), nil } } - if n.class == plugins.ClassCDN { - return pluginscdn.JoinPath(n.fs.Base(), "module.js") - } - if s.cdn.PluginSupported(n.pluginJSON.ID) { - return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "module.js") - } - if n.parent != nil { - relPath, err := n.parent.fs.Rel(n.fs.Base()) - if err != nil { - return "", err - } - if s.cdn.PluginSupported(n.parent.pluginJSON.ID) { - return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, "module.js")) - } - return path.Join("public/plugins", n.parent.pluginJSON.ID, relPath, "module.js"), nil - } - - return path.Join("public/plugins", n.pluginJSON.ID, "module.js"), nil + return s.RelativeURL(n, "module.js") } // RelativeURL returns the relative URL for an arbitrary plugin asset. @@ -145,15 +120,6 @@ func (s *Service) DefaultLogoPath(pluginType plugins.Type) string { return path.Join("public/img", fmt.Sprintf("icn-%s.svg", string(pluginType))) } -func getBaseDir(pluginDir string) string { - baseDir := filepath.Base(pluginDir) - // Decoupled core plugins will be suffixed with "dist" if they have been built - if baseDir == "dist" { - return filepath.Base(strings.TrimSuffix(pluginDir, baseDir)) - } - return baseDir -} - func (s *Service) GetTranslations(n PluginInfo) (map[string]string, error) { pathToTranslations, err := s.RelativeURL(n, "locales") if err != nil { diff --git a/pkg/plugins/manager/loader/assetpath/assetpath_test.go b/pkg/plugins/manager/loader/assetpath/assetpath_test.go index e9cf4638298..424060471a5 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath_test.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath_test.go @@ -78,7 +78,7 @@ func TestService(t *testing.T) { base, err = svc.Base(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS, nil)) require.NoError(t, err) - require.Equal(t, "public/app/plugins/table-old", base) + require.Equal(t, "public/plugins/table-old", base) parentFS := pluginFS(oneCDNURL) parentFS.RelFunc = func(_ string) (string, error) { @@ -243,9 +243,9 @@ func TestService_ChildPlugins(t *testing.T) { return childInfo }, expected: expected{ - module: "public/plugins/parent/child/module.js", - baseURL: "public/plugins/parent/child", - relURL: "public/plugins/parent/child/path/to/file.txt", + module: "public/plugins/child/module.js", + baseURL: "public/plugins/child", + relURL: "public/plugins/child/path/to/file.txt", }, }, { @@ -256,8 +256,8 @@ func TestService_ChildPlugins(t *testing.T) { }, expected: expected{ module: "core:plugin/parent", - baseURL: "public/app/plugins/parent", - relURL: "public/app/plugins/parent/path/to/file.txt", + baseURL: "public/plugins/parent", + relURL: "public/plugins/parent/path/to/file.txt", }, }, { @@ -268,8 +268,8 @@ func TestService_ChildPlugins(t *testing.T) { }, expected: expected{ module: "public/plugins/parent/module.js", - baseURL: "public/app/plugins/parent", - relURL: "public/app/plugins/parent/path/to/file.txt", + baseURL: "public/plugins/parent", + relURL: "public/plugins/parent/path/to/file.txt", }, }, { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index c51b200074c..dcd9a536baa 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -85,8 +85,8 @@ func TestLoader_Load(t *testing.T) { Description: "Data source for Amazon AWS monitoring service", Keywords: []string{"aws", "amazon"}, Logos: plugins.Logos{ - Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Small: "public/plugins/cloudwatch/img/amazon-web-services.png", + Large: "public/plugins/cloudwatch/img/amazon-web-services.png", }, Links: []plugins.InfoLink{ {Name: "Raise issue", URL: "https://github.com/grafana/grafana/issues/new"}, @@ -123,7 +123,7 @@ func TestLoader_Load(t *testing.T) { QueryOptions: map[string]bool{"minInterval": true}, }, Module: "core:plugin/cloudwatch", - BaseURL: "public/app/plugins/datasource/cloudwatch", + BaseURL: "public/plugins/cloudwatch", FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureStatusInternal, Class: plugins.ClassCore, diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 965760bab50..6e118dc4b13 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -126,8 +126,9 @@ type AddedComponent struct { } type AddedFunction struct { - Targets []string `json:"targets"` - Title string `json:"title"` + Targets []string `json:"targets"` + Title string `json:"title"` + Description string `json:"description"` } type ExposedComponent struct { diff --git a/pkg/promlib/converter/prom.go b/pkg/promlib/converter/prom.go index 3fb5edb50c6..a341ecf72ca 100644 --- a/pkg/promlib/converter/prom.go +++ b/pkg/promlib/converter/prom.go @@ -1140,8 +1140,8 @@ streamField: return parsedLabelsMap, structuredMetadataMap, nil } -func resultTypeToCustomMeta(resultType string) map[string]string { - return map[string]string{"resultType": resultType} +func resultTypeToCustomMeta(resultType string) map[string]any { + return map[string]any{"resultType": resultType} } func timeFromFloat(fv float64) time.Time { diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index afcd65eb147..84813438862 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -13,7 +13,7 @@ require ( go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/otel/trace v1.36.0 google.golang.org/protobuf v1.36.6 - k8s.io/apimachinery v0.33.1 + k8s.io/apimachinery v0.33.2 ) require ( @@ -121,14 +121,14 @@ require ( golang.org/x/text v0.26.0 // indirect golang.org/x/tools v0.34.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.233.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/api v0.235.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.33.1 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 7c31bdb56ee..b114304cebd 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -3,8 +3,8 @@ cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= @@ -126,8 +126,8 @@ 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/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -407,12 +407,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI= -google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= @@ -430,10 +430,10 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= diff --git a/pkg/promlib/models/result.go b/pkg/promlib/models/result.go index 3cefc7fb3d6..dd6bafa8027 100644 --- a/pkg/promlib/models/result.go +++ b/pkg/promlib/models/result.go @@ -20,7 +20,7 @@ func ResultTypeFromFrame(frame *data.Frame) ResultType { if frame.Meta.Custom == nil { return ResultTypeUnknown } - custom, ok := frame.Meta.Custom.(map[string]string) + custom, ok := frame.Meta.Custom.(map[string]any) if !ok { return ResultTypeUnknown } diff --git a/pkg/promlib/querydata/response.go b/pkg/promlib/querydata/response.go index f9253e5ccba..808d88e9423 100644 --- a/pkg/promlib/querydata/response.go +++ b/pkg/promlib/querydata/response.go @@ -54,6 +54,14 @@ func (s *QueryData) parseResponse(ctx context.Context, q *models.Query, res *htt addMetadataToMultiFrame(q, frame) if i == 0 { frame.Meta.ExecutedQueryString = executedQueryString(q) + if frame.Meta.Custom == nil { + frame.Meta.Custom = make(map[string]any) + } + if custom, ok := frame.Meta.Custom.(map[string]any); ok { + // This is required for incremental querying feature + // Knowing the calculated minStep is required for merging and caching the frames on frontend side + custom["calculatedMinStep"] = q.Step.Milliseconds() + } } } diff --git a/pkg/promlib/testdata/range_auto.result.golden.jsonc b/pkg/promlib/testdata/range_auto.result.golden.jsonc index 9bb659c7679..ad04ad76303 100644 --- a/pkg/promlib/testdata/range_auto.result.golden.jsonc +++ b/pkg/promlib/testdata/range_auto.result.golden.jsonc @@ -7,6 +7,7 @@ // 1 // ], // "custom": { +// "calculatedMinStep": 1000, // "resultType": "matrix" // }, // "executedQueryString": "Expr: histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[4s])) by (le))\nStep: 1s" @@ -44,6 +45,7 @@ 1 ], "custom": { + "calculatedMinStep": 1000, "resultType": "matrix" }, "executedQueryString": "Expr: histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[4s])) by (le))\nStep: 1s" diff --git a/pkg/promlib/testdata/range_infinity.result.golden.jsonc b/pkg/promlib/testdata/range_infinity.result.golden.jsonc index db0f3edfbce..91240b239c4 100644 --- a/pkg/promlib/testdata/range_infinity.result.golden.jsonc +++ b/pkg/promlib/testdata/range_infinity.result.golden.jsonc @@ -7,6 +7,7 @@ // 1 // ], // "custom": { +// "calculatedMinStep": 1000, // "resultType": "matrix" // }, // "executedQueryString": "Expr: 1 / 0\nStep: 1s" @@ -37,6 +38,7 @@ 1 ], "custom": { + "calculatedMinStep": 1000, "resultType": "matrix" }, "executedQueryString": "Expr: 1 / 0\nStep: 1s" diff --git a/pkg/promlib/testdata/range_missing.result.golden.jsonc b/pkg/promlib/testdata/range_missing.result.golden.jsonc index 4272beb9993..f9cc45aed1a 100644 --- a/pkg/promlib/testdata/range_missing.result.golden.jsonc +++ b/pkg/promlib/testdata/range_missing.result.golden.jsonc @@ -7,6 +7,7 @@ // 1 // ], // "custom": { +// "calculatedMinStep": 1000, // "resultType": "matrix" // }, // "executedQueryString": "Expr: test1\nStep: 1s" @@ -37,6 +38,7 @@ 1 ], "custom": { + "calculatedMinStep": 1000, "resultType": "matrix" }, "executedQueryString": "Expr: test1\nStep: 1s" diff --git a/pkg/promlib/testdata/range_nan.result.golden.jsonc b/pkg/promlib/testdata/range_nan.result.golden.jsonc index 9da1ecfeca7..1a9ae359b61 100644 --- a/pkg/promlib/testdata/range_nan.result.golden.jsonc +++ b/pkg/promlib/testdata/range_nan.result.golden.jsonc @@ -7,6 +7,7 @@ // 1 // ], // "custom": { +// "calculatedMinStep": 1000, // "resultType": "matrix" // }, // "executedQueryString": "Expr: \nStep: 1s" @@ -37,6 +38,7 @@ 1 ], "custom": { + "calculatedMinStep": 1000, "resultType": "matrix" }, "executedQueryString": "Expr: \nStep: 1s" diff --git a/pkg/promlib/testdata/range_simple.result.golden.jsonc b/pkg/promlib/testdata/range_simple.result.golden.jsonc index a0fe631e2e3..3f68c6c818a 100644 --- a/pkg/promlib/testdata/range_simple.result.golden.jsonc +++ b/pkg/promlib/testdata/range_simple.result.golden.jsonc @@ -7,6 +7,7 @@ // 1 // ], // "custom": { +// "calculatedMinStep": 1000, // "resultType": "matrix" // }, // "executedQueryString": "Expr: \nStep: 1s" @@ -60,6 +61,7 @@ 1 ], "custom": { + "calculatedMinStep": 1000, "resultType": "matrix" }, "executedQueryString": "Expr: \nStep: 1s" diff --git a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go b/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go index 18796640c96..bb7231e86f6 100644 --- a/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go +++ b/pkg/registry/apis/dashboard/legacy/legacy_migrator_mock.go @@ -1,12 +1,11 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package legacy import ( context "context" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" mock "github.com/stretchr/testify/mock" ) diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 21c83690c8a..1c14928daeb 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -489,10 +489,10 @@ type panel struct { FolderUID sql.NullString Created time.Time - CreatedBy string + CreatedBy sql.NullString Updated time.Time - UpdatedBy string + UpdatedBy sql.NullString Version int64 @@ -633,13 +633,13 @@ func parseLibraryPanelRow(p panel) (dashboardV0.LibraryPanel, error) { if p.FolderUID.Valid { meta.SetFolder(p.FolderUID.String) } - meta.SetCreatedBy(p.CreatedBy) + meta.SetCreatedBy(getUserID(p.CreatedBy, sql.NullInt64{})) meta.SetGeneration(p.Version) meta.SetDeprecatedInternalID(p.ID) //nolint:staticcheck // Only set updated metadata if it is different - if p.UpdatedBy != p.CreatedBy || p.Updated.Sub(p.Created) > time.Second { - meta.SetUpdatedBy(p.UpdatedBy) + if p.UpdatedBy.Valid && p.Updated.Sub(p.Created) > time.Second { + meta.SetUpdatedBy(getUserID(p.UpdatedBy, sql.NullInt64{})) meta.SetUpdatedTimestamp(&p.Updated) } diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index 3c96a843467..626d022d39d 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -230,9 +230,9 @@ func TestParseLibraryPanelRow(t *testing.T) { UID: "panel-uid", FolderUID: sql.NullString{String: "folder-uid", Valid: true}, Created: time.Now(), - CreatedBy: "creator", + CreatedBy: sql.NullString{String: "creator", Valid: true}, Updated: time.Now(), - UpdatedBy: "updator", + UpdatedBy: sql.NullString{String: "updator", Valid: true}, Version: 2, Type: "graph", Description: "desc from db", @@ -261,6 +261,8 @@ func TestParseLibraryPanelRow(t *testing.T) { t.Run("metadata is set correctly", func(t *testing.T) { p := basePanel p.Name = "Test Panel" + // Ensure there's a time difference greater than 1 second to trigger updated metadata + p.Updated = p.Created.Add(2 * time.Second) model := map[string]interface{}{ "title": "Test Panel", "type": "graph", @@ -278,8 +280,8 @@ func TestParseLibraryPanelRow(t *testing.T) { require.Equal(t, p.ID, meta.GetDeprecatedInternalID()) // nolint:staticcheck require.Equal(t, p.Version, meta.GetGeneration()) require.Equal(t, p.FolderUID.String, meta.GetFolder()) - require.Equal(t, p.CreatedBy, meta.GetCreatedBy()) - require.Equal(t, p.UpdatedBy, meta.GetUpdatedBy()) + require.Equal(t, "user:creator", meta.GetCreatedBy()) + require.Equal(t, "user:updator", meta.GetUpdatedBy()) }) t.Run("panel title in dashboard vs library panel title is set correctly", func(t *testing.T) { @@ -300,4 +302,66 @@ func TestParseLibraryPanelRow(t *testing.T) { require.Equal(t, "Model Title", item.Spec.PanelTitle) require.Equal(t, "Database Name", item.Spec.Title) }) + + t.Run("handles NULL created_by and updated_by fields", func(t *testing.T) { + p := basePanel + p.Name = "Test Panel" + // Set CreatedBy and UpdatedBy to NULL (Invalid) + p.CreatedBy = sql.NullString{String: "", Valid: false} + p.UpdatedBy = sql.NullString{String: "", Valid: false} + model := map[string]interface{}{ + "title": "Test Panel", + "type": "graph", + "description": "desc from db", + } + modelBytes, err := json.Marshal(model) + require.NoError(t, err) + p.Model = modelBytes + + item, err := parseLibraryPanelRow(p) + require.NoError(t, err) + + meta, err := utils.MetaAccessor(&item) + require.NoError(t, err) + + // When CreatedBy is NULL, it should be set to empty string + require.Equal(t, "", meta.GetCreatedBy()) + + // When UpdatedBy is NULL and not valid, updated metadata should not be set + require.Equal(t, "", meta.GetUpdatedBy()) + // The updated timestamp should not be set when UpdatedBy is invalid + updatedTimestamp, err := meta.GetUpdatedTimestamp() + require.NoError(t, err) + require.Nil(t, updatedTimestamp) + }) + + t.Run("handles valid created_by but NULL updated_by", func(t *testing.T) { + p := basePanel + p.Name = "Test Panel" + p.CreatedBy = sql.NullString{String: "creator", Valid: true} + p.UpdatedBy = sql.NullString{String: "", Valid: false} + // Make sure there's a time difference to trigger the update logic + p.Updated = p.Created.Add(10 * time.Second) + model := map[string]interface{}{ + "title": "Test Panel", + "type": "graph", + "description": "desc from db", + } + modelBytes, err := json.Marshal(model) + require.NoError(t, err) + p.Model = modelBytes + + item, err := parseLibraryPanelRow(p) + require.NoError(t, err) + + meta, err := utils.MetaAccessor(&item) + require.NoError(t, err) + + require.Equal(t, "user:creator", meta.GetCreatedBy()) + // Even with time difference, if UpdatedBy is not valid, updated metadata should not be set + require.Equal(t, "", meta.GetUpdatedBy()) + updatedTimestamp, err := meta.GetUpdatedTimestamp() + require.NoError(t, err) + require.Nil(t, updatedTimestamp) + }) } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 6ca9b87fbc4..f2fda01d1c9 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -23,7 +23,8 @@ import ( internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/conversion" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -158,16 +159,18 @@ func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion { if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) { // If dashboards v2 is enabled, we want to use v2alpha1 as the default API version. return []schema.GroupVersion{ - dashv2.DashboardResourceInfo.GroupVersion(), + dashv2alpha1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), dashv1.DashboardResourceInfo.GroupVersion(), + dashv2alpha2.DashboardResourceInfo.GroupVersion(), } } return []schema.GroupVersion{ dashv1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), - dashv2.DashboardResourceInfo.GroupVersion(), + dashv2alpha1.DashboardResourceInfo.GroupVersion(), + dashv2alpha2.DashboardResourceInfo.GroupVersion(), } } @@ -179,7 +182,11 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { if err := dashv1.AddToScheme(scheme); err != nil { return err } - if err := dashv2.AddToScheme(scheme); err != nil { + if err := dashv2alpha1.AddToScheme(scheme); err != nil { + return err + } + + if err := dashv2alpha2.AddToScheme(scheme); err != nil { return err } @@ -391,7 +398,10 @@ func getDashboardProperties(obj runtime.Object) (string, string, error) { case *dashv1.Dashboard: title = d.Spec.GetNestedString(dashboardSpecTitle) refresh = d.Spec.GetNestedString(dashboardSpecRefreshInterval) - case *dashv2.Dashboard: + case *dashv2alpha1.Dashboard: + title = d.Spec.Title + refresh = d.Spec.TimeSettings.AutoRefresh + case *dashv2alpha2.Dashboard: title = d.Spec.Title refresh = d.Spec.TimeSettings.AutoRefresh default: @@ -456,11 +466,28 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver // v2alpha1 if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, - dashv2.DashboardResourceInfo, + dashv2alpha1.DashboardResourceInfo, nil, // do not register library panel func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { - dto := &dashv2.DashboardWithAccessInfo{} - dash, ok := obj.(*dashv2.Dashboard) + dto := &dashv2alpha1.DashboardWithAccessInfo{} + dash, ok := obj.(*dashv2alpha1.Dashboard) + if ok { + dto.Dashboard = *dash + } + if access != nil { + err = b.scheme.Convert(access, &dto.Access, nil) + } + return dto, err + }); err != nil { + return err + } + + if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, + dashv2alpha2.DashboardResourceInfo, + nil, // do not register library panel + func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { + dto := &dashv2alpha2.DashboardWithAccessInfo{} + dash, ok := obj.(*dashv2alpha2.Dashboard) if ok { dto.Dashboard = *dash } @@ -544,7 +571,8 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { defs := dashv0.GetOpenAPIDefinitions(ref) maps.Copy(defs, dashv1.GetOpenAPIDefinitions(ref)) - maps.Copy(defs, dashv2.GetOpenAPIDefinitions(ref)) + maps.Copy(defs, dashv2alpha1.GetOpenAPIDefinitions(ref)) + maps.Copy(defs, dashv2alpha2.GetOpenAPIDefinitions(ref)) return defs } } diff --git a/pkg/registry/apis/dashboard/register_test.go b/pkg/registry/apis/dashboard/register_test.go index 58ebb470cd9..2bccf5a1453 100644 --- a/pkg/registry/apis/dashboard/register_test.go +++ b/pkg/registry/apis/dashboard/register_test.go @@ -13,7 +13,8 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -176,7 +177,8 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { expected: []schema.GroupVersion{ dashv1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), - dashv2.DashboardResourceInfo.GroupVersion(), + dashv2alpha1.DashboardResourceInfo.GroupVersion(), + dashv2alpha2.DashboardResourceInfo.GroupVersion(), }, }, { @@ -187,7 +189,8 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { expected: []schema.GroupVersion{ dashv1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), - dashv2.DashboardResourceInfo.GroupVersion(), + dashv2alpha1.DashboardResourceInfo.GroupVersion(), + dashv2alpha2.DashboardResourceInfo.GroupVersion(), }, }, { @@ -196,9 +199,10 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { featuremgmt.FlagDashboardNewLayouts, }, expected: []schema.GroupVersion{ - dashv2.DashboardResourceInfo.GroupVersion(), + dashv2alpha1.DashboardResourceInfo.GroupVersion(), dashv0.DashboardResourceInfo.GroupVersion(), dashv1.DashboardResourceInfo.GroupVersion(), + dashv2alpha2.DashboardResourceInfo.GroupVersion(), }, }, } diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index bf743893816..e0f748fa54b 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -57,7 +57,7 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) }, Spec: folders.FolderSpec{ Title: v.Title, - Description: descr(v.Description), + Description: &v.Description, }, } @@ -94,10 +94,3 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) f.UID = gapiutil.CalculateClusterWideUID(f) return f, nil } - -func descr(str string) *string { - if str == "" { - return nil - } - return &str -} diff --git a/pkg/registry/apis/folders/folder_storage.go b/pkg/registry/apis/folders/folder_storage.go index 588681e0311..2f3ab79873e 100644 --- a/pkg/registry/apis/folders/folder_storage.go +++ b/pkg/registry/apis/folders/folder_storage.go @@ -11,6 +11,7 @@ import ( claims "github.com/grafana/authlib/types" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" @@ -79,7 +80,8 @@ func (s *folderStorage) Create(ctx context.Context, ) (runtime.Object, error) { obj, err := s.store.Create(ctx, obj, createValidation, options) if err != nil { - return nil, err + statusErr := apierrors.ToFolderStatusError(err) + return nil, &statusErr } info, err := request.NamespaceInfoFrom(ctx, true) diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index d3b21177afc..397cbcb94a5 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util" ) var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) @@ -236,7 +237,7 @@ func (b *FolderAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, _ if !ok { return fmt.Errorf("obj is not folders.Folder") } - f.Spec.Title = strings.Trim(f.Spec.Title, "") + f.Spec.Title = strings.Trim(f.Spec.Title, " ") return nil } return nil @@ -302,6 +303,14 @@ func (b *FolderAPIBuilder) validateOnCreate(ctx context.Context, id string, obj } } + if !util.IsValidShortUID(id) { + return dashboards.ErrDashboardInvalidUid + } + + if util.IsShortUIDTooLong(id) { + return dashboards.ErrDashboardUidTooLong + } + f, ok := obj.(*folders.Folder) if !ok { return fmt.Errorf("obj is not folders.Folder") diff --git a/pkg/registry/apis/folders/register_test.go b/pkg/registry/apis/folders/register_test.go index d693bd53106..f4ab009ec10 100644 --- a/pkg/registry/apis/folders/register_test.go +++ b/pkg/registry/apis/folders/register_test.go @@ -479,7 +479,7 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := b.Validate(context.Background(), admission.NewAttributesRecord( + admAttr := admission.NewAttributesRecord( tt.input, nil, folders.SchemeGroupVersion.WithKind("folder"), @@ -491,13 +491,18 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { nil, true, &user.SignedInUser{}, - ), nil) + ) + err := b.Validate(context.Background(), admAttr, nil) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + + err = b.Mutate(context.Background(), admAttr, nil) + require.NoError(t, err) + require.Equal(t, tt.input, tt.expected) }) } } @@ -585,7 +590,7 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := b.Validate(context.Background(), admission.NewAttributesRecord( + admAttr := admission.NewAttributesRecord( tt.input, existingObj, folders.SchemeGroupVersion.WithKind("folder"), @@ -597,13 +602,18 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { nil, true, &user.SignedInUser{}, - ), nil) + ) + err := b.Validate(context.Background(), admAttr, nil) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) + + err = b.Mutate(context.Background(), admAttr, nil) + require.NoError(t, err) + require.Equal(t, tt.input, tt.expected) }) } } diff --git a/pkg/registry/apis/iam/common/common.go b/pkg/registry/apis/iam/common/common.go index a93c90a7d36..ff11de13799 100644 --- a/pkg/registry/apis/iam/common/common.go +++ b/pkg/registry/apis/iam/common/common.go @@ -5,8 +5,9 @@ import ( "strconv" authlib "github.com/grafana/authlib/types" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/team" ) @@ -20,11 +21,19 @@ func OptionalFormatInt(num int64) string { return "" } -func MapTeamPermission(p team.PermissionType) iamv0.TeamPermission { +func MapTeamPermission(p team.PermissionType) iamv0alpha1.TeamBindingTeamPermission { if p == team.PermissionTypeAdmin { - return iamv0.TeamPermissionAdmin + return iamv0alpha1.TeamBindingTeamPermissionAdmin } else { - return iamv0.TeamPermissionMember + return iamv0alpha1.TeamBindingTeamPermissionMember + } +} + +func MapUserTeamPermission(p team.PermissionType) legacyiamv0.TeamPermission { + if p == team.PermissionTypeAdmin { + return legacyiamv0.TeamPermissionAdmin + } else { + return legacyiamv0.TeamPermissionMember } } diff --git a/pkg/registry/apis/iam/legacy/display_query.sql b/pkg/registry/apis/iam/legacy/display_query.sql index c6364f0afa5..15dfff35d09 100644 --- a/pkg/registry/apis/iam/legacy/display_query.sql +++ b/pkg/registry/apis/iam/legacy/display_query.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM {{ .Ident .UserTable }} as u JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id WHERE o.org_id = {{ .Arg .Query.OrgID }} AND ( 1=2 {{ if .Query.UIDs }} diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql index 8346f0a8a7d..66f977fe3db 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR u.id IN (1, 2) diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql index a93f7b2e662..933d45adbee 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql index d9727e67feb..a1c7c11dcad 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql index 4ac1c74d87f..399aee41f46 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_1.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql index 5bcc865f1d9..63b9fc610ab 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_page_2.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql index 2c36c9f9fbb..bf2a9b7ccb5 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--users_query-users_uid.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql index d02afa8cf0a..d3cb1fb1e8c 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR u.id IN (1, 2) diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql index e8a519e465b..dfb4ed5afe2 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql index 2af78d4dfe6..a6989bb94e0 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql index 7de4914aa8d..4e771e18f26 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_1.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql index e938a14392b..f6e2d6dab11 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_page_2.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql index 5c8f1f2ec66..a43df88f8a8 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--users_query-users_uid.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql index d02afa8cf0a..d3cb1fb1e8c 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR u.id IN (1, 2) diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql index e8a519e465b..dfb4ed5afe2 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql index 2af78d4dfe6..a6989bb94e0 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql index 7de4914aa8d..4e771e18f26 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_1.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql index e938a14392b..f6e2d6dab11 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_page_2.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql index 5c8f1f2ec66..a43df88f8a8 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--users_query-users_uid.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 0 AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/legacy/user.go b/pkg/registry/apis/iam/legacy/user.go index 0319abc1520..24d3ab71b5e 100644 --- a/pkg/registry/apis/iam/legacy/user.go +++ b/pkg/registry/apis/iam/legacy/user.go @@ -165,7 +165,8 @@ func (s *legacySQLStore) queryUsers(ctx context.Context, sql *legacysql.LegacyDa for rows.Next() { u := user.User{} err = rows.Scan(&u.OrgID, &u.ID, &u.UID, &u.Login, &u.Email, &u.Name, - &u.Created, &u.Updated, &u.IsServiceAccount, &u.IsDisabled, &u.IsAdmin, + &u.Created, &u.Updated, &u.IsServiceAccount, &u.IsDisabled, &u.IsAdmin, &u.EmailVerified, + &u.IsProvisioned, &u.LastSeenAt, ) if err != nil { return res, err diff --git a/pkg/registry/apis/iam/legacy/users_query.sql b/pkg/registry/apis/iam/legacy/users_query.sql index 59d46d39400..5c66dbd9c54 100644 --- a/pkg/registry/apis/iam/legacy/users_query.sql +++ b/pkg/registry/apis/iam/legacy/users_query.sql @@ -1,5 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, - u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin + u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, + u.email_verified, u.is_provisioned, u.last_seen_at FROM {{ .Ident .UserTable }} as u JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id WHERE o.org_id = {{ .Arg .Query.OrgID }} AND NOT u.is_service_account diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 7e3a54f9ee1..7434577e69f 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -2,6 +2,7 @@ package iam import ( "context" + "maps" "strings" "github.com/prometheus/client_golang/prometheus" @@ -149,18 +150,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } func (b *IdentityAccessManagementAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - defs := legacyiamv0.GetOpenAPIDefinitions - if b.enableAuthZApis { - defs = func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - def1 := legacyiamv0.GetOpenAPIDefinitions(ref) - def2 := iamv0.GetOpenAPIDefinitions(ref) - for k, v := range def2 { - def1[k] = v - } - return def1 - } + return func(rc common.ReferenceCallback) map[string]common.OpenAPIDefinition { + dst := legacyiamv0.GetOpenAPIDefinitions(rc) + maps.Copy(dst, iamv0.GetOpenAPIDefinitions(rc)) + + return dst } - return defs } func (b *IdentityAccessManagementAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { diff --git a/pkg/registry/apis/iam/serviceaccount/rest_token.go b/pkg/registry/apis/iam/serviceaccount/rest_token.go index 49b5a1383c2..1cffc74f74c 100644 --- a/pkg/registry/apis/iam/serviceaccount/rest_token.go +++ b/pkg/registry/apis/iam/serviceaccount/rest_token.go @@ -9,7 +9,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + claims "github.com/grafana/authlib/types" + legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -32,7 +33,7 @@ type LegacyTokenRest struct { // New implements rest.Storage. func (s *LegacyTokenRest) New() runtime.Object { - return &iamv0.UserTeamList{} + return &legacyiamv0.ServiceAccountTokenList{} } // Destroy implements rest.Storage. @@ -70,10 +71,10 @@ func (s *LegacyTokenRest) Connect(ctx context.Context, name string, options runt return } - list := &iamv0.ServiceAccountTokenList{Items: make([]iamv0.ServiceAccountToken, 0, len(res.Items))} + list := &legacyiamv0.ServiceAccountTokenList{Items: make([]legacyiamv0.ServiceAccountToken, 0, len(res.Items))} for _, t := range res.Items { - list.Items = append(list.Items, mapToToken(t)) + list.Items = append(list.Items, mapToToken(t, ns)) } list.Continue = common.OptionalFormatInt(res.Continue) @@ -92,7 +93,7 @@ func (s *LegacyTokenRest) ConnectMethods() []string { return []string{http.MethodGet} } -func mapToToken(t legacy.ServiceAccountToken) iamv0.ServiceAccountToken { +func mapToToken(t legacy.ServiceAccountToken, ns claims.NamespaceInfo) legacyiamv0.ServiceAccountToken { var expires, lastUsed *metav1.Time if t.Expires != nil { @@ -105,7 +106,7 @@ func mapToToken(t legacy.ServiceAccountToken) iamv0.ServiceAccountToken { lastUsed = &ts } - return iamv0.ServiceAccountToken{ + return legacyiamv0.ServiceAccountToken{ Name: t.Name, Expires: expires, LastUsed: lastUsed, diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 7e8d6b18040..02e797cec95 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -10,6 +10,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" claims "github.com/grafana/authlib/types" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -61,7 +62,7 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { res, err := common.List( ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), - func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.ServiceAccount], error) { + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) { found, err := s.store.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{ Pagination: p, }) @@ -70,12 +71,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt return nil, err } - items := make([]iamv0.ServiceAccount, 0, len(found.Items)) + items := make([]iamv0alpha1.ServiceAccount, 0, len(found.Items)) for _, sa := range found.Items { items = append(items, toSAItem(sa, ns.Value)) } - return &common.ListResponse[iamv0.ServiceAccount]{ + return &common.ListResponse[iamv0alpha1.ServiceAccount]{ Items: items, RV: found.RV, Continue: found.Continue, @@ -87,21 +88,21 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt return nil, err } - obj := &iamv0.ServiceAccountList{Items: res.Items} + obj := &iamv0alpha1.ServiceAccountList{Items: res.Items} obj.Continue = common.OptionalFormatInt(res.Continue) obj.ResourceVersion = common.OptionalFormatInt(res.RV) return obj, nil } -func toSAItem(sa legacy.ServiceAccount, ns string) iamv0.ServiceAccount { - item := iamv0.ServiceAccount{ +func toSAItem(sa legacy.ServiceAccount, ns string) iamv0alpha1.ServiceAccount { + item := iamv0alpha1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: sa.UID, Namespace: ns, ResourceVersion: fmt.Sprintf("%d", sa.Updated.UnixMilli()), CreationTimestamp: metav1.NewTime(sa.Created), }, - Spec: iamv0.ServiceAccountSpec{ + Spec: iamv0alpha1.ServiceAccountSpec{ Title: sa.Name, Disabled: sa.Disabled, }, diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index 0db2b96b981..5da6b0c8099 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -11,6 +11,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" claims "github.com/grafana/authlib/types" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -64,7 +65,7 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { res, err := common.List( ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), - func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.Team], error) { + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) { found, err := s.store.ListTeams(ctx, ns, legacy.ListTeamQuery{ Pagination: p, }) @@ -73,12 +74,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt return nil, err } - teams := make([]iamv0.Team, 0, len(found.Teams)) + teams := make([]iamv0alpha1.Team, 0, len(found.Teams)) for _, t := range found.Teams { teams = append(teams, toTeamObject(t, ns)) } - return &common.ListResponse[iamv0.Team]{ + return &common.ListResponse[iamv0alpha1.Team]{ Items: teams, RV: found.RV, Continue: found.Continue, @@ -90,7 +91,7 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt return nil, fmt.Errorf("failed to list teams: %w", err) } - list := &iamv0.TeamList{Items: res.Items} + list := &iamv0alpha1.TeamList{Items: res.Items} list.Continue = common.OptionalFormatInt(res.Continue) list.ResourceVersion = common.OptionalFormatInt(res.RV) @@ -119,18 +120,17 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO return &obj, nil } -func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0.Team { - obj := iamv0.Team{ +func toTeamObject(t team.Team, ns claims.NamespaceInfo) iamv0alpha1.Team { + obj := iamv0alpha1.Team{ ObjectMeta: metav1.ObjectMeta{ Name: t.UID, Namespace: ns.Value, CreationTimestamp: metav1.NewTime(t.Created), ResourceVersion: strconv.FormatInt(t.Updated.UnixMilli(), 10), }, - Spec: iamv0.TeamSpec{ - Title: t.Name, - Email: t.Email, - InternalID: t.ID, + Spec: iamv0alpha1.TeamSpec{ + Title: t.Name, + Email: t.Email, }, } meta, _ := utils.MetaAccessor(&obj) diff --git a/pkg/registry/apis/iam/team/store_binding.go b/pkg/registry/apis/iam/team/store_binding.go index 03e25a393fe..e11bbec15c0 100644 --- a/pkg/registry/apis/iam/team/store_binding.go +++ b/pkg/registry/apis/iam/team/store_binding.go @@ -11,6 +11,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" claims "github.com/grafana/authlib/types" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" @@ -102,8 +103,8 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. return nil, err } - list := iamv0.TeamBindingList{ - Items: make([]iamv0.TeamBinding, 0, len(res.Bindings)), + list := iamv0alpha1.TeamBindingList{ + Items: make([]iamv0alpha1.TeamBinding, 0, len(res.Bindings)), } for _, b := range res.Bindings { @@ -116,7 +117,7 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. return &list, nil } -func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0.TeamBinding { +func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0alpha1.TeamBinding { rv := time.Time{} ct := time.Now() @@ -129,15 +130,15 @@ func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0.Tea } } - return iamv0.TeamBinding{ + return iamv0alpha1.TeamBinding{ ObjectMeta: metav1.ObjectMeta{ Name: b.TeamUID, Namespace: ns.Value, ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10), CreationTimestamp: metav1.NewTime(ct), }, - Spec: iamv0.TeamBindingSpec{ - Team: iamv0.TeamRef{ + Spec: iamv0alpha1.TeamBindingSpec{ + TeamRef: iamv0alpha1.TeamBindingTeamRef{ Name: b.TeamUID, }, Subjects: mapToSubjects(b.Members), @@ -145,14 +146,11 @@ func mapToBindingObject(ns claims.NamespaceInfo, b legacy.TeamBinding) iamv0.Tea } } -func mapToSubjects(members []legacy.TeamMember) []iamv0.TeamSubject { - out := make([]iamv0.TeamSubject, 0, len(members)) +func mapToSubjects(members []legacy.TeamMember) []iamv0alpha1.TeamBindingspecSubject { + out := make([]iamv0alpha1.TeamBindingspecSubject, 0, len(members)) for _, m := range members { - out = append(out, iamv0.TeamSubject{ - Identity: iamv0.IdentityRef{ - Type: claims.TypeUser, - Name: m.UserUID, - }, + out = append(out, iamv0alpha1.TeamBindingspecSubject{ + Name: m.UserUID, Permission: common.MapTeamPermission(m.Permission), }) } diff --git a/pkg/registry/apis/iam/user/rest_user_team.go b/pkg/registry/apis/iam/user/rest_user_team.go index f1e56bc177f..cc30081d0bb 100644 --- a/pkg/registry/apis/iam/user/rest_user_team.go +++ b/pkg/registry/apis/iam/user/rest_user_team.go @@ -7,7 +7,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -29,7 +29,7 @@ type LegacyUserTeamREST struct { // New implements rest.Storage. func (s *LegacyUserTeamREST) New() runtime.Object { - return &iamv0.UserTeamList{} + return &legacyiamv0.UserTeamList{} } // Destroy implements rest.Storage. @@ -62,7 +62,7 @@ func (s *LegacyUserTeamREST) Connect(ctx context.Context, name string, options r return } - list := &iamv0.UserTeamList{Items: make([]iamv0.UserTeam, 0, len(res.Items))} + list := &legacyiamv0.UserTeamList{Items: make([]legacyiamv0.UserTeam, 0, len(res.Items))} for _, m := range res.Items { list.Items = append(list.Items, mapToUserTeam(m)) @@ -84,12 +84,12 @@ func (s *LegacyUserTeamREST) ConnectMethods() []string { return []string{http.MethodGet} } -func mapToUserTeam(t legacy.UserTeam) iamv0.UserTeam { - return iamv0.UserTeam{ +func mapToUserTeam(t legacy.UserTeam) legacyiamv0.UserTeam { + return legacyiamv0.UserTeam{ Title: t.Name, - TeamRef: iamv0.TeamRef{ + TeamRef: legacyiamv0.TeamRef{ Name: t.UID, }, - Permission: common.MapTeamPermission(t.Permission), + Permission: common.MapUserTeamPermission(t.Permission), } } diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 115bbb8f8d2..e9c45178845 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -3,6 +3,7 @@ package user import ( "context" "fmt" + "time" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -10,6 +11,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" claims "github.com/grafana/authlib/types" + iamv0alpha "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -18,6 +20,8 @@ import ( "github.com/grafana/grafana/pkg/services/user" ) +const AnnoKeyLastSeenAt = "iam.grafana.app/lastSeenAt" + var ( _ rest.Scoper = (*LegacyStore)(nil) _ rest.SingularNameProvider = (*LegacyStore)(nil) @@ -62,7 +66,7 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { res, err := common.List( ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), - func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.User], error) { + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha.User], error) { found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ Pagination: p, }) @@ -71,12 +75,12 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt return nil, err } - users := make([]iamv0.User, 0, len(found.Users)) + users := make([]iamv0alpha.User, 0, len(found.Users)) for _, u := range found.Users { users = append(users, toUserItem(&u, ns.Value)) } - return &common.ListResponse[iamv0.User]{ + return &common.ListResponse[iamv0alpha.User]{ Items: users, RV: found.RV, Continue: found.Continue, @@ -88,7 +92,7 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt return nil, err } - obj := &iamv0.UserList{Items: res.Items} + obj := &iamv0alpha.UserList{Items: res.Items} obj.Continue = common.OptionalFormatInt(res.Continue) obj.ResourceVersion = common.OptionalFormatInt(res.RV) return obj, nil @@ -116,25 +120,35 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO return &obj, nil } -func toUserItem(u *user.User, ns string) iamv0.User { - item := &iamv0.User{ +func toUserItem(u *user.User, ns string) iamv0alpha.User { + item := &iamv0alpha.User{ ObjectMeta: metav1.ObjectMeta{ Name: u.UID, Namespace: ns, ResourceVersion: fmt.Sprintf("%d", u.Updated.UnixMilli()), CreationTimestamp: metav1.NewTime(u.Created), }, - Spec: iamv0.UserSpec{ + Spec: iamv0alpha.UserSpec{ Name: u.Name, Login: u.Login, Email: u.Email, EmailVerified: u.EmailVerified, Disabled: u.IsDisabled, - InternalID: u.ID, + GrafanaAdmin: u.IsAdmin, + Provisioned: u.IsProvisioned, }, } obj, _ := utils.MetaAccessor(item) obj.SetUpdatedTimestamp(&u.Updated) + obj.SetAnnotation(AnnoKeyLastSeenAt, formatTime(&u.LastSeenAt)) obj.SetDeprecatedInternalID(u.ID) // nolint:staticcheck return *item } + +func formatTime(v *time.Time) string { + txt := "" + if v != nil && v.Unix() != 0 { + txt = v.UTC().Format(time.RFC3339) + } + return txt +} diff --git a/pkg/registry/apis/ofrep/proxy.go b/pkg/registry/apis/ofrep/proxy.go index c75524272da..793a099339e 100644 --- a/pkg/registry/apis/ofrep/proxy.go +++ b/pkg/registry/apis/ofrep/proxy.go @@ -1,15 +1,20 @@ package ofrep import ( + "bytes" "crypto/tls" "crypto/x509" "encoding/json" "fmt" + "io" "net/http" "net/http/httputil" "os" "path" + "strconv" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util/proxyutil" ) @@ -22,20 +27,31 @@ func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r proxy.ModifyResponse = func(resp *http.Response) error { if resp.StatusCode == http.StatusOK && !isAuthedUser { - var result map[string]interface{} + var result featuremgmt.OFREPBulkResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return err } _ = resp.Body.Close() - filtered := make(map[string]any) - for k, v := range result { - if isPublicFlag(k) { - filtered[k] = v + var filteredFlags []featuremgmt.OFREPFlag + for _, f := range result.Flags { + if isPublicFlag(f.Key) { + filteredFlags = append(filteredFlags, f) } } - writeResponse(http.StatusOK, filtered, b.logger, w) + result.Flags = filteredFlags + newBodyBytes, err := json.Marshal(result) + if err != nil { + logger.Error("Failed to encode filtered result", "error", err) + return err + } + + // Replace the body + resp.Body = io.NopCloser(bytes.NewReader(newBodyBytes)) + resp.ContentLength = int64(len(newBodyBytes)) + resp.Header.Set("Content-Length", strconv.Itoa(len(newBodyBytes))) + resp.Header.Set("Content-Type", "application/json") } return nil diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index b8f2126dbcb..de3f5ab2702 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -32,6 +33,8 @@ var _ builder.APIGroupVersionProvider = (*APIBuilder)(nil) const ofrepPath = "/ofrep/v1/evaluate/flags" +const namespaceMismatchMsg = "rejecting request with namespace mismatch" + var groupVersion = schema.GroupVersion{ Group: "features.grafana.app", Version: "v0alpha1", @@ -57,10 +60,20 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri } } -func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg, staticEvaluator featuremgmt.StaticFlagEvaluator) *APIBuilder { +func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) { + var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider + var err error + + if cfg.OpenFeature.ProviderType == setting.StaticProviderType { + staticEvaluator, err = featuremgmt.CreateStaticEvaluator(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create static evaluator: %w", err) + } + } + b := NewAPIBuilder(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, true, "", staticEvaluator) apiregistration.RegisterAPI(b) - return b + return b, nil } func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { @@ -126,7 +139,7 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { return &builder.APIRoutes{ Namespace: []builder.APIRouteHandler{ { - Path: "ofrep/v1/evaluate/flags/", + Path: "ofrep/v1/evaluate/flags", Spec: &spec3.PathProps{ Post: &spec3.Operation{ OperationProps: spec3.OperationProps{ @@ -225,8 +238,8 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { if !b.validateNamespace(r) { - b.logger.Error("stackId in evaluation context does not match requested namespace") - http.Error(w, "stackId in evaluation context does not match requested namespace", http.StatusUnauthorized) + b.logger.Error(namespaceMismatchMsg) + http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized) return } @@ -255,8 +268,8 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) { func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) { if !b.validateNamespace(r) { - b.logger.Error("stackId in evaluation context does not match requested namespace") - http.Error(w, "stackId in evaluation context does not match requested namespace", http.StatusUnauthorized) + b.logger.Error(namespaceMismatchMsg) + http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized) return } @@ -279,25 +292,25 @@ func writeResponse(statusCode int, result any, logger log.Logger, w http.Respons } } -func (b *APIBuilder) stackIdFromEvalCtx(body []byte) int64 { - // Extract stackID from request body without consuming it +func (b *APIBuilder) namespaceFromEvalCtx(body []byte) string { + // Extract namespace from request body without consuming it var evalCtx struct { Context struct { - StackID int64 `json:"stackId"` // TODO -- replace with namespace "stackId" ONLY makes sense in cloud + Namespace string `json:"namespace"` } `json:"context"` } if err := json.Unmarshal(body, &evalCtx); err != nil { b.logger.Debug("Failed to unmarshal evaluation context", "error", err, "body", string(body)) - return 0 + return "" } - if evalCtx.Context.StackID <= 0 { - b.logger.Debug("Invalid or missing stackId in evaluation context", "stackId", evalCtx.Context.StackID) - return 0 + if evalCtx.Context.Namespace == "" { + b.logger.Debug("namespace missing from evaluation context", "namespace", evalCtx.Context.Namespace) + return "" } - return evalCtx.Context.StackID + return evalCtx.Context.Namespace } // isAuthenticatedRequest returns true if the request is authenticated @@ -309,7 +322,7 @@ func (b *APIBuilder) isAuthenticatedRequest(r *http.Request) bool { return user.GetIdentityType() != "" } -// validateNamespace checks if the stackId in the evaluation context matches the namespace in the request +// validateNamespace checks if the namespace in the evaluation context matches the namespace in the request func (b *APIBuilder) validateNamespace(r *http.Request) bool { // Extract namespace from request context or URL path var namespace string @@ -324,13 +337,7 @@ func (b *APIBuilder) validateNamespace(r *http.Request) bool { namespace = mux.Vars(r)["namespace"] } - info, err := types.ParseNamespace(namespace) - if err != nil { - b.logger.Error("Error parsing namespace", "error", err) - return false - } - - // Extract stackId from feature flag evaluation context + // Extract namespace from feature flag evaluation context body, err := io.ReadAll(r.Body) if err != nil { b.logger.Error("Error reading evaluation request body", "error", err) @@ -338,8 +345,9 @@ func (b *APIBuilder) validateNamespace(r *http.Request) bool { } r.Body = io.NopCloser(bytes.NewBuffer(body)) + evalCtxNamespace := b.namespaceFromEvalCtx(body) // "default" namespace case can only occur in on-prem grafana - if b.stackIdFromEvalCtx(body) == info.StackID { + if (namespace == "default" && evalCtxNamespace == "") || (evalCtxNamespace == namespace) { return true } diff --git a/pkg/registry/apis/provisioning/controller/mutator.go b/pkg/registry/apis/provisioning/controller/mutator.go new file mode 100644 index 00000000000..549c2ba763e --- /dev/null +++ b/pkg/registry/apis/provisioning/controller/mutator.go @@ -0,0 +1,9 @@ +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" +) + +type Mutator func(ctx context.Context, obj runtime.Object) error diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index 7fed0bad25d..4b673907e0f 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -60,7 +59,6 @@ type RepositoryController struct { repoSynced cache.InformerSynced parsers resources.ParserFactory logger logging.Logger - secrets secrets.Service dualwrite dualwrite.Service jobs jobs.Queue @@ -87,7 +85,6 @@ func NewRepositoryController( clients resources.ClientFactory, tester RepositoryTester, jobs jobs.Queue, - secrets secrets.Service, dualwrite dualwrite.Service, ) (*RepositoryController, error) { rc := &RepositoryController{ @@ -110,7 +107,6 @@ func NewRepositoryController( tester: tester, jobs: jobs, logger: logging.DefaultLogger.With("logger", loggerName), - secrets: secrets, dualwrite: dualwrite, } diff --git a/pkg/registry/apis/provisioning/extra.go b/pkg/registry/apis/provisioning/extra.go index f84493973a2..0c5f9e921dd 100644 --- a/pkg/registry/apis/provisioning/extra.go +++ b/pkg/registry/apis/provisioning/extra.go @@ -4,6 +4,7 @@ import ( "context" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -13,11 +14,12 @@ import ( type Extra interface { Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) - Mutate(ctx context.Context, r *provisioning.Repository) error UpdateStorage(storage map[string]rest.Storage) error PostProcessOpenAPI(oas *spec3.OpenAPI) error GetJobWorkers() []jobs.Worker AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) + RepositoryTypes() []provisioning.RepositoryType + Mutators() []controller.Mutator } type ExtraBuilder func(b *APIBuilder) Extra diff --git a/pkg/registry/apis/provisioning/extras/register.go b/pkg/registry/apis/provisioning/extras/register.go new file mode 100644 index 00000000000..30078594ed5 --- /dev/null +++ b/pkg/registry/apis/provisioning/extras/register.go @@ -0,0 +1,13 @@ +package extras + +import ( + "github.com/grafana/grafana/pkg/registry/apis/provisioning" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" +) + +// HACK: This is a hack so that wire can uniquely identify dependencies +func ProvideProvisioningOSSExtras(webhook webhooks.WebhookExtraBuilder) []provisioning.ExtraBuilder { + return []provisioning.ExtraBuilder{ + webhook.ExtraBuilder, + } +} diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go index 619bad795fe..a0548e1055b 100644 --- a/pkg/registry/apis/provisioning/files.go +++ b/pkg/registry/apis/provisioning/files.go @@ -128,12 +128,6 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime. return } - // TODO: Implement folder delete - if r.Method == http.MethodDelete && isDir { - responder.Error(apierrors.NewBadRequest("folder navigation not yet supported")) - return - } - var obj *provisioning.ResourceWrapper code := http.StatusOK switch r.Method { diff --git a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_clone_fn.go b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_clone_fn.go deleted file mode 100644 index bc690475577..00000000000 --- a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_clone_fn.go +++ /dev/null @@ -1,87 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package export - -import ( - context "context" - - repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - mock "github.com/stretchr/testify/mock" -) - -// MockWrapWithCloneFn is an autogenerated mock type for the WrapWithCloneFn type -type MockWrapWithCloneFn struct { - mock.Mock -} - -type MockWrapWithCloneFn_Expecter struct { - mock *mock.Mock -} - -func (_m *MockWrapWithCloneFn) EXPECT() *MockWrapWithCloneFn_Expecter { - return &MockWrapWithCloneFn_Expecter{mock: &_m.Mock} -} - -// Execute provides a mock function with given fields: ctx, repo, cloneOptions, pushOptions, fn -func (_m *MockWrapWithCloneFn) Execute(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error) error { - ret := _m.Called(ctx, repo, cloneOptions, pushOptions, fn) - - if len(ret) == 0 { - panic("no return value specified for Execute") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error); ok { - r0 = rf(ctx, repo, cloneOptions, pushOptions, fn) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockWrapWithCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' -type MockWrapWithCloneFn_Execute_Call struct { - *mock.Call -} - -// Execute is a helper method to define mock.On call -// - ctx context.Context -// - repo repository.Repository -// - cloneOptions repository.CloneOptions -// - pushOptions repository.PushOptions -// - fn func(repository.Repository , bool) error -func (_e *MockWrapWithCloneFn_Expecter) Execute(ctx interface{}, repo interface{}, cloneOptions interface{}, pushOptions interface{}, fn interface{}) *MockWrapWithCloneFn_Execute_Call { - return &MockWrapWithCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, cloneOptions, pushOptions, fn)} -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error)) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.CloneOptions), args[3].(repository.PushOptions), args[4].(func(repository.Repository, bool) error)) - }) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Return(_a0 error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(run) - return _c -} - -// NewMockWrapWithCloneFn creates a new instance of MockWrapWithCloneFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockWrapWithCloneFn(t interface { - mock.TestingT - Cleanup(func()) -}) *MockWrapWithCloneFn { - mock := &MockWrapWithCloneFn{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go new file mode 100644 index 00000000000..eef200f8693 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/export/mock_wrap_with_stage_fn.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package export + +import ( + context "context" + + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + mock "github.com/stretchr/testify/mock" +) + +// MockWrapWithStageFn is an autogenerated mock type for the WrapWithStageFn type +type MockWrapWithStageFn struct { + mock.Mock +} + +type MockWrapWithStageFn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockWrapWithStageFn) EXPECT() *MockWrapWithStageFn_Expecter { + return &MockWrapWithStageFn_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ctx, repo, stageOptions, fn +func (_m *MockWrapWithStageFn) Execute(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error) error { + ret := _m.Called(ctx, repo, stageOptions, fn) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error); ok { + r0 = rf(ctx, repo, stageOptions, fn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockWrapWithStageFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type MockWrapWithStageFn_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ctx context.Context +// - repo repository.Repository +// - stageOptions repository.StageOptions +// - fn func(repository.Repository , bool) error +func (_e *MockWrapWithStageFn_Expecter) Execute(ctx interface{}, repo interface{}, stageOptions interface{}, fn interface{}) *MockWrapWithStageFn_Execute_Call { + return &MockWrapWithStageFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, stageOptions, fn)} +} + +func (_c *MockWrapWithStageFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repository.Repository, bool) error)) *MockWrapWithStageFn_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.StageOptions), args[3].(func(repository.Repository, bool) error)) + }) + return _c +} + +func (_c *MockWrapWithStageFn_Execute_Call) Return(_a0 error) *MockWrapWithStageFn_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockWrapWithStageFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.StageOptions, func(repository.Repository, bool) error) error) *MockWrapWithStageFn_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewMockWrapWithStageFn creates a new instance of MockWrapWithStageFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockWrapWithStageFn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWrapWithStageFn { + mock := &MockWrapWithStageFn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/export/resources.go b/pkg/registry/apis/provisioning/jobs/export/resources.go index c469f4def8c..e02b7d7bfad 100644 --- a/pkg/registry/apis/provisioning/jobs/export/resources.go +++ b/pkg/registry/apis/provisioning/jobs/export/resources.go @@ -67,7 +67,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, } } - if err := exportResource(ctx, options, client, shim, repositoryResources, progress); err != nil { + if err := exportResource(ctx, kind.Resource, options, client, shim, repositoryResources, progress); err != nil { return fmt.Errorf("export %s: %w", kind.Resource, err) } } @@ -76,6 +76,7 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions, } func exportResource(ctx context.Context, + resource string, options provisioning.ExportJobOptions, client dynamic.ResourceInterface, shim conversionShim, @@ -88,7 +89,7 @@ func exportResource(ctx context.Context, gvk := item.GroupVersionKind() result := jobs.JobResourceResult{ Name: item.GetName(), - Resource: gvk.Kind, + Resource: resource, Group: gvk.Group, Action: repository.FileActionCreated, } diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index ca4df587476..5ded8555257 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -9,34 +9,33 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) //go:generate mockery --name ExportFn --structname MockExportFn --inpackage --filename mock_export_fn.go --with-expecter type ExportFn func(ctx context.Context, repoName string, options provisioning.ExportJobOptions, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error -//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter -type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error +//go:generate mockery --name WrapWithStageFn --structname MockWrapWithStageFn --inpackage --filename mock_wrap_with_stage_fn.go --with-expecter +type WrapWithStageFn func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repo repository.Repository, staged bool) error) error type ExportWorker struct { clientFactory resources.ClientFactory repositoryResources resources.RepositoryResourcesFactory exportFn ExportFn - wrapWithCloneFn WrapWithCloneFn + wrapWithStageFn WrapWithStageFn } func NewExportWorker( clientFactory resources.ClientFactory, repositoryResources resources.RepositoryResourcesFactory, exportFn ExportFn, - wrapWithCloneFn WrapWithCloneFn, + wrapWithStageFn WrapWithStageFn, ) *ExportWorker { return &ExportWorker{ clientFactory: clientFactory, repositoryResources: repositoryResources, exportFn: exportFn, - wrapWithCloneFn: wrapWithCloneFn, + wrapWithStageFn: wrapWithStageFn, } } @@ -57,32 +56,9 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, return err } - writer := gogit.Progress(func(line string) { - progress.SetMessage(ctx, line) - }, "finished") - - cloneOptions := repository.CloneOptions{ + cloneOptions := repository.StageOptions{ Timeout: 10 * time.Minute, PushOnWrites: false, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "clone target") - // :( the branch is now baked into the repo - if options.Branch != "" { - return fmt.Errorf("branch is not supported for clonable repositories") - } - - return nil - }, - } - - pushOptions := repository.PushOptions{ - Timeout: 10 * time.Minute, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "push changes") - return nil - }, } fn := func(repo repository.Repository, _ bool) error { @@ -104,5 +80,5 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, return r.exportFn(ctx, cfg.Name, *options, clients, repositoryResources, progress) } - return r.wrapWithCloneFn(ctx, repo, cloneOptions, pushOptions, fn) + return r.wrapWithStageFn(ctx, repo, cloneOptions, fn) } diff --git a/pkg/registry/apis/provisioning/jobs/export/worker_test.go b/pkg/registry/apis/provisioning/jobs/export/worker_test.go index abbf2b34143..aa5f2ea1c5e 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -143,12 +142,12 @@ func TestExportWorker_ProcessFailedToCreateClients(t *testing.T) { mockClients := resources.NewMockClientFactory(t) mockClients.On("Clients", context.Background(), "test-namespace").Return(nil, errors.New("failed to create clients")) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute) mockProgress := jobs.NewMockJobProgressRecorder(t) err := r.Process(context.Background(), mockRepo, job, mockProgress) @@ -179,12 +178,12 @@ func TestExportWorker_ProcessNotReaderWriter(t *testing.T) { mockClients.On("Clients", context.Background(), "test-namespace").Return(resourceClients, nil) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export job submitted targeting repository that is not a ReaderWriter") } @@ -216,16 +215,16 @@ func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) { mockRepoResources.On("Client", context.Background(), mockRepo).Return(nil, fmt.Errorf("failed to create repository resources client")) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, nil, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, nil, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "create repository resource client: failed to create repository resources client") } -func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) { +func TestExportWorker_ProcessStageOptions(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, @@ -245,9 +244,7 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) - // Verify progress messages are set - mockProgress.On("SetMessage", mock.Anything, "clone target").Return() - mockProgress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current implementation mockClients := resources.NewMockClientFactory(t) mockResourceClients := resources.NewMockResourceClients(t) @@ -260,21 +257,15 @@ func TestExportWorker_ProcessCloneAndPushOptions(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockCloneFn := NewMockWrapWithCloneFn(t) + mockStageFn := NewMockWrapWithStageFn(t) // Verify clone and push options - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool { - return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil - }), mock.MatchedBy(func(opts repository.PushOptions) bool { - return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil - }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { - // Execute both BeforeFn functions to verify progress messages - assert.NoError(t, cloneOpts.BeforeFn()) - assert.NoError(t, pushOpts.BeforeFn()) - + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { + return opts.Timeout == 10*time.Minute && !opts.PushOnWrites + }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -310,17 +301,17 @@ func TestExportWorker_ProcessExportFnError(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed")) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export failed") } -func TestExportWorker_ProcessWrapWithCloneFnError(t *testing.T) { +func TestExportWorker_ProcessWrapWithStageFnError(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, @@ -340,15 +331,15 @@ func TestExportWorker_ProcessWrapWithCloneFnError(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("clone failed")) + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(errors.New("stage failed")) - r := NewExportWorker(nil, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(nil, nil, nil, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) - require.EqualError(t, err, "clone failed") + require.EqualError(t, err, "stage failed") } -func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing.T) { +func TestExportWorker_ProcessBranchNotAllowedForStageableRepositories(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, @@ -362,24 +353,16 @@ func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing. mockRepo.On("Config").Return(&v0alpha1.Repository{ Spec: v0alpha1.RepositorySpec{ Type: v0alpha1.GitHubRepositoryType, - Workflows: []v0alpha1.Workflow{v0alpha1.BranchWorkflow}, + Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, // Only write workflow, not branch }, }) mockProgress := jobs.NewMockJobProgressRecorder(t) - mockProgress.On("SetMessage", mock.Anything, "clone target").Return() - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { - if cloneOpts.BeforeFn != nil { - return cloneOpts.BeforeFn() - } + // No progress messages expected in current implementation - return fn(repo, true) - }) - - r := NewExportWorker(nil, nil, nil, mockCloneFn.Execute) + r := NewExportWorker(nil, nil, nil, nil) err := r.Process(context.Background(), mockRepo, job, mockProgress) - require.EqualError(t, err, "branch is not supported for clonable repositories") + require.EqualError(t, err, "this repository does not support the branch workflow") } func TestExportWorker_ProcessGitRepository(t *testing.T) { @@ -407,9 +390,7 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) - // Verify progress messages are set - mockProgress.On("SetMessage", mock.Anything, "clone target").Return() - mockProgress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current implementation mockClients := resources.NewMockClientFactory(t) mockResourceClients := resources.NewMockResourceClients(t) @@ -422,21 +403,15 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - mockCloneFn := NewMockWrapWithCloneFn(t) + mockStageFn := NewMockWrapWithStageFn(t) // Verify clone and push options - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool { - return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil - }), mock.MatchedBy(func(opts repository.PushOptions) bool { - return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil - }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { - // Execute both BeforeFn functions to verify progress messages - assert.NoError(t, cloneOpts.BeforeFn()) - assert.NoError(t, pushOpts.BeforeFn()) - + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { + return opts.Timeout == 10*time.Minute && !opts.PushOnWrites + }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -477,12 +452,12 @@ func TestExportWorker_ProcessGitRepositoryExportFnError(t *testing.T) { mockExportFn := NewMockExportFn(t) mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed")) - mockCloneFn := NewMockWrapWithCloneFn(t) - mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error { + mockStageFn := NewMockWrapWithStageFn(t) + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export failed") } diff --git a/pkg/registry/apis/provisioning/jobs/history_mock.go b/pkg/registry/apis/provisioning/jobs/history_mock.go index fd762e9f485..dbb61568566 100644 --- a/pkg/registry/apis/provisioning/jobs/history_mock.go +++ b/pkg/registry/apis/provisioning/jobs/history_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go index f7f4c53681f..7ead8a206e7 100644 --- a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go +++ b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go index ad2754bc444..191e0c4922f 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go @@ -10,57 +10,38 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" ) type LegacyMigrator struct { legacyMigrator LegacyResourcesMigrator storageSwapper StorageSwapper syncWorker jobs.Worker - wrapWithCloneFn WrapWithCloneFn + wrapWithStageFn WrapWithStageFn } func NewLegacyMigrator( legacyMigrator LegacyResourcesMigrator, storageSwapper StorageSwapper, syncWorker jobs.Worker, - wrapWithCloneFn WrapWithCloneFn, + wrapWithStageFn WrapWithStageFn, ) *LegacyMigrator { return &LegacyMigrator{ legacyMigrator: legacyMigrator, storageSwapper: storageSwapper, syncWorker: syncWorker, - wrapWithCloneFn: wrapWithCloneFn, + wrapWithStageFn: wrapWithStageFn, } } func (m *LegacyMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { namespace := rw.Config().Namespace - - writer := gogit.Progress(func(line string) { - progress.SetMessage(ctx, line) - }, "finished") - cloneOptions := repository.CloneOptions{ + stageOptions := repository.StageOptions{ PushOnWrites: options.History, // TODO: make this configurable - Timeout: 10 * time.Minute, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "clone repository") - return nil - }, - } - pushOptions := repository.PushOptions{ - // TODO: make this configurable - Timeout: 10 * time.Minute, - Progress: writer, - BeforeFn: func() error { - progress.SetMessage(ctx, "push changes") - return nil - }, + Timeout: 10 * time.Minute, } - if err := m.wrapWithCloneFn(ctx, rw, cloneOptions, pushOptions, func(repo repository.Repository, cloned bool) error { + if err := m.wrapWithStageFn(ctx, rw, stageOptions, func(repo repository.Repository, staged bool) error { rw, ok := repo.(repository.ReaderWriter) if !ok { return errors.New("migration job submitted targeting repository that is not a ReaderWriter") diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go index 5dc64480333..877bd01fd33 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go @@ -3,7 +3,6 @@ package migrate import ( "context" "errors" - "fmt" "testing" "time" @@ -16,12 +15,12 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" ) -func TestWrapWithCloneFn(t *testing.T) { +func TestWrapWithStageFn(t *testing.T) { t.Run("should return error when repository is not a ReaderWriter", func(t *testing.T) { // Setup ctx := context.Background() // Create the wrapper function that matches WrapWithCloneFn signature - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { // pass a reader to function call repo := repository.NewMockReader(t) return fn(repo, true) @@ -56,7 +55,7 @@ func TestWrapWithCloneFn_Error(t *testing.T) { expectedErr := errors.New("clone failed") // Create the wrapper function that returns an error - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return expectedErr } @@ -98,7 +97,7 @@ func TestLegacyMigrator_MigrateFails(t *testing.T) { mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -147,7 +146,7 @@ func TestLegacyMigrator_ResetUnifiedStorageFails(t *testing.T) { mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -202,7 +201,7 @@ func TestLegacyMigrator_SyncFails(t *testing.T) { }), mock.Anything).Return(expectedErr) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -257,7 +256,7 @@ func TestLegacyMigrator_SyncFails(t *testing.T) { }), mock.Anything).Return(syncErr) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -310,7 +309,7 @@ func TestLegacyMigrator_Success(t *testing.T) { }), mock.Anything).Return(nil) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(rw, true) } @@ -352,19 +351,7 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) { mockStorageSwapper := NewMockStorageSwapper(t) mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { - if clone.BeforeFn != nil { - if err := clone.BeforeFn(); err != nil { - return err - } - } - - if push.BeforeFn != nil { - if err := push.BeforeFn(); err != nil { - return err - } - } - + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return errors.New("abort test here") } @@ -376,8 +363,7 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "clone repository").Return() - progress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current staging implementation // Execute repo := repository.NewMockRepository(t) @@ -399,19 +385,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { mockWorker := jobs.NewMockWorker(t) // Create a wrapper function that calls the provided function - wrapFn := func(ctx context.Context, rw repository.Repository, clone repository.CloneOptions, push repository.PushOptions, fn func(repository.Repository, bool) error) error { - if clone.Progress != nil { - if _, err := clone.Progress.Write([]byte("clone repository\n")); err != nil { - return fmt.Errorf("failed to write to clone progress in tests: %w", err) - } - } - - if push.Progress != nil { - if _, err := push.Progress.Write([]byte("push changes\n")); err != nil { - return fmt.Errorf("failed to write to push progress in tests: %w", err) - } - } - + wrapFn := func(ctx context.Context, rw repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return errors.New("abort test here") } @@ -423,8 +397,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, "clone repository").Return() - progress.On("SetMessage", mock.Anything, "push changes").Return() + // No progress messages expected in current staging implementation repo := repository.NewMockRepository(t) repo.On("Config").Return(&provisioning.Repository{ @@ -437,10 +410,7 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { require.EqualError(t, err, "migrate from SQL: abort test here") require.Eventually(t, func() bool { - if len(progress.Calls) != 2 { - return false - } - + // No progress message calls expected in current staging implementation return progress.AssertExpectations(t) }, time.Second, 10*time.Millisecond) }) diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go index ff6a0cd0cf7..9fa21e12ab0 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/mock_bulk_store_client.go @@ -9,7 +9,7 @@ import ( mock "github.com/stretchr/testify/mock" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) // MockBulkStoreClient is an autogenerated mock type for the BulkStoreClient type diff --git a/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_clone_fn.go b/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_clone_fn.go deleted file mode 100644 index 8ab2973de33..00000000000 --- a/pkg/registry/apis/provisioning/jobs/migrate/mock_wrap_with_clone_fn.go +++ /dev/null @@ -1,87 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package migrate - -import ( - context "context" - - repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - mock "github.com/stretchr/testify/mock" -) - -// MockWrapWithCloneFn is an autogenerated mock type for the WrapWithCloneFn type -type MockWrapWithCloneFn struct { - mock.Mock -} - -type MockWrapWithCloneFn_Expecter struct { - mock *mock.Mock -} - -func (_m *MockWrapWithCloneFn) EXPECT() *MockWrapWithCloneFn_Expecter { - return &MockWrapWithCloneFn_Expecter{mock: &_m.Mock} -} - -// Execute provides a mock function with given fields: ctx, repo, cloneOptions, pushOptions, fn -func (_m *MockWrapWithCloneFn) Execute(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error) error { - ret := _m.Called(ctx, repo, cloneOptions, pushOptions, fn) - - if len(ret) == 0 { - panic("no return value specified for Execute") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error); ok { - r0 = rf(ctx, repo, cloneOptions, pushOptions, fn) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockWrapWithCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' -type MockWrapWithCloneFn_Execute_Call struct { - *mock.Call -} - -// Execute is a helper method to define mock.On call -// - ctx context.Context -// - repo repository.Repository -// - cloneOptions repository.CloneOptions -// - pushOptions repository.PushOptions -// - fn func(repository.Repository , bool) error -func (_e *MockWrapWithCloneFn_Expecter) Execute(ctx interface{}, repo interface{}, cloneOptions interface{}, pushOptions interface{}, fn interface{}) *MockWrapWithCloneFn_Execute_Call { - return &MockWrapWithCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, cloneOptions, pushOptions, fn)} -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repository.Repository, bool) error)) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Repository), args[2].(repository.CloneOptions), args[3].(repository.PushOptions), args[4].(func(repository.Repository, bool) error)) - }) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) Return(_a0 error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockWrapWithCloneFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Repository, repository.CloneOptions, repository.PushOptions, func(repository.Repository, bool) error) error) *MockWrapWithCloneFn_Execute_Call { - _c.Call.Return(run) - return _c -} - -// NewMockWrapWithCloneFn creates a new instance of MockWrapWithCloneFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockWrapWithCloneFn(t interface { - mock.TestingT - Cleanup(func()) -}) *MockWrapWithCloneFn { - mock := &MockWrapWithCloneFn{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go index 2879d14fe7e..e2caeb7b3f4 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" ) -//go:generate mockery --name WrapWithCloneFn --structname MockWrapWithCloneFn --inpackage --filename mock_wrap_with_clone_fn.go --with-expecter -type WrapWithCloneFn func(ctx context.Context, repo repository.Repository, cloneOptions repository.CloneOptions, pushOptions repository.PushOptions, fn func(repo repository.Repository, cloned bool) error) error +//go:generate mockery --name WrapWithStageFn --structname MockWrapWithStageFn --inpackage --filename mock_wrap_with_stage_fn.go --with-expecter +type WrapWithStageFn func(ctx context.Context, repo repository.Repository, stageOptions repository.StageOptions, fn func(repo repository.Repository, staged bool) error) error type UnifiedStorageMigrator struct { namespaceCleaner NamespaceCleaner diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go index 1524487bf75..31242a48189 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go @@ -12,6 +12,7 @@ import ( provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/local" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -88,7 +89,7 @@ func TestMigrationWorker_WithHistory(t *testing.T) { progressRecorder.On("SetTotal", mock.Anything, 10).Return() progressRecorder.On("Strict").Return() - repo := repository.NewLocal(&provisioning.Repository{}, nil) + repo := local.NewLocal(&provisioning.Repository{}, nil) err := worker.Process(context.Background(), repo, job, progressRecorder) require.EqualError(t, err, "history is only supported for github repositories") }) diff --git a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go index fd634d60844..cfe7e46f88b 100644 --- a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/queue_mock.go b/pkg/registry/apis/provisioning/jobs/queue_mock.go index f20a8be6221..41552749cad 100644 --- a/pkg/registry/apis/provisioning/jobs/queue_mock.go +++ b/pkg/registry/apis/provisioning/jobs/queue_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go b/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go index d0152e725bc..b7dc38de650 100644 --- a/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go +++ b/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/store_mock.go b/pkg/registry/apis/provisioning/jobs/store_mock.go index 300bf5fb271..8a6bb364f80 100644 --- a/pkg/registry/apis/provisioning/jobs/store_mock.go +++ b/pkg/registry/apis/provisioning/jobs/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/sync/changes.go b/pkg/registry/apis/provisioning/jobs/sync/changes.go index 2e9b789c992..4c1f5db9136 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/changes.go +++ b/pkg/registry/apis/provisioning/jobs/sync/changes.go @@ -3,7 +3,6 @@ package sync import ( "context" "fmt" - "sort" "strings" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" @@ -143,17 +142,7 @@ func Changes(source []repository.FileTreeEntry, target *provisioning.ResourceLis } // Deepest first (stable sort order) - sort.Slice(changes, func(i, j int) bool { - if safepath.Depth(changes[i].Path) > safepath.Depth(changes[j].Path) { - return true - } - - if safepath.Depth(changes[i].Path) < safepath.Depth(changes[j].Path) { - return false - } - - return changes[i].Path < changes[j].Path - }) + safepath.SortByDepth(changes, func(c ResourceFileChange) string { return c.Path }, false) return changes, nil } diff --git a/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go index 7cb74e9497a..9a27ddb394c 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/compare_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go index 29e44c5c02c..d2da032c6c0 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go index cef2b0deb07..b38f813e68d 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go index 6998d67f019..fb44f7eda29 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/repository_patch_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go b/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go index 79ac697436d..3eb3e4de7bb 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/syncer_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package sync diff --git a/pkg/registry/apis/provisioning/jobs/worker_mock.go b/pkg/registry/apis/provisioning/jobs/worker_mock.go index b264b258d1c..59e3c0df1b4 100644 --- a/pkg/registry/apis/provisioning/jobs/worker_mock.go +++ b/pkg/registry/apis/provisioning/jobs/worker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/refs.go b/pkg/registry/apis/provisioning/refs.go new file mode 100644 index 00000000000..47fe2b24324 --- /dev/null +++ b/pkg/registry/apis/provisioning/refs.go @@ -0,0 +1,85 @@ +package provisioning + +import ( + "context" + "net/http" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" +) + +type refsConnector struct { + getter RepoGetter +} + +func NewRefsConnector(getter RepoGetter) *refsConnector { + return &refsConnector{getter: getter} +} + +func (*refsConnector) New() runtime.Object { + return &provisioning.RefList{} +} + +func (*refsConnector) Destroy() {} + +func (*refsConnector) ProducesMIMETypes(verb string) []string { + return []string{"application/json"} +} + +func (*refsConnector) ProducesObject(verb string) any { + return &provisioning.RefList{} +} + +func (*refsConnector) ConnectMethods() []string { + return []string{http.MethodGet} +} + +func (*refsConnector) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (c *refsConnector) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + logger := logging.FromContext(ctx).With("logger", "refs-connector", "repository_name", name) + ctx = logging.Context(ctx, logger) + repo, err := c.getter.GetHealthyRepository(ctx, name) + if err != nil { + logger.Debug("failed to find repository", "error", err) + return nil, err + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + responder.Error(apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method)) + return + } + + versionedRepo, ok := repo.(repository.Versioned) + if !ok { + responder.Error(apierrors.NewBadRequest("repository does not support versioned operations")) + return + } + + refs, err := versionedRepo.ListRefs(ctx) + if err != nil { + responder.Error(err) + return + } + + refsList := &provisioning.RefList{ + Items: refs, + } + + responder.Object(http.StatusOK, refsList) + }), nil +} + +var ( + _ rest.Storage = (*refsConnector)(nil) + _ rest.Connecter = (*refsConnector)(nil) + _ rest.StorageMetadata = (*refsConnector)(nil) +) diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index fe5087ca1d3..a0f02502dba 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -2,6 +2,7 @@ package provisioning import ( "context" + "errors" "fmt" "net/http" "path/filepath" @@ -37,6 +38,7 @@ import ( client "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" informers "github.com/grafana/grafana/pkg/generated/informers/externalversions" listers "github.com/grafana/grafana/pkg/generated/listers/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" @@ -45,9 +47,9 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/migrate" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/sync" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/nanogit" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/local" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" @@ -55,7 +57,6 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" - grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -75,8 +76,9 @@ var ( type APIBuilder struct { features featuremgmt.FeatureToggles + tracer tracing.Tracer getter rest.Getter - localFileResolver *repository.LocalFolderResolver + localFileResolver *local.LocalFolderResolver parsers resources.ParserFactory repositoryResources resources.RepositoryResourcesFactory clients resources.ClientFactory @@ -86,26 +88,28 @@ type APIBuilder struct { jobs.Queue jobs.Store } - jobHistory jobs.History - tester *RepositoryTester - resourceLister resources.ResourceLister - repositoryLister listers.RepositoryLister - legacyMigrator legacy.LegacyMigrator - storageStatus dualwrite.Service - unified resource.ResourceClient - secrets secrets.Service - client client.ProvisioningV0alpha1Interface - access authlib.AccessChecker - statusPatcher *controller.RepositoryStatusPatcher + jobHistory jobs.History + tester *RepositoryTester + resourceLister resources.ResourceLister + repositoryLister listers.RepositoryLister + legacyMigrator legacy.LegacyMigrator + storageStatus dualwrite.Service + unified resource.ResourceClient + repositorySecrets secrets.RepositorySecrets + client client.ProvisioningV0alpha1Interface + access authlib.AccessChecker + mutators []controller.Mutator + statusPatcher *controller.RepositoryStatusPatcher // Extras provides additional functionality to the API. - extras []Extra + extras []Extra + availableRepositoryTypes map[provisioning.RepositoryType]bool } // NewAPIBuilder creates an API builder. // It avoids anything that is core to Grafana, such that it can be used in a multi-tenant service down the line. // This means there are no hidden dependencies, and no use of e.g. *settings.Cfg. func NewAPIBuilder( - local *repository.LocalFolderResolver, + local *local.LocalFolderResolver, features featuremgmt.FeatureToggles, unified resource.ResourceClient, clonedir string, // where repo clones are managed @@ -113,15 +117,23 @@ func NewAPIBuilder( ghFactory *github.Factory, legacyMigrator legacy.LegacyMigrator, storageStatus dualwrite.Service, - secrets secrets.Service, + repositorySecrets secrets.RepositorySecrets, access authlib.AccessChecker, + tracer tracing.Tracer, extraBuilders []ExtraBuilder, ) *APIBuilder { clients := resources.NewClientFactory(configProvider) parsers := resources.NewParserFactory(clients) resourceLister := resources.NewResourceLister(unified, unified, legacyMigrator, storageStatus) + mutators := []controller.Mutator{ + git.Mutator(repositorySecrets), + github.Mutator(repositorySecrets), + } + b := &APIBuilder{ + mutators: mutators, + tracer: tracer, localFileResolver: local, features: features, ghFactory: ghFactory, @@ -133,15 +145,28 @@ func NewAPIBuilder( legacyMigrator: legacyMigrator, storageStatus: storageStatus, unified: unified, - secrets: secrets, + repositorySecrets: repositorySecrets, access: access, jobHistory: jobs.NewJobHistoryCache(), + availableRepositoryTypes: map[provisioning.RepositoryType]bool{ + provisioning.LocalRepositoryType: true, + provisioning.GitHubRepositoryType: true, + }, } for _, builder := range extraBuilders { b.extras = append(b.extras, builder(b)) } + // Add the available repository types and mutators from the extras + for _, extra := range b.extras { + for _, t := range extra.RepositoryTypes() { + b.availableRepositoryTypes[t] = true + } + + b.mutators = append(b.mutators, extra.Mutators()...) + } + return b } @@ -160,22 +185,15 @@ func RegisterAPIService( legacyMigrator legacy.LegacyMigrator, storageStatus dualwrite.Service, usageStatsService usagestats.Service, - // FIXME: use multi-tenant service when one exists. In this state, we can't make this a multi-tenant service! - secretsSvc grafanasecrets.Service, + repositorySecrets secrets.RepositorySecrets, + tracer tracing.Tracer, extraBuilders []ExtraBuilder, ) (*APIBuilder, error) { if !features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { return nil, nil } - logger := logging.DefaultLogger.With("logger", "provisioning startup") - if features.IsEnabledGlobally(featuremgmt.FlagNanoGit) { - logger.Info("Using nanogit for repositories") - } else { - logger.Debug("Using go-git and Github API for repositories") - } - - folderResolver := &repository.LocalFolderResolver{ + folderResolver := &local.LocalFolderResolver{ PermittedPrefixes: cfg.PermittedProvisioningPaths, HomePath: safepath.Clean(cfg.HomePath), } @@ -184,7 +202,9 @@ func RegisterAPIService( filepath.Join(cfg.DataPath, "clone"), // where repositories are cloned (temporarialy for now) configProvider, ghFactory, legacyMigrator, storageStatus, - secrets.NewSingleTenant(secretsSvc), access, + repositorySecrets, + access, + tracer, extraBuilders, ) apiregistration.RegisterAPI(builder) @@ -206,6 +226,7 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { // * Reading and modifying a repository's configuration requires administrator privileges. // * Reading a repository's limited configuration (/stats & /settings) requires viewer privileges. // * Reading a repository's files requires viewer privileges. + // * Reading a repository's refs requires viewer privileges. // * Editing a repository's files requires editor privileges. // * Syncing a repository requires editor privileges. // * Exporting a repository requires administrator privileges. @@ -237,6 +258,12 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { } return authorizer.DecisionDeny, "admin role is required", nil + case "refs": + // This is strictly a read operation. It is handy on the frontend for viewers. + if id.GetOrgRole().Includes(identity.RoleViewer) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "viewer role is required", nil case "files": // Access to files is controlled by the AccessClient return authorizer.DecisionAllow, "", nil @@ -357,6 +384,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI getter: b, } storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.access) + storage[provisioning.RepositoryResourceInfo.StoragePath("refs")] = NewRefsConnector(b) storage[provisioning.RepositoryResourceInfo.StoragePath("resources")] = &listConnector{ getter: b, lister: b.resourceLister, @@ -445,17 +473,9 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis r.Spec.Workflows = []provisioning.Workflow{} } - if err := b.encryptGithubToken(ctx, r); err != nil { - return fmt.Errorf("failed to encrypt secrets: %w", err) - } - - if err := b.encryptGitToken(ctx, r); err != nil { - return fmt.Errorf("failed to encrypt secrets: %w", err) - } - // Mutate the repository with any extra mutators - for _, extra := range b.extras { - if err := extra.Mutate(ctx, r); err != nil { + for _, mutator := range b.mutators { + if err := mutator(ctx, r); err != nil { return fmt.Errorf("failed to mutate repository: %w", err) } } @@ -463,36 +483,6 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis return nil } -// TODO: move this to a more appropriate place -func (b *APIBuilder) encryptGithubToken(ctx context.Context, repo *provisioning.Repository) error { - var err error - if repo.Spec.GitHub != nil && - repo.Spec.GitHub.Token != "" { - repo.Spec.GitHub.EncryptedToken, err = b.secrets.Encrypt(ctx, []byte(repo.Spec.GitHub.Token)) - if err != nil { - return err - } - repo.Spec.GitHub.Token = "" - } - - return nil -} - -// TODO: move this to a more appropriate place -func (b *APIBuilder) encryptGitToken(ctx context.Context, repo *provisioning.Repository) error { - var err error - if repo.Spec.Git != nil && - repo.Spec.Git.Token != "" { - repo.Spec.Git.EncryptedToken, err = b.secrets.Encrypt(ctx, []byte(repo.Spec.Git.Token)) - if err != nil { - return err - } - repo.Spec.Git.Token = "" - } - - return nil -} - // TODO: move logic to a more appropriate place. Probably controller/validation.go func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { obj := a.GetObject() @@ -606,11 +596,12 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.repositoryLister = repoInformer.Lister() + stageIfPossible := repository.WrapWithStageAndPushIfPossible exportWorker := export.NewExportWorker( b.clients, b.repositoryResources, export.ExportAll, - repository.WrapWithCloneAndPushIfPossible, + stageIfPossible, ) b.statusPatcher = controller.NewRepositoryStatusPatcher(b.GetClient()) @@ -636,7 +627,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH legacyResources, storageSwapper, syncWorker, - repository.WrapWithCloneAndPushIfPossible, + stageIfPossible, ) cleaner := migrate.NewNamespaceCleaner(b.clients) @@ -680,7 +671,6 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.clients, &repository.Tester{}, b.jobs, - b.secrets, b.storageStatus, ) if err != nil { @@ -770,6 +760,22 @@ func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, err sub.Get.Parameters = []*spec3.Parameter{ref} } + // Show refs endpoint documentation + sub = oas.Paths.Paths[repoprefix+"/refs"] + if sub != nil { + sub.Get.Description = "Get the repository references" + sub.Get.Summary = "Repository refs listing" + sub.Get.Parameters = []*spec3.Parameter{} + sub.Post = nil + sub.Put = nil + sub.Delete = nil + + // Replace the content type for this response + mt := sub.Get.Responses.StatusCodeResponses[200].Content + s := defs[defsBase+"RefList"].Schema + mt["*/*"].Schema = &s + } + // Show a special list command sub = oas.Paths.Paths[repoprefix+"/files"] if sub != nil { @@ -1169,53 +1175,70 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor } switch r.Spec.Type { + case provisioning.BitbucketRepositoryType: + return nil, errors.New("repository type bitbucket is not available") + case provisioning.GitLabRepositoryType: + return nil, errors.New("repository type gitlab is not available") case provisioning.LocalRepositoryType: - return repository.NewLocal(r, b.localFileResolver), nil + return local.NewLocal(r, b.localFileResolver), nil case provisioning.GitRepositoryType: - return nanogit.NewGitRepository(ctx, b.secrets, r, nanogit.RepositoryConfig{ + // Decrypt token if needed + token := r.Spec.Git.Token + if token == "" && len(r.Spec.Git.EncryptedToken) > 0 { + decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(r.Spec.Git.EncryptedToken)) + if err != nil { + return nil, fmt.Errorf("decrypt git token: %w", err) + } + token = string(decrypted) + } + + cfg := git.RepositoryConfig{ URL: r.Spec.Git.URL, Branch: r.Spec.Git.Branch, Path: r.Spec.Git.Path, - Token: r.Spec.Git.Token, + Token: token, EncryptedToken: r.Spec.Git.EncryptedToken, - }) + } + + return git.NewGitRepository(ctx, r, cfg, b.repositorySecrets) case provisioning.GitHubRepositoryType: - cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { - return gogit.Clone(ctx, b.clonedir, r, opts, b.secrets) - } - - apiRepo, err := repository.NewGitHub(ctx, r, b.ghFactory, b.secrets, cloneFn) - if err != nil { - return nil, fmt.Errorf("create github API repository: %w", err) - } - logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path) - if !b.features.IsEnabledGlobally(featuremgmt.FlagNanoGit) { - logger.Debug("Instantiating Github repository with go-git and Github API") - return apiRepo, nil - } - - logger.Info("Instantiating Github repository with nanogit") + logger.Info("Instantiating Github repository") ghCfg := r.Spec.GitHub if ghCfg == nil { return nil, fmt.Errorf("github configuration is required for nano git") } - gitCfg := nanogit.RepositoryConfig{ + // Decrypt GitHub token if needed + ghToken := ghCfg.Token + if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { + decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken)) + if err != nil { + return nil, fmt.Errorf("decrypt github token: %w", err) + } + ghToken = string(decrypted) + } + + gitCfg := git.RepositoryConfig{ URL: ghCfg.URL, Branch: ghCfg.Branch, Path: ghCfg.Path, - Token: ghCfg.Token, + Token: ghToken, EncryptedToken: ghCfg.EncryptedToken, } - nanogitRepo, err := nanogit.NewGitRepository(ctx, b.secrets, r, gitCfg) + gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, b.repositorySecrets) if err != nil { - return nil, fmt.Errorf("error creating nanogit repository: %w", err) + return nil, fmt.Errorf("error creating git repository: %w", err) } - return nanogit.NewGithubRepository(apiRepo, nanogitRepo), nil + ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, ghToken, b.repositorySecrets) + if err != nil { + return nil, fmt.Errorf("error creating github repository: %w", err) + } + + return ghRepo, nil default: return nil, fmt.Errorf("unknown repository type (%s)", r.Spec.Type) } diff --git a/pkg/registry/apis/provisioning/repository/clonable_repository_mock.go b/pkg/registry/apis/provisioning/repository/clonable_repository_mock.go deleted file mode 100644 index 9e1d671fe36..00000000000 --- a/pkg/registry/apis/provisioning/repository/clonable_repository_mock.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package repository - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockClonableRepository is an autogenerated mock type for the ClonableRepository type -type MockClonableRepository struct { - mock.Mock -} - -type MockClonableRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *MockClonableRepository) EXPECT() *MockClonableRepository_Expecter { - return &MockClonableRepository_Expecter{mock: &_m.Mock} -} - -// Clone provides a mock function with given fields: ctx, opts -func (_m *MockClonableRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Clone") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClonableRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' -type MockClonableRepository_Clone_Call struct { - *mock.Call -} - -// Clone is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockClonableRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockClonableRepository_Clone_Call { - return &MockClonableRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)} -} - -func (_c *MockClonableRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockClonableRepository_Clone_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockClonableRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockClonableRepository_Clone_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClonableRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockClonableRepository_Clone_Call { - _c.Call.Return(run) - return _c -} - -// NewMockClonableRepository creates a new instance of MockClonableRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockClonableRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *MockClonableRepository { - mock := &MockClonableRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/clone.go b/pkg/registry/apis/provisioning/repository/clone.go deleted file mode 100644 index d7224a23631..00000000000 --- a/pkg/registry/apis/provisioning/repository/clone.go +++ /dev/null @@ -1,44 +0,0 @@ -package repository - -import ( - context "context" - "fmt" - - "github.com/grafana/grafana-app-sdk/logging" -) - -// WrapWithCloneAndPushIfPossible clones a repository if possible, executes operations on the clone, -// and automatically pushes changes when the function completes. For repositories that support cloning, -// all operations are transparently executed on the clone, and the clone is automatically cleaned up -// afterward. If cloning is not supported, the original repository instance is used directly. -func WrapWithCloneAndPushIfPossible( - ctx context.Context, - repo Repository, - cloneOptions CloneOptions, - pushOptions PushOptions, - fn func(repo Repository, cloned bool) error, -) error { - clonable, ok := repo.(ClonableRepository) - if !ok { - return fn(repo, false) - } - - clone, err := clonable.Clone(ctx, cloneOptions) - if err != nil { - return fmt.Errorf("clone repository: %w", err) - } - - // We don't, we simply log it - // FIXME: should we handle this differently? - defer func() { - if err := clone.Remove(ctx); err != nil { - logging.FromContext(ctx).Error("failed to remove cloned repository after export", "err", err) - } - }() - - if err := fn(clone, true); err != nil { - return err - } - - return clone.Push(ctx, pushOptions) -} diff --git a/pkg/registry/apis/provisioning/repository/clone_fn_mock.go b/pkg/registry/apis/provisioning/repository/clone_fn_mock.go deleted file mode 100644 index f248ee243a6..00000000000 --- a/pkg/registry/apis/provisioning/repository/clone_fn_mock.go +++ /dev/null @@ -1,95 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package repository - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockCloneFn is an autogenerated mock type for the CloneFn type -type MockCloneFn struct { - mock.Mock -} - -type MockCloneFn_Expecter struct { - mock *mock.Mock -} - -func (_m *MockCloneFn) EXPECT() *MockCloneFn_Expecter { - return &MockCloneFn_Expecter{mock: &_m.Mock} -} - -// Execute provides a mock function with given fields: ctx, opts -func (_m *MockCloneFn) Execute(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Execute") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockCloneFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' -type MockCloneFn_Execute_Call struct { - *mock.Call -} - -// Execute is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockCloneFn_Expecter) Execute(ctx interface{}, opts interface{}) *MockCloneFn_Execute_Call { - return &MockCloneFn_Execute_Call{Call: _e.mock.On("Execute", ctx, opts)} -} - -func (_c *MockCloneFn_Execute_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockCloneFn_Execute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockCloneFn_Execute_Call) Return(_a0 ClonedRepository, _a1 error) *MockCloneFn_Execute_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockCloneFn_Execute_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockCloneFn_Execute_Call { - _c.Call.Return(run) - return _c -} - -// NewMockCloneFn creates a new instance of MockCloneFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockCloneFn(t interface { - mock.TestingT - Cleanup(func()) -}) *MockCloneFn { - mock := &MockCloneFn{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/clone_test.go b/pkg/registry/apis/provisioning/repository/clone_test.go deleted file mode 100644 index ac685386aac..00000000000 --- a/pkg/registry/apis/provisioning/repository/clone_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package repository - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type mockClonableRepo struct { - *MockClonableRepository - *MockClonedRepository -} - -func Test_WrapWithCloneAndPushIfPossible_NonClonableRepository(t *testing.T) { - nonClonable := NewMockRepository(t) - var called bool - fn := func(repo Repository, cloned bool) error { - called = true - return errors.New("operation failed") - } - - err := WrapWithCloneAndPushIfPossible(context.Background(), nonClonable, CloneOptions{}, PushOptions{}, fn) - require.EqualError(t, err, "operation failed") - require.True(t, called) -} - -func TestWrapWithCloneAndPushIfPossible(t *testing.T) { - tests := []struct { - name string - setupMocks func(t *testing.T) *mockClonableRepo - operation func(repo Repository, cloned bool) error - expectedError string - }{ - { - name: "successful clone, operation, and push", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(nil) - mockCloned.EXPECT().Remove(mock.Anything).Return(nil) - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - require.True(t, cloned) - return nil - }, - }, - { - name: "clone failure", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(nil, errors.New("clone failed")) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - } - }, - operation: func(repo Repository, cloned bool) error { - return nil - }, - expectedError: "clone repository: clone failed", - }, - { - name: "operation failure", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Remove(mock.Anything).Return(nil) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - return errors.New("operation failed") - }, - expectedError: "operation failed", - }, - { - name: "push failure", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(errors.New("push failed")) - mockCloned.EXPECT().Remove(mock.Anything).Return(nil) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - return nil - }, - expectedError: "push failed", - }, - { - name: "remove failure should only log", - setupMocks: func(t *testing.T) *mockClonableRepo { - mockRepo := NewMockClonableRepository(t) - mockCloned := NewMockClonedRepository(t) - - mockRepo.EXPECT().Clone(mock.Anything, CloneOptions{}).Return(mockCloned, nil) - mockCloned.EXPECT().Push(mock.Anything, PushOptions{}).Return(nil) - mockCloned.EXPECT().Remove(mock.Anything).Return(errors.New("remove failed")) - - return &mockClonableRepo{ - MockClonableRepository: mockRepo, - MockClonedRepository: mockCloned, - } - }, - operation: func(repo Repository, cloned bool) error { - return nil - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - repo := tt.setupMocks(t) - err := WrapWithCloneAndPushIfPossible(context.Background(), repo, CloneOptions{}, PushOptions{}, tt.operation) - - if tt.expectedError != "" { - require.EqualError(t, err, tt.expectedError) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/git/branch.go b/pkg/registry/apis/provisioning/repository/git/branch.go new file mode 100644 index 00000000000..28d4ec7b84c --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/git/branch.go @@ -0,0 +1,33 @@ +package git + +import ( + "regexp" + "strings" +) + +// basicGitBranchNameRegex is a regular expression to validate a git branch name +// it does not cover all cases as positive lookaheads are not supported in Go's regexp +var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`) + +// IsValidGitBranchName checks if a branch name is valid. +// It uses the following regexp `^[a-zA-Z0-9\-\_\/\.]+$` to validate the branch name with some additional checks that must satisfy the following rules: +// 1. The branch name must have at least one character and must not be empty. +// 2. The branch name cannot start with `/` or end with `/`, `.`, or whitespace. +// 3. The branch name cannot contain consecutive slashes (`//`). +// 4. The branch name cannot contain consecutive dots (`..`). +// 5. The branch name cannot contain `@{`. +// 6. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`. +func IsValidGitBranchName(branch string) bool { + if !basicGitBranchNameRegex.MatchString(branch) { + return false + } + + // Additional checks for invalid patterns + if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") || + strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") || + strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") { + return false + } + + return true +} diff --git a/pkg/registry/apis/provisioning/repository/git/branch_test.go b/pkg/registry/apis/provisioning/repository/git/branch_test.go new file mode 100644 index 00000000000..fd6d3259a81 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/git/branch_test.go @@ -0,0 +1,47 @@ +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidGitBranchName(t *testing.T) { + tests := []struct { + name string + branch string + expected bool + }{ + {"Valid branch name", "feature/add-tests", true}, + {"Valid branch name with numbers", "feature/123-add-tests", true}, + {"Valid branch name with dots", "feature.add.tests", true}, + {"Valid branch name with hyphens", "feature-add-tests", true}, + {"Valid branch name with underscores", "feature_add_tests", true}, + {"Valid branch name with mixed characters", "feature/add_tests-123", true}, + {"Starts with /", "/feature", false}, + {"Ends with /", "feature/", false}, + {"Ends with .", "feature.", false}, + {"Ends with space", "feature ", false}, + {"Contains consecutive slashes", "feature//branch", false}, + {"Contains consecutive dots", "feature..branch", false}, + {"Contains @{", "feature@{branch", false}, + {"Contains invalid character ~", "feature~branch", false}, + {"Contains invalid character ^", "feature^branch", false}, + {"Contains invalid character :", "feature:branch", false}, + {"Contains invalid character ?", "feature?branch", false}, + {"Contains invalid character *", "feature*branch", false}, + {"Contains invalid character [", "feature[branch", false}, + {"Contains invalid character ]", "feature]branch", false}, + {"Contains invalid character \\", "feature\\branch", false}, + {"Empty branch name", "", false}, + {"Only whitespace", " ", false}, + {"Single valid character", "a", true}, + {"Ends with .lock", "feature.lock", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsValidGitBranchName(tt.branch)) + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/git.go b/pkg/registry/apis/provisioning/repository/git/git.go similarity index 58% rename from pkg/registry/apis/provisioning/repository/git.go rename to pkg/registry/apis/provisioning/repository/git/git.go index 6a4244e76da..e7ef7ca4b38 100644 --- a/pkg/registry/apis/provisioning/repository/git.go +++ b/pkg/registry/apis/provisioning/repository/git/git.go @@ -1,15 +1,18 @@ -package repository +package git + +import "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" // GitRepository is an interface that combines all repository capabilities // needed for Git repositories. // //go:generate mockery --name GitRepository --structname MockGitRepository --inpackage --filename git_repository_mock.go --with-expecter type GitRepository interface { - Repository - Versioned - Writer - Reader - ClonableRepository + repository.Repository + repository.Versioned + repository.Writer + repository.Reader + repository.StageableRepository + repository.Hooks URL() string Branch() string } diff --git a/pkg/registry/apis/provisioning/repository/git_repository_mock.go b/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go similarity index 70% rename from pkg/registry/apis/provisioning/repository/git_repository_mock.go rename to pkg/registry/apis/provisioning/repository/git/git_repository_mock.go index 07ef1c73cba..d16a5450326 100644 --- a/pkg/registry/apis/provisioning/repository/git_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.52.4. DO NOT EDIT. -package repository +package git import ( context "context" @@ -8,6 +8,8 @@ import ( mock "github.com/stretchr/testify/mock" field "k8s.io/apimachinery/pkg/util/validation/field" + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) @@ -69,83 +71,24 @@ func (_c *MockGitRepository_Branch_Call) RunAndReturn(run func() string) *MockGi return _c } -// Clone provides a mock function with given fields: ctx, opts -func (_m *MockGitRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Clone") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockGitRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' -type MockGitRepository_Clone_Call struct { - *mock.Call -} - -// Clone is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockGitRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockGitRepository_Clone_Call { - return &MockGitRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)} -} - -func (_c *MockGitRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockGitRepository_Clone_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockGitRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockGitRepository_Clone_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockGitRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockGitRepository_Clone_Call { - _c.Call.Return(run) - return _c -} - // CompareFiles provides a mock function with given fields: ctx, base, ref -func (_m *MockGitRepository) CompareFiles(ctx context.Context, base string, ref string) ([]VersionedFileChange, error) { +func (_m *MockGitRepository) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) { ret := _m.Called(ctx, base, ref) if len(ret) == 0 { panic("no return value specified for CompareFiles") } - var r0 []VersionedFileChange + var r0 []repository.VersionedFileChange var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok { return rf(ctx, base, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok { r0 = rf(ctx, base, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]VersionedFileChange) + r0 = ret.Get(0).([]repository.VersionedFileChange) } } @@ -178,12 +121,12 @@ func (_c *MockGitRepository_CompareFiles_Call) Run(run func(ctx context.Context, return _c } -func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGitRepository_CompareFiles_Call { +func (_c *MockGitRepository_CompareFiles_Call) Return(_a0 []repository.VersionedFileChange, _a1 error) *MockGitRepository_CompareFiles_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGitRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]VersionedFileChange, error)) *MockGitRepository_CompareFiles_Call { +func (_c *MockGitRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockGitRepository_CompareFiles_Call { _c.Call.Return(run) return _c } @@ -450,24 +393,244 @@ func (_c *MockGitRepository_LatestRef_Call) RunAndReturn(run func(context.Contex return _c } +// ListRefs provides a mock function with given fields: ctx +func (_m *MockGitRepository) ListRefs(ctx context.Context) ([]v0alpha1.RefItem, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListRefs") + } + + var r0 []v0alpha1.RefItem + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]v0alpha1.RefItem, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []v0alpha1.RefItem); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]v0alpha1.RefItem) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGitRepository_ListRefs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRefs' +type MockGitRepository_ListRefs_Call struct { + *mock.Call +} + +// ListRefs is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGitRepository_Expecter) ListRefs(ctx interface{}) *MockGitRepository_ListRefs_Call { + return &MockGitRepository_ListRefs_Call{Call: _e.mock.On("ListRefs", ctx)} +} + +func (_c *MockGitRepository_ListRefs_Call) Run(run func(ctx context.Context)) *MockGitRepository_ListRefs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGitRepository_ListRefs_Call) Return(_a0 []v0alpha1.RefItem, _a1 error) *MockGitRepository_ListRefs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGitRepository_ListRefs_Call) RunAndReturn(run func(context.Context) ([]v0alpha1.RefItem, error)) *MockGitRepository_ListRefs_Call { + _c.Call.Return(run) + return _c +} + +// OnCreate provides a mock function with given fields: ctx +func (_m *MockGitRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OnCreate") + } + + var r0 []map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGitRepository_OnCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnCreate' +type MockGitRepository_OnCreate_Call struct { + *mock.Call +} + +// OnCreate is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGitRepository_Expecter) OnCreate(ctx interface{}) *MockGitRepository_OnCreate_Call { + return &MockGitRepository_OnCreate_Call{Call: _e.mock.On("OnCreate", ctx)} +} + +func (_c *MockGitRepository_OnCreate_Call) Run(run func(ctx context.Context)) *MockGitRepository_OnCreate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGitRepository_OnCreate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGitRepository_OnCreate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGitRepository_OnCreate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGitRepository_OnCreate_Call { + _c.Call.Return(run) + return _c +} + +// OnDelete provides a mock function with given fields: ctx +func (_m *MockGitRepository) OnDelete(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OnDelete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockGitRepository_OnDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDelete' +type MockGitRepository_OnDelete_Call struct { + *mock.Call +} + +// OnDelete is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGitRepository_Expecter) OnDelete(ctx interface{}) *MockGitRepository_OnDelete_Call { + return &MockGitRepository_OnDelete_Call{Call: _e.mock.On("OnDelete", ctx)} +} + +func (_c *MockGitRepository_OnDelete_Call) Run(run func(ctx context.Context)) *MockGitRepository_OnDelete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGitRepository_OnDelete_Call) Return(_a0 error) *MockGitRepository_OnDelete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockGitRepository_OnDelete_Call) RunAndReturn(run func(context.Context) error) *MockGitRepository_OnDelete_Call { + _c.Call.Return(run) + return _c +} + +// OnUpdate provides a mock function with given fields: ctx +func (_m *MockGitRepository) OnUpdate(ctx context.Context) ([]map[string]interface{}, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OnUpdate") + } + + var r0 []map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGitRepository_OnUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnUpdate' +type MockGitRepository_OnUpdate_Call struct { + *mock.Call +} + +// OnUpdate is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGitRepository_Expecter) OnUpdate(ctx interface{}) *MockGitRepository_OnUpdate_Call { + return &MockGitRepository_OnUpdate_Call{Call: _e.mock.On("OnUpdate", ctx)} +} + +func (_c *MockGitRepository_OnUpdate_Call) Run(run func(ctx context.Context)) *MockGitRepository_OnUpdate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGitRepository_OnUpdate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGitRepository_OnUpdate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGitRepository_OnUpdate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGitRepository_OnUpdate_Call { + _c.Call.Return(run) + return _c +} + // Read provides a mock function with given fields: ctx, path, ref -func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { +func (_m *MockGitRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { ret := _m.Called(ctx, path, ref) if len(ret) == 0 { panic("no return value specified for Read") } - var r0 *FileInfo + var r0 *repository.FileInfo var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok { return rf(ctx, path, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok { r0 = rf(ctx, path, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*FileInfo) + r0 = ret.Get(0).(*repository.FileInfo) } } @@ -500,34 +663,34 @@ func (_c *MockGitRepository_Read_Call) Run(run func(ctx context.Context, path st return _c } -func (_c *MockGitRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGitRepository_Read_Call { +func (_c *MockGitRepository_Read_Call) Return(_a0 *repository.FileInfo, _a1 error) *MockGitRepository_Read_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGitRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockGitRepository_Read_Call { +func (_c *MockGitRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.FileInfo, error)) *MockGitRepository_Read_Call { _c.Call.Return(run) return _c } // ReadTree provides a mock function with given fields: ctx, ref -func (_m *MockGitRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (_m *MockGitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { ret := _m.Called(ctx, ref) if len(ret) == 0 { panic("no return value specified for ReadTree") } - var r0 []FileTreeEntry + var r0 []repository.FileTreeEntry var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) ([]repository.FileTreeEntry, error)); ok { return rf(ctx, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) []repository.FileTreeEntry); ok { r0 = rf(ctx, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]FileTreeEntry) + r0 = ret.Get(0).([]repository.FileTreeEntry) } } @@ -559,12 +722,71 @@ func (_c *MockGitRepository_ReadTree_Call) Run(run func(ctx context.Context, ref return _c } -func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGitRepository_ReadTree_Call { +func (_c *MockGitRepository_ReadTree_Call) Return(_a0 []repository.FileTreeEntry, _a1 error) *MockGitRepository_ReadTree_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGitRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockGitRepository_ReadTree_Call { +func (_c *MockGitRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]repository.FileTreeEntry, error)) *MockGitRepository_ReadTree_Call { + _c.Call.Return(run) + return _c +} + +// Stage provides a mock function with given fields: ctx, opts +func (_m *MockGitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Stage") + } + + var r0 repository.StagedRepository + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) (repository.StagedRepository, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) repository.StagedRepository); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(repository.StagedRepository) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.StageOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGitRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage' +type MockGitRepository_Stage_Call struct { + *mock.Call +} + +// Stage is a helper method to define mock.On call +// - ctx context.Context +// - opts repository.StageOptions +func (_e *MockGitRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockGitRepository_Stage_Call { + return &MockGitRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)} +} + +func (_c *MockGitRepository_Stage_Call) Run(run func(ctx context.Context, opts repository.StageOptions)) *MockGitRepository_Stage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.StageOptions)) + }) + return _c +} + +func (_c *MockGitRepository_Stage_Call) Return(_a0 repository.StagedRepository, _a1 error) *MockGitRepository_Stage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGitRepository_Stage_Call) RunAndReturn(run func(context.Context, repository.StageOptions) (repository.StagedRepository, error)) *MockGitRepository_Stage_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/repository/git/mutator.go b/pkg/registry/apis/provisioning/repository/git/mutator.go new file mode 100644 index 00000000000..a67f473b82d --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/git/mutator.go @@ -0,0 +1,36 @@ +package git + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" +) + +func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { + return func(ctx context.Context, obj runtime.Object) error { + repo, ok := obj.(*provisioning.Repository) + if !ok { + return nil + } + + if repo.Spec.Git == nil { + return nil + } + + if repo.Spec.Git.Token != "" { + secretName := repo.Name + gitTokenSecretSuffix + nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.Git.Token) + if err != nil { + return err + } + repo.Spec.Git.EncryptedToken = nameOrValue + repo.Spec.Git.Token = "" + } + + return nil + } +} diff --git a/pkg/registry/apis/provisioning/repository/git/mutator_test.go b/pkg/registry/apis/provisioning/repository/git/mutator_test.go new file mode 100644 index 00000000000..f924b1b2146 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/git/mutator_test.go @@ -0,0 +1,159 @@ +package git + +import ( + "context" + "errors" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestMutator(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + token string + setupMocks func(*secrets.MockRepositorySecrets) + expectedToken string + expectedEncryptedToken string + expectedError string + }{ + { + name: "successful token encryption", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + Git: &provisioning.GitRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Encrypt( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + Git: &provisioning.GitRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + "test-repo"+gitTokenSecretSuffix, + "secret-token", + ).Return([]byte("encrypted-token"), nil) + }, + expectedToken: "", + expectedEncryptedToken: "encrypted-token", + }, + { + name: "encryption error", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + Git: &provisioning.GitRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Encrypt( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + Git: &provisioning.GitRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + "test-repo"+gitTokenSecretSuffix, + "secret-token", + ).Return(nil, errors.New("encryption failed")) + }, + expectedError: "encryption failed", + }, + { + name: "no git spec", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + Git: nil, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + { + name: "empty token", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + Git: &provisioning.GitRepositoryConfig{ + Token: "", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + { + name: "non-repository object", + obj: &runtime.Unknown{}, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMocks(mockSecrets) + + mutator := Mutator(mockSecrets) + err := mutator(context.Background(), tt.obj) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + + // Check that token was cleared and encrypted token was set + if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.Git != nil { + if tt.expectedEncryptedToken != "" { + // Token should be cleared after encryption + assert.Empty(t, repo.Spec.Git.Token, "Token should be cleared after encryption") + // EncryptedToken should be set to the expected value + assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.Git.EncryptedToken), "EncryptedToken should match expected value") + } + } + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/nanogit/git.go b/pkg/registry/apis/provisioning/repository/git/repository.go similarity index 88% rename from pkg/registry/apis/provisioning/repository/nanogit/git.go rename to pkg/registry/apis/provisioning/repository/git/repository.go index ce1773e787a..2b252ca9c09 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/git.go +++ b/pkg/registry/apis/provisioning/repository/git/repository.go @@ -1,12 +1,14 @@ -package nanogit +package git import ( + "bytes" "context" "errors" "fmt" "log/slog" "net/http" "net/url" + "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -25,6 +27,9 @@ import ( "github.com/grafana/nanogit/protocol/hash" ) +//nolint:gosec // This is a constant for a secret suffix +const gitTokenSecretSuffix = "-git-token" + type RepositoryConfig struct { URL string Branch string @@ -38,28 +43,21 @@ type gitRepository struct { config *provisioning.Repository gitConfig RepositoryConfig client nanogit.Client - secrets secrets.Service + secrets secrets.RepositorySecrets } func NewGitRepository( ctx context.Context, - secrets secrets.Service, config *provisioning.Repository, gitConfig RepositoryConfig, -) (repository.GitRepository, error) { - if gitConfig.Token == "" { - decrypted, err := secrets.Decrypt(ctx, gitConfig.EncryptedToken) - if err != nil { - return nil, fmt.Errorf("decrypt token: %w", err) - } - gitConfig.Token = string(decrypted) + secrets secrets.RepositorySecrets, +) (GitRepository, error) { + var opts []options.Option + if len(gitConfig.Token) > 0 { + opts = append(opts, options.WithBasicAuth("git", gitConfig.Token)) } - // Create nanogit client with authentication - client, err := nanogit.NewHTTPClient( - gitConfig.URL, - options.WithBasicAuth("git", gitConfig.Token), - ) + client, err := nanogit.NewHTTPClient(gitConfig.URL, opts...) if err != nil { return nil, fmt.Errorf("create nanogit client: %w", err) } @@ -98,12 +96,15 @@ func (r *gitRepository) Validate() (list field.ErrorList) { } if cfg.Branch == "" { list = append(list, field.Required(field.NewPath("spec", t, "branch"), "a git branch is required")) - } else if !repository.IsValidGitBranchName(cfg.Branch) { + } else if !IsValidGitBranchName(cfg.Branch) { list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), cfg.Branch, "invalid branch name")) } - if cfg.Token == "" && len(cfg.EncryptedToken) == 0 { - list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required")) + // If the repository has workflows, we require a token or encrypted token + if len(r.config.Spec.Workflows) > 0 { + if cfg.Token == "" && len(cfg.EncryptedToken) == 0 { + list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required")) + } } if err := safepath.IsSafe(cfg.Path); err != nil { @@ -412,11 +413,15 @@ func (r *gitRepository) Write(ctx context.Context, path string, ref string, data } ctx, _ = r.logger(ctx, ref) - _, err := r.Read(ctx, path, ref) + info, err := r.Read(ctx, path, ref) if err != nil && !(errors.Is(err, repository.ErrFileNotFound)) { return fmt.Errorf("check if file exists before writing: %w", err) } if err == nil { + // If the value already exists and is the same, we don't need to do anything + if bytes.Equal(info.Data, data) { + return nil + } return r.Update(ctx, path, ref, data, message) } @@ -450,7 +455,8 @@ func (r *gitRepository) delete(ctx context.Context, path string, writer nanogit. finalPath := safepath.Join(r.gitConfig.Path, path) // Check if it's a directory - use DeleteTree for directories, DeleteBlob for files if safepath.IsDir(path) { - if _, err := writer.DeleteTree(ctx, finalPath); err != nil { + trimmed := strings.TrimSuffix(finalPath, "/") + if _, err := writer.DeleteTree(ctx, trimmed); err != nil { if errors.Is(err, nanogit.ErrObjectNotFound) { return repository.ErrFileNotFound } @@ -477,6 +483,27 @@ func (r *gitRepository) History(_ context.Context, _ string, _ string) ([]provis }} } +func (r *gitRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) { + refs, err := r.client.ListRefs(ctx) + if err != nil { + return nil, fmt.Errorf("list refs: %w", err) + } + refItems := make([]provisioning.RefItem, 0, len(refs)) + for _, ref := range refs { + // Only branches + if !strings.HasPrefix(ref.Name, "refs/heads/") { + continue + } + + refItems = append(refItems, provisioning.RefItem{ + Name: strings.TrimPrefix(ref.Name, "refs/heads/"), + Hash: ref.Hash.String(), + }) + } + + return refItems, nil +} + func (r *gitRepository) LatestRef(ctx context.Context) (string, error) { ctx, _ = r.logger(ctx, "") branchRef, err := r.client.GetRef(ctx, fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch)) @@ -582,7 +609,7 @@ func (r *gitRepository) CompareFiles(ctx context.Context, base, ref string) ([]r return changes, nil } -func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { +func (r *gitRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { return NewStagedGitRepository(ctx, r, opts) } @@ -590,23 +617,26 @@ func (r *gitRepository) Clone(ctx context.Context, opts repository.CloneOptions) func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash.Hash, error) { // Use default branch if ref is empty if ref == "" { - ref = fmt.Sprintf("refs/heads/%s", r.gitConfig.Branch) + ref = r.gitConfig.Branch } // Try to parse ref as a hash first refHash, err := hash.FromHex(ref) - if err == nil && refHash != nil { + if err == nil && refHash != hash.Zero { // Valid hash, return it return refHash, nil } + // Prefix ref with refs/heads/ + ref = fmt.Sprintf("refs/heads/%s", ref) + // Not a valid hash, try to resolve as a branch reference branchRef, err := r.client.GetRef(ctx, ref) if err != nil { if errors.Is(err, nanogit.ErrObjectNotFound) { - return nil, fmt.Errorf("ref not found: %s: %w", ref, repository.ErrRefNotFound) + return hash.Zero, fmt.Errorf("ref not found: %s: %w", ref, repository.ErrRefNotFound) } - return nil, fmt.Errorf("get ref %s: %w", ref, err) + return hash.Zero, fmt.Errorf("get ref %s: %w", ref, err) } return branchRef.Hash, nil @@ -615,7 +645,7 @@ func (r *gitRepository) resolveRefToHash(ctx context.Context, ref string) (hash. // ensureBranchExists checks if a branch exists and creates it if it doesn't, // returning the branch reference to avoid duplicate GetRef calls func (r *gitRepository) ensureBranchExists(ctx context.Context, branchName string) (nanogit.Ref, error) { - if !repository.IsValidGitBranchName(branchName) { + if !IsValidGitBranchName(branchName) { return nanogit.Ref{}, &apierrors.StatusError{ ErrStatus: metav1.Status{ Code: http.StatusBadRequest, @@ -730,3 +760,23 @@ func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context return ctx, logger } + +func (r *gitRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) { + return nil, nil +} + +func (r *gitRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) { + return nil, nil +} + +func (r *gitRepository) OnDelete(ctx context.Context) error { + logger := logging.FromContext(ctx) + secretName := r.config.Name + gitTokenSecretSuffix + if err := r.secrets.Delete(ctx, r.config, secretName); err != nil { + return fmt.Errorf("delete git token secret: %w", err) + } + + logger.Info("Deleted git token secret", "secretName", secretName) + + return nil +} diff --git a/pkg/registry/apis/provisioning/repository/nanogit/git_test.go b/pkg/registry/apis/provisioning/repository/git/repository_test.go similarity index 88% rename from pkg/registry/apis/provisioning/repository/nanogit/git_test.go rename to pkg/registry/apis/provisioning/repository/git/repository_test.go index 20df321fc74..0f13c2f9854 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/git_test.go +++ b/pkg/registry/apis/provisioning/repository/git/repository_test.go @@ -1,4 +1,4 @@ -package nanogit +package git import ( "context" @@ -7,6 +7,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" @@ -14,10 +19,6 @@ import ( "github.com/grafana/nanogit/mocks" "github.com/grafana/nanogit/protocol" "github.com/grafana/nanogit/protocol/hash" - "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/validation/field" ) func TestGitRepository_Validate(t *testing.T) { @@ -138,10 +139,11 @@ func TestGitRepository_Validate(t *testing.T) { }, }, { - name: "missing token", + name: "missing token for R/W repository", config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ - Type: "test_type", + Type: "test_type", + Workflows: []provisioning.Workflow{provisioning.WriteWorkflow}, }, }, gitConfig: RepositoryConfig{ @@ -153,6 +155,21 @@ func TestGitRepository_Validate(t *testing.T) { field.Required(field.NewPath("spec", "test_type", "token"), "a git access token is required"), }, }, + { + name: "missing token for read-only repository", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Type: "test_type", + Workflows: nil, // read-only + }, + }, + gitConfig: RepositoryConfig{ + URL: "https://git.example.com/repo.git", + Branch: "main", + Token: "", // Empty token + }, + want: nil, + }, { name: "unsafe path", config: &provisioning.Repository{ @@ -258,20 +275,8 @@ func TestIsValidGitURL(t *testing.T) { } } -// Mock secrets service for testing -type mockSecretsService struct{} - -func (m *mockSecretsService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - return []byte("decrypted-token"), nil -} - -func (m *mockSecretsService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - return []byte("encrypted-token"), nil -} - func TestNewGit(t *testing.T) { ctx := context.Background() - mockSecrets := &mockSecretsService{} config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -286,9 +291,10 @@ func TestNewGit(t *testing.T) { Path: "configs", } + mockSecrets := secrets.NewMockRepositorySecrets(t) // This should succeed in creating the client but won't be able to connect // We just test that the basic structure is created correctly - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig) + gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets) require.NoError(t, err) require.NotNil(t, gitRepo) require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL()) @@ -695,7 +701,7 @@ func TestGitRepository_Read(t *testing.T) { Hash: hash.Hash{}, }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash([]byte("tree-hash")), + Tree: hash.MustFromHex("abcdef1234567890abcdef1234567890abcdef12"), }, nil) mockClient.GetTreeByPathReturns(&nanogit.Tree{ Hash: hash.Hash{}, @@ -1073,27 +1079,27 @@ func TestGitRepository_Update(t *testing.T) { func TestGitRepository_Delete(t *testing.T) { tests := []struct { - name string - setupMock func(*mocks.FakeClient) - gitConfig RepositoryConfig - path string - ref string - comment string - wantError bool - errorType error + name string + setupMock func(*mocks.FakeClient, *mocks.FakeStagedWriter) + assertions func(*testing.T, *mocks.FakeClient, *mocks.FakeStagedWriter) + gitConfig RepositoryConfig + path string + ref string + comment string + wantError bool + errorType error }{ { name: "success - delete file", - setupMock: func(mockClient *mocks.FakeClient) { + setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", Hash: hash.Hash{}, }, nil) - mockWriter := &mocks.FakeStagedWriter{} + mockWriter.DeleteBlobReturns(hash.Hash{}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) - mockClient.NewStagedWriterReturns(mockWriter, nil) }, gitConfig: RepositoryConfig{ Branch: "main", @@ -1106,16 +1112,19 @@ func TestGitRepository_Delete(t *testing.T) { }, { name: "success - delete directory", - setupMock: func(mockClient *mocks.FakeClient) { + setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", Hash: hash.Hash{}, }, nil) - mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteTreeReturns(hash.Hash{}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) - mockClient.NewStagedWriterReturns(mockWriter, nil) + }, + assertions: func(t *testing.T, fakeClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { + require.Equal(t, 1, mockWriter.DeleteTreeCallCount(), "DeleteTree should be called once") + _, p := mockWriter.DeleteTreeArgsForCall(0) + require.Equal(t, "configs/testdir", p, "DeleteTree should be called with correct path") }, gitConfig: RepositoryConfig{ Branch: "main", @@ -1128,14 +1137,12 @@ func TestGitRepository_Delete(t *testing.T) { }, { name: "failure - file not found", - setupMock: func(mockClient *mocks.FakeClient) { + setupMock: func(mockClient *mocks.FakeClient, mockWriter *mocks.FakeStagedWriter) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", Hash: hash.Hash{}, }, nil) - mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound) - mockClient.NewStagedWriterReturns(mockWriter, nil) }, gitConfig: RepositoryConfig{ Branch: "main", @@ -1152,7 +1159,10 @@ func TestGitRepository_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mocks.FakeClient{} - tt.setupMock(mockClient) + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + tt.setupMock(mockClient, mockWriter) gitRepo := &gitRepository{ client: mockClient, @@ -1174,6 +1184,101 @@ func TestGitRepository_Delete(t *testing.T) { } else { require.NoError(t, err) } + + if tt.assertions != nil { + tt.assertions(t, mockClient, mockWriter) + } + }) + } +} + +func TestGitRepository_ListRefs(t *testing.T) { + tests := []struct { + name string + setupMock func(*mocks.FakeClient) + gitConfig RepositoryConfig + wantError bool + wantRefs []provisioning.RefItem + errorType error + }{ + { + name: "success - list refs", + setupMock: func(mockClient *mocks.FakeClient) { + mockClient.ListRefsReturns([]nanogit.Ref{ + { + Name: "refs/heads/main", + Hash: hash.MustFromHex("abcdef1234567890abcdef1234567890abcdef12"), + }, + { + Name: "refs/heads/feature", + Hash: hash.MustFromHex("1234567890abcdef1234567890abcdef12345678"), + }, + { + Name: "refs/tags/v1.0.0", + Hash: hash.MustFromHex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), + }, + }, nil) + }, + gitConfig: RepositoryConfig{ + Branch: "main", + }, + wantError: false, + wantRefs: []provisioning.RefItem{ + { + Name: "main", + Hash: "abcdef1234567890abcdef1234567890abcdef12", + }, + { + Name: "feature", + Hash: "1234567890abcdef1234567890abcdef12345678", + }, + }, + }, + { + name: "failure - list refs error", + setupMock: func(mockClient *mocks.FakeClient) { + mockClient.ListRefsReturns(nil, errors.New("list refs failed")) + }, + gitConfig: RepositoryConfig{ + Branch: "main", + }, + wantError: true, + errorType: errors.New("list refs failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &mocks.FakeClient{} + tt.setupMock(mockClient) + + gitRepo := &gitRepository{ + client: mockClient, + gitConfig: tt.gitConfig, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitHubRepositoryType, + }, + }, + } + + refs, err := gitRepo.ListRefs(context.Background()) + + if tt.wantError { + require.Error(t, err) + if tt.errorType != nil { + require.Contains(t, err.Error(), tt.errorType.Error()) + } + require.Nil(t, refs) + } else { + require.NoError(t, err) + require.NotNil(t, refs) + require.Equal(t, len(tt.wantRefs), len(refs)) + for i, wantRef := range tt.wantRefs { + require.Equal(t, wantRef.Name, refs[i].Name) + require.Equal(t, wantRef.Hash, refs[i].Hash) + } + } }) } } @@ -1191,7 +1296,7 @@ func TestGitRepository_LatestRef(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, // Non-empty hash + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, // Non-empty hash }, nil) }, gitConfig: RepositoryConfig{ @@ -1412,11 +1517,11 @@ func TestGitRepository_CompareFiles(t *testing.T) { // Return refs for base and ref mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) // Return comparison results @@ -1449,11 +1554,11 @@ func TestGitRepository_CompareFiles(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{}, nil) @@ -1547,7 +1652,7 @@ func TestGitRepository_ensureBranchExists(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) }, gitConfig: RepositoryConfig{ @@ -1565,7 +1670,7 @@ func TestGitRepository_ensureBranchExists(t *testing.T) { // Second call - get source branch mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) // CreateRef succeeds mockClient.CreateRefReturns(nil) @@ -1729,57 +1834,21 @@ func TestGitRepository_createSignature(t *testing.T) { func TestNewGitRepository(t *testing.T) { tests := []struct { - name string - setupMock func(*mockSecretsService) - gitConfig RepositoryConfig - wantError bool - expectURL string - expectToken string + name string + gitConfig RepositoryConfig + wantError bool + expectURL string }{ { name: "success - with token", - setupMock: func(mockSecrets *mockSecretsService) { - // No setup needed for token case - }, gitConfig: RepositoryConfig{ URL: "https://git.example.com/owner/repo.git", Branch: "main", Token: "plain-token", Path: "configs", }, - wantError: false, - expectURL: "https://git.example.com/owner/repo.git", - expectToken: "plain-token", - }, - { - name: "success - with encrypted token", - setupMock: func(mockSecrets *mockSecretsService) { - // Mock will return decrypted token - }, - gitConfig: RepositoryConfig{ - URL: "https://git.example.com/owner/repo.git", - Branch: "main", - Token: "", // Empty token, will use encrypted - EncryptedToken: []byte("encrypted-token"), - Path: "configs", - }, - wantError: false, - expectURL: "https://git.example.com/owner/repo.git", - expectToken: "decrypted-token", // From mock - }, - { - name: "failure - decryption error", - setupMock: func(mockSecrets *mockSecretsService) { - // This test will use the separate mockSecretsServiceWithError - }, - gitConfig: RepositoryConfig{ - URL: "https://git.example.com/owner/repo.git", - Branch: "main", - Token: "", - EncryptedToken: []byte("bad-encrypted-token"), - Path: "configs", - }, - wantError: true, + wantError: false, + expectURL: "https://git.example.com/owner/repo.git", }, } @@ -1787,20 +1856,14 @@ func TestNewGitRepository(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - var mockSecrets secrets.Service - if tt.name == "failure - decryption error" { - mockSecrets = &mockSecretsServiceWithError{shouldError: true} - } else { - mockSecrets = &mockSecretsService{} - } - config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ Type: provisioning.GitHubRepositoryType, }, } - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, tt.gitConfig) + mockSecrets := secrets.NewMockRepositorySecrets(t) + gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig, mockSecrets) if tt.wantError { require.Error(t, err) @@ -1862,7 +1925,7 @@ func TestGitRepository_resolveRefToHash(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) }, gitConfig: RepositoryConfig{ @@ -1887,7 +1950,7 @@ func TestGitRepository_resolveRefToHash(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) }, gitConfig: RepositoryConfig{ @@ -1939,7 +2002,7 @@ func TestGitRepository_resolveRefToHash(t *testing.T) { if tt.wantError { require.Error(t, err) - require.Nil(t, refHash) + require.Equal(t, hash.Zero, refHash) } else { require.NoError(t, err) require.NotNil(t, refHash) @@ -2160,15 +2223,23 @@ func TestGitRepository_logger(t *testing.T) { // First call creates the logger context ctx1, logger1 := gitRepo.logger(ctx, "branch1") - // Second call should return the existing logger + // Second call should return the existing logger context ctx2, logger2 := gitRepo.logger(ctx1, "branch2") + // When logger context already exists, it should return the same context require.Equal(t, ctx1, ctx2) - require.Equal(t, logger1, logger2) + + // The logger should be the same instance from the existing context + require.NotNil(t, logger1) + require.NotNil(t, logger2) + + // Both loggers should be functionally equivalent since they come from the same context + // We verify this by checking that they produce the same output + require.IsType(t, logger1, logger2) }) } -func TestGitRepository_Clone(t *testing.T) { +func TestGitRepository_Stage(t *testing.T) { gitRepo := &gitRepository{ config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -2184,9 +2255,8 @@ func TestGitRepository_Clone(t *testing.T) { t.Run("calls NewStagedGitRepository", func(t *testing.T) { ctx := context.Background() - opts := repository.CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, + opts := repository.StageOptions{ + PushOnWrites: true, } // Since NewStagedGitRepository is not mocked and may panic, we expect this to fail @@ -2198,11 +2268,11 @@ func TestGitRepository_Clone(t *testing.T) { } }() - cloned, err := gitRepo.Clone(ctx, opts) + staged, err := gitRepo.Stage(ctx, opts) // This will likely error/panic since we don't have a real implementation // but we're testing that the method exists and forwards to NewStagedGitRepository - _ = cloned + _ = staged _ = err }) } @@ -2212,7 +2282,7 @@ func TestGitRepository_EdgeCases(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockWriter := &mocks.FakeStagedWriter{} @@ -2251,7 +2321,7 @@ func TestGitRepository_EdgeCases(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetCommitReturns(&nanogit.Commit{Tree: hash.Hash{}}, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{}, errors.New("some read error")) @@ -2271,50 +2341,6 @@ func TestGitRepository_EdgeCases(t *testing.T) { }) } -// Enhanced secrets service mock with error handling -type mockSecretsServiceWithError struct { - shouldError bool -} - -func (m *mockSecretsServiceWithError) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - if m.shouldError { - return nil, errors.New("decryption failed") - } - return []byte("decrypted-token"), nil -} - -func (m *mockSecretsServiceWithError) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - if m.shouldError { - return nil, errors.New("encryption failed") - } - return []byte("encrypted-token"), nil -} - -func TestNewGitRepository_DecryptionError(t *testing.T) { - ctx := context.Background() - mockSecrets := &mockSecretsServiceWithError{shouldError: true} - - config := &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - }, - } - - gitConfig := RepositoryConfig{ - URL: "https://git.example.com/owner/repo.git", - Branch: "main", - Token: "", - EncryptedToken: []byte("encrypted-token"), - Path: "configs", - } - - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig) - - require.Error(t, err) - require.Nil(t, gitRepo) - require.Contains(t, err.Error(), "decrypt token") -} - func TestGitRepository_ValidateBranchNames(t *testing.T) { tests := []struct { name string @@ -2401,7 +2427,7 @@ func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/invalid-hex", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) }, ref: "invalid-hex-zzz", @@ -2412,7 +2438,7 @@ func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) }, ref: "refs/heads/feature", @@ -2423,7 +2449,7 @@ func TestGitRepository_ResolveRefToHash_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/tags/v1.0.0", - Hash: hash.Hash{7, 8, 9}, + Hash: hash.Hash{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, }, nil) }, ref: "refs/tags/v1.0.0", @@ -2540,11 +2566,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns(nil, errors.New("compare error")) }, @@ -2558,11 +2584,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -2580,11 +2606,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -2606,11 +2632,11 @@ func TestGitRepository_CompareFiles_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -2683,7 +2709,7 @@ func TestGitRepository_ReadTree_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetFlatTreeReturns(nil, errors.New("flat tree error")) }, @@ -2694,23 +2720,23 @@ func TestGitRepository_ReadTree_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.GetFlatTreeReturns(&nanogit.FlatTree{ Entries: []nanogit.FlatTreeEntry{ { Path: "other/file.yaml", // Outside configs/ - Hash: hash.Hash{4, 5, 6}, + Hash: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Type: protocol.ObjectTypeBlob, }, { Path: "configs/included.yaml", // Inside configs/ - Hash: hash.Hash{7, 8, 9}, + Hash: hash.Hash{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, Type: protocol.ObjectTypeBlob, }, { Path: "configs/dir", // Directory without trailing slash - Hash: hash.Hash{10, 11, 12}, + Hash: hash.Hash{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}, Type: protocol.ObjectTypeTree, }, }, @@ -2782,7 +2808,6 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) { // This test would require mocking nanogit.NewHTTPClient which is difficult // We test the path where the client creation would fail by using invalid URL ctx := context.Background() - mockSecrets := &mockSecretsService{} config := &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -2797,7 +2822,8 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) { Path: "configs", } - gitRepo, err := NewGitRepository(ctx, mockSecrets, config, gitConfig) + mockSecrets := secrets.NewMockRepositorySecrets(t) + gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets) // We expect this to fail during client creation require.Error(t, err) @@ -2828,7 +2854,7 @@ func TestGitRepository_ensureBranchExists_ErrorConditions(t *testing.T) { // Second call - get source branch mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) // CreateRef fails mockClient.CreateRefReturns(errors.New("create ref failed")) @@ -2880,10 +2906,10 @@ func TestGitRepository_Read_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121315"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash([]byte("tree-hash")), + Tree: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetTreeByPathReturns(nil, errors.New("tree error")) }, @@ -2895,7 +2921,7 @@ func TestGitRepository_Read_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(nil, errors.New("commit error")) }, @@ -2907,10 +2933,10 @@ func TestGitRepository_Read_EdgeCases(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708092a0b0c0d0e0f1011121314"), }, nil) mockClient.GetBlobByPathReturns(nil, errors.New("blob error")) }, @@ -2965,7 +2991,7 @@ func TestGitRepository_Create_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error")) }, @@ -2977,7 +3003,7 @@ func TestGitRepository_Create_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob error")) @@ -3030,7 +3056,7 @@ func TestGitRepository_Update_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error")) }, @@ -3042,7 +3068,7 @@ func TestGitRepository_Update_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob error")) @@ -3096,7 +3122,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("staged writer error")) }, @@ -3109,7 +3135,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob error")) @@ -3124,7 +3150,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteTreeReturns(hash.Hash{}, errors.New("delete tree error")) @@ -3139,7 +3165,7 @@ func TestGitRepository_Delete_ErrorConditions(t *testing.T) { setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.DeleteTreeReturns(hash.Hash{}, nanogit.ErrObjectNotFound) @@ -3186,7 +3212,7 @@ func TestGitRepository_CompareFiles_EmptyBase(t *testing.T) { // Only setup for ref resolution mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -3218,8 +3244,8 @@ func TestGitRepository_CompareFiles_EmptyBase(t *testing.T) { // Verify CompareCommits was called with empty base hash and feature hash require.Equal(t, 1, mockClient.CompareCommitsCallCount()) _, baseHash, refHash := mockClient.CompareCommitsArgsForCall(0) - require.Nil(t, baseHash) // Empty hash for empty base - require.Equal(t, hash.Hash{4, 5, 6}, refHash) + require.Equal(t, hash.Zero, baseHash) // Empty hash for empty base + require.Equal(t, hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), refHash) } func TestGitRepository_EmptyRefHandling(t *testing.T) { @@ -3237,7 +3263,7 @@ func TestGitRepository_EmptyRefHandling(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} @@ -3299,7 +3325,7 @@ func TestGitRepository_CompareFiles_ResolveErrors(t *testing.T) { // First call succeeds for base mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) // Second call fails for ref mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, errors.New("ref error")) @@ -3341,14 +3367,14 @@ func TestGitRepository_Read_EmptyRef(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Tree: hash.Hash{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, }, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{ Content: []byte("file content"), - Hash: hash.Hash{7, 8, 9}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) gitRepo := &gitRepository{ @@ -3376,13 +3402,13 @@ func TestGitRepository_ReadTree_EmptyRef(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetFlatTreeReturns(&nanogit.FlatTree{ Entries: []nanogit.FlatTreeEntry{ { Path: "configs/test.yaml", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), Type: protocol.ObjectTypeBlob, }, }, @@ -3504,17 +3530,17 @@ func TestGitRepository_Write_DefaultRef(t *testing.T) { // First call for Read check - file not found mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{}, nanogit.ErrObjectNotFound) // Second call for Create mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockWriter := &mocks.FakeStagedWriter{} mockWriter.CreateBlobReturns(hash.Hash{}, nil) @@ -3545,14 +3571,14 @@ func TestGitRepository_Read_RefInFileInfo(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetBlobByPathReturns(&nanogit.Blob{ Content: []byte("file content"), - Hash: hash.Hash{7, 8, 9}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) gitRepo := &gitRepository{ @@ -3582,10 +3608,10 @@ func TestGitRepository_Read_GetTreeByPath_NotFound(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetCommitReturns(&nanogit.Commit{ - Tree: hash.Hash([]byte("tree-hash")), + Tree: hash.Hash{30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}, }, nil) mockClient.GetTreeByPathReturns(nil, nanogit.ErrObjectNotFound) @@ -3614,7 +3640,7 @@ func TestGitRepository_ReadTree_GetFlatTree_NotFound(t *testing.T) { mockClient := &mocks.FakeClient{} mockClient.GetRefReturns(nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetFlatTreeReturns(nil, nanogit.ErrObjectNotFound) @@ -3655,11 +3681,11 @@ func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *te mockClient := &mocks.FakeClient{} mockClient.GetRefReturnsOnCall(0, nanogit.Ref{ Name: "refs/heads/main", - Hash: hash.Hash{1, 2, 3}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), }, nil) mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ Name: "refs/heads/feature", - Hash: hash.Hash{4, 5, 6}, + Hash: hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121315"), }, nil) mockClient.CompareCommitsReturns([]nanogit.CommitFile{ { @@ -3710,3 +3736,79 @@ func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *te }) } } + +func TestGitRepository_OnDelete(t *testing.T) { + tests := []struct { + name string + setupMock func(*secrets.MockRepositorySecrets) + config *provisioning.Repository + expectedError string + }{ + { + name: "successful secret deletion", + setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Delete( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + "test-repo"+gitTokenSecretSuffix, + ).Return(nil) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + }, + { + name: "secret deletion error", + setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Delete( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + "test-repo"+gitTokenSecretSuffix, + ).Return(errors.New("failed to delete secret")) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + expectedError: "delete git token secret: failed to delete secret", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMock(mockSecrets) + + gitRepo := &gitRepository{ + config: tt.config, + secrets: mockSecrets, + } + + err := gitRepo.OnDelete(context.Background()) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + } + + mockSecrets.AssertExpectations(t) + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/nanogit/staged.go b/pkg/registry/apis/provisioning/repository/git/staged.go similarity index 86% rename from pkg/registry/apis/provisioning/repository/nanogit/staged.go rename to pkg/registry/apis/provisioning/repository/git/staged.go index d383e5b47e3..e11044230b2 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/staged.go +++ b/pkg/registry/apis/provisioning/repository/git/staged.go @@ -1,4 +1,4 @@ -package nanogit +package git import ( "context" @@ -15,17 +15,11 @@ import ( // once that happens we could do more magic here. type stagedGitRepository struct { *gitRepository - opts repository.CloneOptions + opts repository.StageOptions writer nanogit.StagedWriter } -func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repository.CloneOptions) (repository.ClonedRepository, error) { - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return nil, err - } - } - +func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repository.StageOptions) (repository.StagedRepository, error) { if opts.Timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, opts.Timeout) @@ -89,28 +83,35 @@ func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil } +func (r *stagedGitRepository) blobExists(ctx context.Context, path string) (bool, error) { + if r.gitConfig.Path != "" { + path = safepath.Join(r.gitConfig.Path, path) + } + return r.writer.BlobExists(ctx, path) +} + func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data []byte, message string) error { if ref != "" && ref != r.gitConfig.Branch { return errors.New("ref is not supported for staged repository") } - ok, err := r.writer.BlobExists(ctx, path) + exists, err := r.blobExists(ctx, path) if err != nil { return fmt.Errorf("check if file exists: %w", err) } - if !ok { - if err := r.create(ctx, path, data, r.writer); err != nil { + if exists { + if err := r.update(ctx, path, data, r.writer); err != nil { return err } } else { - if err := r.update(ctx, path, data, r.writer); err != nil { + if err := r.create(ctx, path, data, r.writer); err != nil { return err } } @@ -120,7 +121,7 @@ func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil @@ -144,7 +145,7 @@ func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil @@ -164,22 +165,16 @@ func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message str } if r.opts.PushOnWrites { - return r.Push(ctx, repository.PushOptions{}) + return r.Push(ctx) } return nil } -func (r *stagedGitRepository) Push(ctx context.Context, opts repository.PushOptions) error { - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return err - } - } - - if opts.Timeout > 0 { +func (r *stagedGitRepository) Push(ctx context.Context) error { + if r.opts.Timeout > 0 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, opts.Timeout) + ctx, cancel = context.WithTimeout(ctx, r.opts.Timeout) defer cancel() } diff --git a/pkg/registry/apis/provisioning/repository/nanogit/staged_test.go b/pkg/registry/apis/provisioning/repository/git/staged_test.go similarity index 88% rename from pkg/registry/apis/provisioning/repository/nanogit/staged_test.go rename to pkg/registry/apis/provisioning/repository/git/staged_test.go index 77f63016a66..9dc502cf8c2 100644 --- a/pkg/registry/apis/provisioning/repository/nanogit/staged_test.go +++ b/pkg/registry/apis/provisioning/repository/git/staged_test.go @@ -1,4 +1,4 @@ -package nanogit +package git import ( "context" @@ -18,7 +18,7 @@ func TestNewStagedGitRepository(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeClient) - opts repository.CloneOptions + opts repository.StageOptions wantError error }{ { @@ -31,9 +31,8 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: nil, }, @@ -47,12 +46,8 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, - BeforeFn: func() error { - return nil - }, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: nil, }, @@ -66,33 +61,19 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, - Timeout: time.Second * 5, + opts: repository.StageOptions{ + PushOnWrites: false, + Timeout: time.Second * 5, }, wantError: nil, }, - { - name: "fails with BeforeFn error", - setupMock: func(mockClient *mocks.FakeClient) { - // No setup needed as BeforeFn fails first - }, - opts: repository.CloneOptions{ - BeforeFn: func() error { - return errors.New("before function failed") - }, - }, - wantError: errors.New("before function failed"), - }, { name: "fails with GetRef error", setupMock: func(mockClient *mocks.FakeClient) { mockClient.GetRefReturns(nanogit.Ref{}, errors.New("ref not found")) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: errors.New("ref not found"), }, @@ -105,9 +86,8 @@ func TestNewStagedGitRepository(t *testing.T) { }, nil) mockClient.NewStagedWriterReturns(nil, errors.New("failed to create writer")) }, - opts: repository.CloneOptions{ - CreateIfNotExists: false, - PushOnWrites: false, + opts: repository.StageOptions{ + PushOnWrites: false, }, wantError: errors.New("build staged writer: failed to create writer"), }, @@ -141,13 +121,8 @@ func TestNewStagedGitRepository(t *testing.T) { // Compare opts fields individually since function pointers can't be compared directly actualOpts := stagedRepo.(*stagedGitRepository).opts - require.Equal(t, tt.opts.CreateIfNotExists, actualOpts.CreateIfNotExists) require.Equal(t, tt.opts.PushOnWrites, actualOpts.PushOnWrites) - require.Equal(t, tt.opts.MaxSize, actualOpts.MaxSize) require.Equal(t, tt.opts.Timeout, actualOpts.Timeout) - require.Equal(t, tt.opts.Progress, actualOpts.Progress) - // BeforeFn is a function pointer, so we just check if both are nil or both are not nil - require.Equal(t, tt.opts.BeforeFn == nil, actualOpts.BeforeFn == nil) } }) } @@ -287,7 +262,7 @@ func TestStagedGitRepository_Create(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string data []byte @@ -301,7 +276,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -318,7 +293,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -333,7 +308,7 @@ func TestStagedGitRepository_Create(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -347,7 +322,7 @@ func TestStagedGitRepository_Create(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -362,7 +337,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -378,7 +353,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(errors.New("push failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -419,7 +394,7 @@ func TestStagedGitRepository_Write(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string data []byte @@ -435,7 +410,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -454,7 +429,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -470,7 +445,7 @@ func TestStagedGitRepository_Write(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -484,7 +459,7 @@ func TestStagedGitRepository_Write(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.BlobExistsReturns(false, errors.New("blob exists check failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -499,7 +474,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.BlobExistsReturns(false, nil) mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -514,7 +489,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.BlobExistsReturns(true, nil) mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -530,7 +505,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -570,7 +545,7 @@ func TestStagedGitRepository_Update(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string data []byte @@ -584,7 +559,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -601,7 +576,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "test.yaml", @@ -616,7 +591,7 @@ func TestStagedGitRepository_Update(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -630,7 +605,7 @@ func TestStagedGitRepository_Update(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "directory/", @@ -644,7 +619,7 @@ func TestStagedGitRepository_Update(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -659,7 +634,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.UpdateBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -699,7 +674,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.CloneOptions + opts repository.StageOptions path string ref string message string @@ -712,7 +687,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -728,7 +703,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) mockWriter.PushReturns(nil) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: true, }, path: "testdir/", @@ -742,7 +717,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { // No setup needed as error occurs before writer calls }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -755,7 +730,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -769,7 +744,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.DeleteBlobReturns(hash.Hash{1, 2, 3}, nil) mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, - opts: repository.CloneOptions{ + opts: repository.StageOptions{ PushOnWrites: false, }, path: "test.yaml", @@ -808,7 +783,6 @@ func TestStagedGitRepository_Push(t *testing.T) { tests := []struct { name string setupMock func(*mocks.FakeStagedWriter) - opts repository.PushOptions wantError error expectCalls int }{ @@ -817,7 +791,6 @@ func TestStagedGitRepository_Push(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - opts: repository.PushOptions{}, wantError: nil, expectCalls: 1, }, @@ -826,11 +799,6 @@ func TestStagedGitRepository_Push(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - opts: repository.PushOptions{ - BeforeFn: func() error { - return nil - }, - }, wantError: nil, expectCalls: 1, }, @@ -839,31 +807,14 @@ func TestStagedGitRepository_Push(t *testing.T) { setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - opts: repository.PushOptions{ - Timeout: time.Second * 5, - }, wantError: nil, expectCalls: 1, }, - { - name: "fails with before fn error", - setupMock: func(_ *mocks.FakeStagedWriter) { - // No setup needed as BeforeFn fails first - }, - opts: repository.PushOptions{ - BeforeFn: func() error { - return errors.New("before function failed") - }, - }, - wantError: errors.New("before function failed"), - expectCalls: 0, - }, { name: "fails with push error", setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(errors.New("push failed")) }, - opts: repository.PushOptions{}, wantError: errors.New("push failed"), expectCalls: 1, }, @@ -874,9 +825,9 @@ func TestStagedGitRepository_Push(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} tt.setupMock(mockWriter) - stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}) + stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}) - err := stagedRepo.Push(context.Background(), tt.opts) + err := stagedRepo.Push(context.Background()) if tt.wantError != nil { require.EqualError(t, err, tt.wantError.Error()) @@ -892,7 +843,7 @@ func TestStagedGitRepository_Push(t *testing.T) { func TestStagedGitRepository_Remove(t *testing.T) { t.Run("succeeds with remove", func(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} - stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}) + stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}) err := stagedRepo.Remove(context.Background()) require.NoError(t, err) @@ -904,10 +855,10 @@ func TestStagedGitRepository_Remove(t *testing.T) { func createTestStagedRepository(mockClient *mocks.FakeClient) *stagedGitRepository { mockWriter := &mocks.FakeStagedWriter{} - return createTestStagedRepositoryWithWriter(mockWriter, repository.CloneOptions{}, mockClient) + return createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}, mockClient) } -func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.CloneOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository { +func createTestStagedRepositoryWithWriter(mockWriter *mocks.FakeStagedWriter, opts repository.StageOptions, mockClient ...*mocks.FakeClient) *stagedGitRepository { var client nanogit.Client if len(mockClient) > 0 { client = mockClient[0] diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go deleted file mode 100644 index 42c78e55c8e..00000000000 --- a/pkg/registry/apis/provisioning/repository/github.go +++ /dev/null @@ -1,683 +0,0 @@ -package repository - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net/http" - "net/url" - "regexp" - "strings" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/validation/field" - - "github.com/grafana/grafana-app-sdk/logging" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" -) - -// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. -type githubRepository struct { - config *provisioning.Repository - gh pgh.Client // assumes github.com base URL - secrets secrets.Service - - owner string - repo string - - cloneFn CloneFn -} - -// GithubRepository is an interface that combines all repository capabilities -// needed for GitHub repositories. - -//go:generate mockery --name GithubRepository --structname MockGithubRepository --inpackage --filename github_repository_mock.go --with-expecter -type GithubRepository interface { - Repository - Versioned - Writer - Reader - RepositoryWithURLs - ClonableRepository - Owner() string - Repo() string - Client() pgh.Client -} - -func NewGitHub( - ctx context.Context, - config *provisioning.Repository, - factory *pgh.Factory, - secrets secrets.Service, - cloneFn CloneFn, -) (GithubRepository, error) { - owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL) - if err != nil { - return nil, fmt.Errorf("parse owner and repo: %w", err) - } - - token := config.Spec.GitHub.Token - if token == "" { - decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) - if err != nil { - return nil, fmt.Errorf("decrypt token: %w", err) - } - token = string(decrypted) - } - - return &githubRepository{ - config: config, - gh: factory.New(ctx, token), // TODO, baseURL from config - secrets: secrets, - owner: owner, - repo: repo, - cloneFn: cloneFn, - }, nil -} - -func (r *githubRepository) Config() *provisioning.Repository { - return r.config -} - -func (r *githubRepository) Owner() string { - return r.owner -} - -func (r *githubRepository) Repo() string { - return r.repo -} - -func (r *githubRepository) Client() pgh.Client { - return r.gh -} - -// Validate implements provisioning.Repository. -func (r *githubRepository) Validate() (list field.ErrorList) { - gh := r.config.Spec.GitHub - if gh == nil { - list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) - return list - } - if gh.URL == "" { - list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) - } else { - _, _, err := ParseOwnerRepoGithub(gh.URL) - if err != nil { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) - } else if !strings.HasPrefix(gh.URL, "https://github.com/") { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) - } - } - if gh.Branch == "" { - list = append(list, field.Required(field.NewPath("spec", "github", "branch"), "a github branch is required")) - } else if !IsValidGitBranchName(gh.Branch) { - list = append(list, field.Invalid(field.NewPath("spec", "github", "branch"), gh.Branch, "invalid branch name")) - } - // TODO: Use two fields for token - if gh.Token == "" && len(gh.EncryptedToken) == 0 { - list = append(list, field.Required(field.NewPath("spec", "github", "token"), "a github access token is required")) - } - - if err := safepath.IsSafe(gh.Path); err != nil { - list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, err.Error())) - } - - if safepath.IsAbs(gh.Path) { - list = append(list, field.Invalid(field.NewPath("spec", "github", "prefix"), gh.Path, "path must be relative")) - } - - return list -} - -func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) { - parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git")) - if e != nil { - err = e - return - } - parts := strings.Split(parsed.Path, "/") - if len(parts) < 3 { - err = fmt.Errorf("unable to parse repo+owner from url") - return - } - return parts[1], parts[2], nil -} - -// Test implements provisioning.Repository. -func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { - if err := r.gh.IsAuthenticated(ctx); err != nil { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: field.NewPath("spec", "github", "token").String(), - Detail: err.Error(), - }}}, nil - } - - url := r.config.Spec.GitHub.URL - owner, repo, err := ParseOwnerRepoGithub(url) - if err != nil { - return fromFieldError(field.Invalid( - field.NewPath("spec", "github", "url"), url, err.Error())), nil - } - - // FIXME: check token permissions - ok, err := r.gh.RepoExists(ctx, owner, repo) - if err != nil { - return fromFieldError(field.Invalid( - field.NewPath("spec", "github", "url"), url, err.Error())), nil - } - - if !ok { - return fromFieldError(field.NotFound( - field.NewPath("spec", "github", "url"), url)), nil - } - - branch := r.config.Spec.GitHub.Branch - ok, err = r.gh.BranchExists(ctx, r.owner, r.repo, branch) - if err != nil { - return fromFieldError(field.Invalid( - field.NewPath("spec", "github", "branch"), branch, err.Error())), nil - } - - if !ok { - return fromFieldError(field.NotFound( - field.NewPath("spec", "github", "branch"), branch)), nil - } - - return &provisioning.TestResults{ - Code: http.StatusOK, - Success: true, - }, nil -} - -// ReadResource implements provisioning.Repository. -func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*FileInfo, error) { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, filePath) - content, dirContent, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return nil, ErrFileNotFound - } - - return nil, fmt.Errorf("get contents: %w", err) - } - if dirContent != nil { - return &FileInfo{ - Path: filePath, - Ref: ref, - }, nil - } - - data, err := content.GetFileContent() - if err != nil { - return nil, fmt.Errorf("get content: %w", err) - } - return &FileInfo{ - Path: filePath, - Ref: ref, - Data: []byte(data), - Hash: content.GetSHA(), - }, nil -} - -func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - - ctx, _ = r.logger(ctx, ref) - tree, truncated, err := r.gh.GetTree(ctx, r.owner, r.repo, r.config.Spec.GitHub.Path, ref, true) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return nil, &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: fmt.Sprintf("tree not found; ref=%s", ref), - Code: http.StatusNotFound, - }, - } - } - return nil, fmt.Errorf("get tree: %w", err) - } - - if truncated { - return nil, fmt.Errorf("tree truncated") - } - - entries := make([]FileTreeEntry, 0, len(tree)) - for _, entry := range tree { - isBlob := !entry.IsDirectory() - // FIXME: this we could potentially do somewhere else on in a different way - filePath := entry.GetPath() - if !isBlob && !safepath.IsDir(filePath) { - filePath = filePath + "/" - } - - converted := FileTreeEntry{ - Path: filePath, - Size: entry.GetSize(), - Hash: entry.GetSHA(), - Blob: !entry.IsDirectory(), - } - entries = append(entries, converted) - } - return entries, nil -} - -func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - if err := r.ensureBranchExists(ctx, ref); err != nil { - return err - } - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - - // Create .keep file if it is a directory - if safepath.IsDir(finalPath) { - if data != nil { - return apierrors.NewBadRequest("data cannot be provided for a directory") - } - - finalPath = safepath.Join(finalPath, ".keep") - data = []byte{} - } - - err := r.gh.CreateFile(ctx, r.owner, r.repo, finalPath, ref, comment, data) - if errors.Is(err, pgh.ErrResourceAlreadyExists) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file already exists", - Code: http.StatusConflict, - }, - } - } - - return err -} - -func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - if err := r.ensureBranchExists(ctx, ref); err != nil { - return err - } - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - file, _, err := r.gh.GetContents(ctx, r.owner, r.repo, finalPath, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file not found", - Code: http.StatusNotFound, - }, - } - } - - return fmt.Errorf("get content before file update: %w", err) - } - if file.IsDirectory() { - return apierrors.NewBadRequest("cannot update a directory") - } - - if err := r.gh.UpdateFile(ctx, r.owner, r.repo, finalPath, ref, comment, file.GetSHA(), data); err != nil { - return fmt.Errorf("update file: %w", err) - } - return nil -} - -func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - - ctx, _ = r.logger(ctx, ref) - _, err := r.Read(ctx, path, ref) - if err != nil && !(errors.Is(err, ErrFileNotFound)) { - return fmt.Errorf("check if file exists before writing: %w", err) - } - if err == nil { - return r.Update(ctx, path, ref, data, message) - } - - return r.Create(ctx, path, ref, data, message) -} - -func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - if err := r.ensureBranchExists(ctx, ref); err != nil { - return err - } - - // TODO: should add some protection against deleting the root directory? - - // Inside deleteRecursively, all paths are relative to the root of the repository - // so we need to prepend the prefix there but only here. - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - - return r.deleteRecursively(ctx, finalPath, ref, comment) -} - -func (r *githubRepository) deleteRecursively(ctx context.Context, path, ref, comment string) error { - file, contents, err := r.gh.GetContents(ctx, r.owner, r.repo, path, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return ErrFileNotFound - } - - return fmt.Errorf("find file to delete: %w", err) - } - - if file != nil && !file.IsDirectory() { - return r.gh.DeleteFile(ctx, r.owner, r.repo, path, ref, comment, file.GetSHA()) - } - - for _, c := range contents { - p := c.GetPath() - if c.IsDirectory() { - if err := r.deleteRecursively(ctx, p, ref, comment); err != nil { - return fmt.Errorf("delete directory recursively: %w", err) - } - continue - } - - if err := r.gh.DeleteFile(ctx, r.owner, r.repo, p, ref, comment, c.GetSHA()); err != nil { - return fmt.Errorf("delete file: %w", err) - } - } - - return nil -} - -func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - ctx, _ = r.logger(ctx, ref) - - finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) - commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref) - if err != nil { - if errors.Is(err, pgh.ErrResourceNotFound) { - return nil, ErrFileNotFound - } - - return nil, fmt.Errorf("get commits: %w", err) - } - - ret := make([]provisioning.HistoryItem, 0, len(commits)) - for _, commit := range commits { - authors := make([]provisioning.Author, 0) - if commit.Author != nil { - authors = append(authors, provisioning.Author{ - Name: commit.Author.Name, - Username: commit.Author.Username, - AvatarURL: commit.Author.AvatarURL, - }) - } - - if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name { - authors = append(authors, provisioning.Author{ - Name: commit.Committer.Name, - Username: commit.Committer.Username, - AvatarURL: commit.Committer.AvatarURL, - }) - } - - ret = append(ret, provisioning.HistoryItem{ - Ref: commit.Ref, - Message: commit.Message, - Authors: authors, - CreatedAt: commit.CreatedAt.UnixMilli(), - }) - } - - return ret, nil -} - -// basicGitBranchNameRegex is a regular expression to validate a git branch name -// it does not cover all cases as positive lookaheads are not supported in Go's regexp -var basicGitBranchNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\/\.]+$`) - -// IsValidGitBranchName checks if a branch name is valid. -// It uses the following regexp `^[a-zA-Z0-9\-\_\/\.]+$` to validate the branch name with some additional checks that must satisfy the following rules: -// 1. The branch name must have at least one character and must not be empty. -// 2. The branch name cannot start with `/` or end with `/`, `.`, or whitespace. -// 3. The branch name cannot contain consecutive slashes (`//`). -// 4. The branch name cannot contain consecutive dots (`..`). -// 5. The branch name cannot contain `@{`. -// 6. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`. -func IsValidGitBranchName(branch string) bool { - if !basicGitBranchNameRegex.MatchString(branch) { - return false - } - - // Additional checks for invalid patterns - if strings.HasPrefix(branch, "/") || strings.HasSuffix(branch, "/") || - strings.HasSuffix(branch, ".") || strings.Contains(branch, "..") || - strings.Contains(branch, "//") || strings.HasSuffix(branch, ".lock") { - return false - } - - return true -} - -func (r *githubRepository) ensureBranchExists(ctx context.Context, branchName string) error { - if !IsValidGitBranchName(branchName) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusBadRequest, - Message: "invalid branch name", - }, - } - } - - ok, err := r.gh.BranchExists(ctx, r.owner, r.repo, branchName) - if err != nil { - return fmt.Errorf("check branch exists: %w", err) - } - - if ok { - logging.FromContext(ctx).Info("branch already exists", "branch", branchName) - - return nil - } - - srcBranch := r.config.Spec.GitHub.Branch - if err := r.gh.CreateBranch(ctx, r.owner, r.repo, srcBranch, branchName); err != nil { - if errors.Is(err, pgh.ErrResourceAlreadyExists) { - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusConflict, - Message: "branch already exists", - }, - } - } - - return fmt.Errorf("create branch: %w", err) - } - - return nil -} - -func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { - ctx, _ = r.logger(ctx, "") - branch, err := r.gh.GetBranch(ctx, r.owner, r.repo, r.Config().Spec.GitHub.Branch) - if err != nil { - return "", fmt.Errorf("get branch: %w", err) - } - - return branch.Sha, nil -} - -func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error) { - if ref == "" { - var err error - ref, err = r.LatestRef(ctx) - if err != nil { - return nil, fmt.Errorf("get latest ref: %w", err) - } - } - ctx, logger := r.logger(ctx, ref) - - files, err := r.gh.CompareCommits(ctx, r.owner, r.repo, base, ref) - if err != nil { - return nil, fmt.Errorf("compare commits: %w", err) - } - - changes := make([]VersionedFileChange, 0) - for _, f := range files { - // reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit - switch f.GetStatus() { - case "added", "copied": - currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - if err != nil { - // do nothing as it's outside of configured path - continue - } - - changes = append(changes, VersionedFileChange{ - Path: currentPath, - Ref: ref, - Action: FileActionCreated, - }) - case "modified", "changed": - currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - if err != nil { - // do nothing as it's outside of configured path - continue - } - - changes = append(changes, VersionedFileChange{ - Path: currentPath, - Ref: ref, - Action: FileActionUpdated, - }) - case "renamed": - previousPath, previousErr := safepath.RelativeTo(f.GetPreviousFilename(), r.config.Spec.GitHub.Path) - currentPath, currentErr := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - - // Handle all possible combinations of path validation results: - // 1. Both paths outside configured path, do nothing - // 2. Both paths inside configured path, rename - // 3. Moving out of configured path, delete previous file - // 4. Moving into configured path, create new file - switch { - case previousErr != nil && currentErr != nil: - // do nothing as it's outside of configured path - case previousErr == nil && currentErr == nil: - changes = append(changes, VersionedFileChange{ - Path: currentPath, - PreviousPath: previousPath, - Ref: ref, - PreviousRef: base, - Action: FileActionRenamed, - }) - case previousErr == nil && currentErr != nil: - changes = append(changes, VersionedFileChange{ - Path: previousPath, - Ref: base, - Action: FileActionDeleted, - }) - case previousErr != nil && currentErr == nil: - changes = append(changes, VersionedFileChange{ - Path: currentPath, - Ref: ref, - Action: FileActionCreated, - }) - } - case "removed": - currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) - if err != nil { - // do nothing as it's outside of configured path - continue - } - - changes = append(changes, VersionedFileChange{ - Ref: ref, - PreviousRef: base, - Path: currentPath, - PreviousPath: currentPath, - Action: FileActionDeleted, - }) - case "unchanged": - // do nothing - default: - logger.Error("ignore unhandled file", "file", f.GetFilename(), "status", f.GetStatus()) - } - } - - return changes, nil -} - -// ResourceURLs implements RepositoryWithURLs. -func (r *githubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) { - cfg := r.config.Spec.GitHub - if file.Path == "" || cfg == nil { - return nil, nil - } - - ref := file.Ref - if ref == "" { - ref = cfg.Branch - } - - urls := &provisioning.ResourceURLs{ - RepositoryURL: cfg.URL, - SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path), - } - - if ref != cfg.Branch { - urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref) - - // Create a new pull request - urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL) - } - - return urls, nil -} - -func (r *githubRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - return r.cloneFn(ctx, opts) -} - -func (r *githubRepository) logger(ctx context.Context, ref string) (context.Context, logging.Logger) { - logger := logging.FromContext(ctx) - - type containsGh int - var containsGhKey containsGh - if ctx.Value(containsGhKey) != nil { - return ctx, logging.FromContext(ctx) - } - - if ref == "" { - ref = r.config.Spec.GitHub.Branch - } - logger = logger.With(slog.Group("github_repository", "owner", r.owner, "name", r.repo, "ref", ref)) - ctx = logging.Context(ctx, logger) - // We want to ensure we don't add multiple github_repository keys. With doesn't deduplicate the keys... - ctx = context.WithValue(ctx, containsGhKey, true) - return ctx, logger -} diff --git a/pkg/registry/apis/provisioning/repository/github/client.go b/pkg/registry/apis/provisioning/repository/github/client.go index 9c35ca3898a..5b21a5a4f1e 100644 --- a/pkg/registry/apis/provisioning/repository/github/client.go +++ b/pkg/registry/apis/provisioning/repository/github/client.go @@ -7,127 +7,34 @@ import ( "errors" "time" - "github.com/google/go-github/v70/github" apierrors "k8s.io/apimachinery/pkg/api/errors" ) // API errors that we need to convey after parsing real GH errors (or faking them). var ( - ErrResourceAlreadyExists = errors.New("the resource already exists") - ErrResourceNotFound = errors.New("the resource does not exist") - ErrMismatchedHash = errors.New("the update cannot be applied because the expected and actual hashes are unequal") - ErrNoSecret = errors.New("new webhooks must have a secret") + ErrResourceNotFound = errors.New("the resource does not exist") //lint:ignore ST1005 this is not punctuation - ErrPathTraversalDisallowed = errors.New("the path contained ..") //nolint:staticcheck - ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable") - ErrFileTooLarge = errors.New("file exceeds maximum allowed size") - ErrTooManyItems = errors.New("maximum number of items exceeded") + ErrServiceUnavailable = apierrors.NewServiceUnavailable("github is unavailable") + ErrTooManyItems = errors.New("maximum number of items exceeded") ) -// MaxFileSize maximum file size limit (10MB) -const MaxFileSize = 10 * 1024 * 1024 // 10MB in bytes - -type ErrRateLimited = github.RateLimitError - //go:generate mockery --name Client --structname MockClient --inpackage --filename mock_client.go --with-expecter type Client interface { - // IsAuthenticated checks if the client is authenticated. - IsAuthenticated(ctx context.Context) error - - // GetContents returns the metadata and content of a file or directory. - // When a file is checked, the first returned value will have a value. For a directory, the second will. The other value is always nil. - // If an error occurs, the returned values may or may not be nil. - // - // If ".." appears in the "path", this method will return an error. - GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) - - // GetTree returns the Git tree in the repository. - // When recursive is given, subtrees are mapped into the returned array. - // When basePath is given, only trees under it are given. The results do not include this path in their names. - // - // The truncated bool will be set to true if the tree is larger than 7 MB or 100 000 entries. - // When truncated is true, you may wish to read each subtree manually instead. - GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) (entries []RepositoryContent, truncated bool, err error) - - // CreateFile creates a new file in the repository under the given path. - // The file is created on the branch given. - // The message given is the commit message. If none is given, an appropriate default is used. - // The content is what the file should contain. An empty slice is valid, though often not very useful. - // - // If ".." appears in the "path", this method will return an error. - CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error - - // UpdateFile updates a file in the repository under the given path. - // The file is updated on the branch given. - // The message given is the commit message. If none is given, an appropriate default is used. - // The content is what the file should contain. An empty slice is valid, though often not very useful. - // If the path does not exist, an error is returned. - // The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this. - // - // If ".." appears in the "path", this method will return an error. - UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error - - // DeleteFile deletes a file in the repository under the given path. - // The file is deleted from the branch given. - // The message given is the commit message. If none is given, an appropriate default is used. - // If the path does not exist, an error is returned. - // The hash given must be the SHA hash of the file contents. Calling GetContents in advance is an easy way of handling this. - // - // If ".." appears in the "path", this method will return an error. - DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error - - // Commits returns the commits for the given path + // Commits Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) - // CompareCommits returns the changes between two commits. - CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) - - // RepoExists checks if a repository exists. - RepoExists(ctx context.Context, owner, repository string) (bool, error) - - // CreateBranch creates a new branch in the repository. - CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error - // BranchExists checks if a branch exists in the repository. - BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) - // GetBranch returns the branch of the repository. - GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) - + // Webhooks ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error) GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error) DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error + // Pull requests ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error) CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error } -//go:generate mockery --name RepositoryContent --structname MockRepositoryContent --inpackage --filename mock_repository_content.go --with-expecter -type RepositoryContent interface { - // Returns true if this is a directory, false if it is a file. - IsDirectory() bool - // Returns the contents of the file. Decoding happens if necessary. - // Returns an error if the content represents a directory. - GetFileContent() (string, error) - // Returns true if this is a symlink. - // If true, GetPath returns the path where this symlink leads. - IsSymlink() bool - // Returns the full path from the root of the repository. - // This has no leading or trailing slashes. - // The path only uses '/' for directories. You can use the 'path' package to interact with these. - GetPath() string - // Get the SHA hash. This is usually a SHA-256, but may also be SHA-512. - // Directories have SHA hashes, too (TODO: how is this calculated?). - GetSHA() string - // The size of the file. Not necessarily non-zero, even if the file is supposed to be non-zero. - GetSize() int64 -} - -type Branch struct { - Name string - Sha string -} - type CommitAuthor struct { Name string Username string @@ -150,20 +57,6 @@ type CommitFile interface { GetStatus() string } -type FileComment struct { - Content string - Path string - Position int - Ref string -} - -type CreateFileOptions struct { - // The message of the commit. May be empty, in which case a default value is entered. - Message string - // The content of the file to write, unencoded. - Content []byte -} - type WebhookConfig struct { // The ID of the webhook. // Can be 0 on creation. diff --git a/pkg/registry/apis/provisioning/repository/github/factory.go b/pkg/registry/apis/provisioning/repository/github/factory.go index b143cd7ad74..da2d50a6131 100644 --- a/pkg/registry/apis/provisioning/repository/github/factory.go +++ b/pkg/registry/apis/provisioning/repository/github/factory.go @@ -27,6 +27,11 @@ func (r *Factory) New(ctx context.Context, ghToken string) Client { tokenSrc := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: ghToken}, ) - tokenClient := oauth2.NewClient(ctx, tokenSrc) - return NewClient(github.NewClient(tokenClient)) + + if len(ghToken) == 0 { + tokenClient := oauth2.NewClient(ctx, tokenSrc) + return NewClient(github.NewClient(tokenClient)) + } + + return NewClient(github.NewClient(&http.Client{})) } diff --git a/pkg/registry/apis/provisioning/repository/github_repository_mock.go b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go similarity index 70% rename from pkg/registry/apis/provisioning/repository/github_repository_mock.go rename to pkg/registry/apis/provisioning/repository/github/github_repository_mock.go index 8d4dd2d58ff..a2c2798819a 100644 --- a/pkg/registry/apis/provisioning/repository/github_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go @@ -1,14 +1,14 @@ // Code generated by mockery v2.52.4. DO NOT EDIT. -package repository +package github import ( context "context" - github "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + mock "github.com/stretchr/testify/mock" field "k8s.io/apimachinery/pkg/util/validation/field" - mock "github.com/stretchr/testify/mock" + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) @@ -27,19 +27,19 @@ func (_m *MockGithubRepository) EXPECT() *MockGithubRepository_Expecter { } // Client provides a mock function with no fields -func (_m *MockGithubRepository) Client() github.Client { +func (_m *MockGithubRepository) Client() Client { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Client") } - var r0 github.Client - if rf, ok := ret.Get(0).(func() github.Client); ok { + var r0 Client + if rf, ok := ret.Get(0).(func() Client); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(github.Client) + r0 = ret.Get(0).(Client) } } @@ -63,93 +63,34 @@ func (_c *MockGithubRepository_Client_Call) Run(run func()) *MockGithubRepositor return _c } -func (_c *MockGithubRepository_Client_Call) Return(_a0 github.Client) *MockGithubRepository_Client_Call { +func (_c *MockGithubRepository_Client_Call) Return(_a0 Client) *MockGithubRepository_Client_Call { _c.Call.Return(_a0) return _c } -func (_c *MockGithubRepository_Client_Call) RunAndReturn(run func() github.Client) *MockGithubRepository_Client_Call { - _c.Call.Return(run) - return _c -} - -// Clone provides a mock function with given fields: ctx, opts -func (_m *MockGithubRepository) Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - ret := _m.Called(ctx, opts) - - if len(ret) == 0 { - panic("no return value specified for Clone") - } - - var r0 ClonedRepository - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) (ClonedRepository, error)); ok { - return rf(ctx, opts) - } - if rf, ok := ret.Get(0).(func(context.Context, CloneOptions) ClonedRepository); ok { - r0 = rf(ctx, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(ClonedRepository) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, CloneOptions) error); ok { - r1 = rf(ctx, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockGithubRepository_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' -type MockGithubRepository_Clone_Call struct { - *mock.Call -} - -// Clone is a helper method to define mock.On call -// - ctx context.Context -// - opts CloneOptions -func (_e *MockGithubRepository_Expecter) Clone(ctx interface{}, opts interface{}) *MockGithubRepository_Clone_Call { - return &MockGithubRepository_Clone_Call{Call: _e.mock.On("Clone", ctx, opts)} -} - -func (_c *MockGithubRepository_Clone_Call) Run(run func(ctx context.Context, opts CloneOptions)) *MockGithubRepository_Clone_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(CloneOptions)) - }) - return _c -} - -func (_c *MockGithubRepository_Clone_Call) Return(_a0 ClonedRepository, _a1 error) *MockGithubRepository_Clone_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockGithubRepository_Clone_Call) RunAndReturn(run func(context.Context, CloneOptions) (ClonedRepository, error)) *MockGithubRepository_Clone_Call { +func (_c *MockGithubRepository_Client_Call) RunAndReturn(run func() Client) *MockGithubRepository_Client_Call { _c.Call.Return(run) return _c } // CompareFiles provides a mock function with given fields: ctx, base, ref -func (_m *MockGithubRepository) CompareFiles(ctx context.Context, base string, ref string) ([]VersionedFileChange, error) { +func (_m *MockGithubRepository) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) { ret := _m.Called(ctx, base, ref) if len(ret) == 0 { panic("no return value specified for CompareFiles") } - var r0 []VersionedFileChange + var r0 []repository.VersionedFileChange var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]VersionedFileChange, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok { return rf(ctx, base, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []VersionedFileChange); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok { r0 = rf(ctx, base, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]VersionedFileChange) + r0 = ret.Get(0).([]repository.VersionedFileChange) } } @@ -182,12 +123,12 @@ func (_c *MockGithubRepository_CompareFiles_Call) Run(run func(ctx context.Conte return _c } -func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []VersionedFileChange, _a1 error) *MockGithubRepository_CompareFiles_Call { +func (_c *MockGithubRepository_CompareFiles_Call) Return(_a0 []repository.VersionedFileChange, _a1 error) *MockGithubRepository_CompareFiles_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGithubRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]VersionedFileChange, error)) *MockGithubRepository_CompareFiles_Call { +func (_c *MockGithubRepository_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockGithubRepository_CompareFiles_Call { _c.Call.Return(run) return _c } @@ -454,6 +395,226 @@ func (_c *MockGithubRepository_LatestRef_Call) RunAndReturn(run func(context.Con return _c } +// ListRefs provides a mock function with given fields: ctx +func (_m *MockGithubRepository) ListRefs(ctx context.Context) ([]v0alpha1.RefItem, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListRefs") + } + + var r0 []v0alpha1.RefItem + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]v0alpha1.RefItem, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []v0alpha1.RefItem); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]v0alpha1.RefItem) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGithubRepository_ListRefs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRefs' +type MockGithubRepository_ListRefs_Call struct { + *mock.Call +} + +// ListRefs is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGithubRepository_Expecter) ListRefs(ctx interface{}) *MockGithubRepository_ListRefs_Call { + return &MockGithubRepository_ListRefs_Call{Call: _e.mock.On("ListRefs", ctx)} +} + +func (_c *MockGithubRepository_ListRefs_Call) Run(run func(ctx context.Context)) *MockGithubRepository_ListRefs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGithubRepository_ListRefs_Call) Return(_a0 []v0alpha1.RefItem, _a1 error) *MockGithubRepository_ListRefs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGithubRepository_ListRefs_Call) RunAndReturn(run func(context.Context) ([]v0alpha1.RefItem, error)) *MockGithubRepository_ListRefs_Call { + _c.Call.Return(run) + return _c +} + +// OnCreate provides a mock function with given fields: ctx +func (_m *MockGithubRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OnCreate") + } + + var r0 []map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGithubRepository_OnCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnCreate' +type MockGithubRepository_OnCreate_Call struct { + *mock.Call +} + +// OnCreate is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGithubRepository_Expecter) OnCreate(ctx interface{}) *MockGithubRepository_OnCreate_Call { + return &MockGithubRepository_OnCreate_Call{Call: _e.mock.On("OnCreate", ctx)} +} + +func (_c *MockGithubRepository_OnCreate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnCreate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGithubRepository_OnCreate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnCreate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGithubRepository_OnCreate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnCreate_Call { + _c.Call.Return(run) + return _c +} + +// OnDelete provides a mock function with given fields: ctx +func (_m *MockGithubRepository) OnDelete(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OnDelete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockGithubRepository_OnDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDelete' +type MockGithubRepository_OnDelete_Call struct { + *mock.Call +} + +// OnDelete is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGithubRepository_Expecter) OnDelete(ctx interface{}) *MockGithubRepository_OnDelete_Call { + return &MockGithubRepository_OnDelete_Call{Call: _e.mock.On("OnDelete", ctx)} +} + +func (_c *MockGithubRepository_OnDelete_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnDelete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGithubRepository_OnDelete_Call) Return(_a0 error) *MockGithubRepository_OnDelete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockGithubRepository_OnDelete_Call) RunAndReturn(run func(context.Context) error) *MockGithubRepository_OnDelete_Call { + _c.Call.Return(run) + return _c +} + +// OnUpdate provides a mock function with given fields: ctx +func (_m *MockGithubRepository) OnUpdate(ctx context.Context) ([]map[string]interface{}, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OnUpdate") + } + + var r0 []map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGithubRepository_OnUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnUpdate' +type MockGithubRepository_OnUpdate_Call struct { + *mock.Call +} + +// OnUpdate is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockGithubRepository_Expecter) OnUpdate(ctx interface{}) *MockGithubRepository_OnUpdate_Call { + return &MockGithubRepository_OnUpdate_Call{Call: _e.mock.On("OnUpdate", ctx)} +} + +func (_c *MockGithubRepository_OnUpdate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnUpdate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockGithubRepository_OnUpdate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnUpdate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGithubRepository_OnUpdate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnUpdate_Call { + _c.Call.Return(run) + return _c +} + // Owner provides a mock function with no fields func (_m *MockGithubRepository) Owner() string { ret := _m.Called() @@ -500,23 +661,23 @@ func (_c *MockGithubRepository_Owner_Call) RunAndReturn(run func() string) *Mock } // Read provides a mock function with given fields: ctx, path, ref -func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { +func (_m *MockGithubRepository) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { ret := _m.Called(ctx, path, ref) if len(ret) == 0 { panic("no return value specified for Read") } - var r0 *FileInfo + var r0 *repository.FileInfo var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (*FileInfo, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok { return rf(ctx, path, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) *FileInfo); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok { r0 = rf(ctx, path, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*FileInfo) + r0 = ret.Get(0).(*repository.FileInfo) } } @@ -549,34 +710,34 @@ func (_c *MockGithubRepository_Read_Call) Run(run func(ctx context.Context, path return _c } -func (_c *MockGithubRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockGithubRepository_Read_Call { +func (_c *MockGithubRepository_Read_Call) Return(_a0 *repository.FileInfo, _a1 error) *MockGithubRepository_Read_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGithubRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockGithubRepository_Read_Call { +func (_c *MockGithubRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.FileInfo, error)) *MockGithubRepository_Read_Call { _c.Call.Return(run) return _c } // ReadTree provides a mock function with given fields: ctx, ref -func (_m *MockGithubRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (_m *MockGithubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { ret := _m.Called(ctx, ref) if len(ret) == 0 { panic("no return value specified for ReadTree") } - var r0 []FileTreeEntry + var r0 []repository.FileTreeEntry var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]FileTreeEntry, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) ([]repository.FileTreeEntry, error)); ok { return rf(ctx, ref) } - if rf, ok := ret.Get(0).(func(context.Context, string) []FileTreeEntry); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) []repository.FileTreeEntry); ok { r0 = rf(ctx, ref) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]FileTreeEntry) + r0 = ret.Get(0).([]repository.FileTreeEntry) } } @@ -608,12 +769,12 @@ func (_c *MockGithubRepository_ReadTree_Call) Run(run func(ctx context.Context, return _c } -func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockGithubRepository_ReadTree_Call { +func (_c *MockGithubRepository_ReadTree_Call) Return(_a0 []repository.FileTreeEntry, _a1 error) *MockGithubRepository_ReadTree_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockGithubRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockGithubRepository_ReadTree_Call { +func (_c *MockGithubRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]repository.FileTreeEntry, error)) *MockGithubRepository_ReadTree_Call { _c.Call.Return(run) return _c } @@ -664,7 +825,7 @@ func (_c *MockGithubRepository_Repo_Call) RunAndReturn(run func() string) *MockG } // ResourceURLs provides a mock function with given fields: ctx, file -func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*v0alpha1.ResourceURLs, error) { +func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*v0alpha1.ResourceURLs, error) { ret := _m.Called(ctx, file) if len(ret) == 0 { @@ -673,10 +834,10 @@ func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo var r0 *v0alpha1.ResourceURLs var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *repository.FileInfo) (*v0alpha1.ResourceURLs, error)); ok { return rf(ctx, file) } - if rf, ok := ret.Get(0).(func(context.Context, *FileInfo) *v0alpha1.ResourceURLs); ok { + if rf, ok := ret.Get(0).(func(context.Context, *repository.FileInfo) *v0alpha1.ResourceURLs); ok { r0 = rf(ctx, file) } else { if ret.Get(0) != nil { @@ -684,7 +845,7 @@ func (_m *MockGithubRepository) ResourceURLs(ctx context.Context, file *FileInfo } } - if rf, ok := ret.Get(1).(func(context.Context, *FileInfo) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *repository.FileInfo) error); ok { r1 = rf(ctx, file) } else { r1 = ret.Error(1) @@ -700,14 +861,14 @@ type MockGithubRepository_ResourceURLs_Call struct { // ResourceURLs is a helper method to define mock.On call // - ctx context.Context -// - file *FileInfo +// - file *repository.FileInfo func (_e *MockGithubRepository_Expecter) ResourceURLs(ctx interface{}, file interface{}) *MockGithubRepository_ResourceURLs_Call { return &MockGithubRepository_ResourceURLs_Call{Call: _e.mock.On("ResourceURLs", ctx, file)} } -func (_c *MockGithubRepository_ResourceURLs_Call) Run(run func(ctx context.Context, file *FileInfo)) *MockGithubRepository_ResourceURLs_Call { +func (_c *MockGithubRepository_ResourceURLs_Call) Run(run func(ctx context.Context, file *repository.FileInfo)) *MockGithubRepository_ResourceURLs_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*FileInfo)) + run(args[0].(context.Context), args[1].(*repository.FileInfo)) }) return _c } @@ -717,7 +878,66 @@ func (_c *MockGithubRepository_ResourceURLs_Call) Return(_a0 *v0alpha1.ResourceU return _c } -func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call { +func (_c *MockGithubRepository_ResourceURLs_Call) RunAndReturn(run func(context.Context, *repository.FileInfo) (*v0alpha1.ResourceURLs, error)) *MockGithubRepository_ResourceURLs_Call { + _c.Call.Return(run) + return _c +} + +// Stage provides a mock function with given fields: ctx, opts +func (_m *MockGithubRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Stage") + } + + var r0 repository.StagedRepository + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) (repository.StagedRepository, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.StageOptions) repository.StagedRepository); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(repository.StagedRepository) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.StageOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockGithubRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage' +type MockGithubRepository_Stage_Call struct { + *mock.Call +} + +// Stage is a helper method to define mock.On call +// - ctx context.Context +// - opts repository.StageOptions +func (_e *MockGithubRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockGithubRepository_Stage_Call { + return &MockGithubRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)} +} + +func (_c *MockGithubRepository_Stage_Call) Run(run func(ctx context.Context, opts repository.StageOptions)) *MockGithubRepository_Stage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.StageOptions)) + }) + return _c +} + +func (_c *MockGithubRepository_Stage_Call) Return(_a0 repository.StagedRepository, _a1 error) *MockGithubRepository_Stage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockGithubRepository_Stage_Call) RunAndReturn(run func(context.Context, repository.StageOptions) (repository.StagedRepository, error)) *MockGithubRepository_Stage_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/repository/github/impl.go b/pkg/registry/apis/provisioning/repository/github/impl.go index d94f2a544cb..c9f5468bc8d 100644 --- a/pkg/registry/apis/provisioning/repository/github/impl.go +++ b/pkg/registry/apis/provisioning/repository/github/impl.go @@ -8,10 +8,6 @@ import ( "time" "github.com/google/go-github/v70/github" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) type githubClient struct { @@ -22,268 +18,12 @@ func NewClient(client *github.Client) Client { return &githubClient{client} } -func (r *githubClient) IsAuthenticated(ctx context.Context) error { - if _, _, err := r.gh.Users.Get(ctx, ""); err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { - switch ghErr.Response.StatusCode { - case http.StatusUnauthorized: - return apierrors.NewUnauthorized("token is invalid or expired") - case http.StatusForbidden: - return &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Status: metav1.StatusFailure, - Code: http.StatusUnauthorized, - Reason: metav1.StatusReasonUnauthorized, - Message: "token is revoked or has insufficient permissions", - }, - } - case http.StatusServiceUnavailable: - return ErrServiceUnavailable - } - } - - return err - } - - return nil -} - -func (r *githubClient) RepoExists(ctx context.Context, owner, repository string) (bool, error) { - _, resp, err := r.gh.Repositories.Get(ctx, owner, repository) - if err == nil { - return true, nil - } - if resp.StatusCode == http.StatusNotFound { - return false, nil - } - - return false, err -} - const ( - maxDirectoryItems = 1000 // Maximum number of items allowed in a directory - maxTreeItems = 10000 // Maximum number of items allowed in a tree - maxCommits = 1000 // Maximum number of commits to fetch - maxCompareFiles = 1000 // Maximum number of files to compare between commits - maxWebhooks = 100 // Maximum number of webhooks allowed per repository - maxPRFiles = 1000 // Maximum number of files allowed in a pull request - maxPullRequestsFileComments = 1000 // Maximum number of comments allowed in a pull request - maxFileSize = 10 * 1024 * 1024 // 10MB in bytes + maxCommits = 1000 // Maximum number of commits to fetch + maxWebhooks = 100 // Maximum number of webhooks allowed per repository + maxPRFiles = 1000 // Maximum number of files allowed in a pull request ) -func (r *githubClient) GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) { - // First try to get repository contents - opts := &github.RepositoryContentGetOptions{ - Ref: ref, - } - - fc, dc, _, err := r.gh.Repositories.GetContents(ctx, owner, repository, path, opts) - if err != nil { - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return nil, nil, err - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return nil, nil, ErrServiceUnavailable - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return nil, nil, ErrResourceNotFound - } - return nil, nil, err - } - - if fc != nil { - // Check file size before returning content - if fc.GetSize() > maxFileSize { - return nil, nil, ErrFileTooLarge - } - return realRepositoryContent{fc}, nil, nil - } - - // For directories, check size limits - if len(dc) > maxDirectoryItems { - return nil, nil, fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems) - } - - // Convert directory contents - allContents := make([]RepositoryContent, 0, len(dc)) - for _, original := range dc { - allContents = append(allContents, realRepositoryContent{original}) - } - - return nil, allContents, nil -} - -func (r *githubClient) GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) ([]RepositoryContent, bool, error) { - var tree *github.Tree - var err error - - subPaths := safepath.Split(basePath) - currentRef := ref - - for { - // If subPaths is empty, we can read recursively, as we're reading the tree from the "base" of the repository. Otherwise, always read only the direct children. - recursive := recursive && len(subPaths) == 0 - - tree, _, err = r.gh.Git.GetTree(ctx, owner, repository, currentRef, recursive) - if err != nil { - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return nil, false, err - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return nil, false, ErrServiceUnavailable - } - if ghErr.Response.StatusCode == http.StatusNotFound { - if currentRef != ref { - // We're operating with a subpath which doesn't exist yet. - // Pretend as if there is simply no files. - // FIXME: why should we pretend this? - return nil, false, nil - } - // currentRef == ref - // This indicates the repository or commitish reference doesn't exist. This should always return an error. - return nil, false, ErrResourceNotFound - } - return nil, false, err - } - - // Check if we've exceeded the maximum allowed items - if len(tree.Entries) > maxTreeItems { - return nil, false, fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems) - } - - // Prep for next iteration. - if len(subPaths) == 0 { - // We're done: we've discovered the tree we want. - break - } - - // the ref must be equal the SHA of the entry corresponding to subPaths[0] - currentRef = "" - for _, e := range tree.Entries { - if e.GetPath() == subPaths[0] { - currentRef = e.GetSHA() - break - } - } - subPaths = subPaths[1:] - if currentRef == "" { - // We couldn't find the folder in the tree... - return nil, false, nil - } - } - - // If the tree is truncated and we're in recursive mode, return an error - if tree.GetTruncated() && recursive { - return nil, true, fmt.Errorf("tree is too large to fetch recursively (more than %d items)", maxTreeItems) - } - - entries := make([]RepositoryContent, 0, len(tree.Entries)) - for _, te := range tree.Entries { - rrc := &realRepositoryContent{ - real: &github.RepositoryContent{ - Path: te.Path, - Size: te.Size, - SHA: te.SHA, - }, - } - if te.GetType() == "tree" { - rrc.real.Type = github.Ptr("dir") - } else { - rrc.real.Type = te.Type - } - entries = append(entries, rrc) - } - return entries, tree.GetTruncated(), nil -} - -func (r *githubClient) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error { - if message == "" { - message = fmt.Sprintf("Create %s", path) - } - - _, _, err := r.gh.Repositories.CreateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ - Branch: &branch, - Message: &message, - Content: content, - }) - if err == nil { - return nil - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return err - } - if ghErr.Response.StatusCode == http.StatusUnprocessableEntity { - return ErrResourceAlreadyExists - } - return err -} - -func (r *githubClient) UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error { - if message == "" { - message = fmt.Sprintf("Update %s", path) - } - - _, _, err := r.gh.Repositories.UpdateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ - Branch: &branch, - Message: &message, - Content: content, - SHA: &hash, - }) - if err == nil { - return nil - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return err - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return ErrResourceNotFound - } - if ghErr.Response.StatusCode == http.StatusConflict { - return ErrMismatchedHash - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return ErrServiceUnavailable - } - return err -} - -func (r *githubClient) DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error { - if message == "" { - message = fmt.Sprintf("Delete %s", path) - } - - _, _, err := r.gh.Repositories.DeleteFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{ - Branch: &branch, - Message: &message, - SHA: &hash, - }) - if err == nil { - return nil - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return err - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return ErrResourceNotFound - } - if ghErr.Response.StatusCode == http.StatusConflict { - return ErrMismatchedHash - } - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return ErrServiceUnavailable - } - return err -} - // Commits returns a list of commits for a given repository and branch. func (r *githubClient) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) { listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) { @@ -343,105 +83,6 @@ func (r *githubClient) Commits(ctx context.Context, owner, repository, path, bra return ret, nil } -func (r *githubClient) CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) { - listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { - compare, resp, err := r.gh.Repositories.CompareCommits(ctx, owner, repository, base, head, opts) - if err != nil { - return nil, resp, err - } - return compare.Files, resp, nil - } - - files, err := paginatedList( - ctx, - listFn, - defaultListOptions(maxCompareFiles), - ) - if errors.Is(err, ErrTooManyItems) { - return nil, fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles) - } - if err != nil { - return nil, err - } - - // Convert to the interface type - ret := make([]CommitFile, 0, len(files)) - for _, f := range files { - ret = append(ret, f) - } - - return ret, nil -} - -func (r *githubClient) GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) { - branch, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0) - if err != nil { - // For some reason, GitHub client handles this case differently by failing with a wrapped error - if resp != nil && resp.StatusCode == http.StatusNotFound { - return Branch{}, ErrResourceNotFound - } - - if resp != nil && resp.StatusCode == http.StatusServiceUnavailable { - return Branch{}, ErrServiceUnavailable - } - - var ghErr *github.ErrorResponse - if !errors.As(err, &ghErr) { - return Branch{}, err - } - // Leaving these just in case - if ghErr.Response.StatusCode == http.StatusServiceUnavailable { - return Branch{}, ErrServiceUnavailable - } - if ghErr.Response.StatusCode == http.StatusNotFound { - return Branch{}, ErrResourceNotFound - } - return Branch{}, err - } - - return Branch{ - Name: branch.GetName(), - Sha: branch.GetCommit().GetSHA(), - }, nil -} - -func (r *githubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error { - // Fail if the branch already exists - if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil { - return ErrResourceAlreadyExists - } - - // Branch out based on the repository branch - baseRef, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 0) - if err != nil { - return fmt.Errorf("get base branch: %w", err) - } - - if _, _, err := r.gh.Git.CreateRef(ctx, owner, repository, &github.Reference{ - Ref: github.Ptr(fmt.Sprintf("refs/heads/%s", branchName)), - Object: &github.GitObject{ - SHA: baseRef.Commit.SHA, - }, - }); err != nil { - return fmt.Errorf("create branch ref: %w", err) - } - - return nil -} - -func (r *githubClient) BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) { - _, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0) - if err == nil { - return true, nil - } - - if resp.StatusCode == http.StatusNotFound { - return false, nil - } - - return false, err -} - func (r *githubClient) ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) { listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { return r.gh.Repositories.ListHooks(ctx, owner, repository, opts) @@ -626,44 +267,6 @@ func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repo return nil } -type realRepositoryContent struct { - real *github.RepositoryContent -} - -var _ RepositoryContent = realRepositoryContent{} - -func (c realRepositoryContent) IsDirectory() bool { - return c.real.GetType() == "dir" -} - -func (c realRepositoryContent) GetFileContent() (string, error) { - return c.real.GetContent() -} - -func (c realRepositoryContent) IsSymlink() bool { - return c.real.Target != nil -} - -func (c realRepositoryContent) GetPath() string { - return c.real.GetPath() -} - -func (c realRepositoryContent) GetSHA() string { - return c.real.GetSHA() -} - -func (c realRepositoryContent) GetSize() int64 { - if c.real.Size != nil { - return int64(*c.real.Size) - } - if c.real.Content != nil { - if c, err := c.real.GetContent(); err == nil { - return int64(len(c)) - } - } - return 0 -} - // listOptions represents pagination parameters for list operations type listOptions struct { github.ListOptions diff --git a/pkg/registry/apis/provisioning/repository/github/impl_test.go b/pkg/registry/apis/provisioning/repository/github/impl_test.go index b979f288836..fedbc2a9850 100644 --- a/pkg/registry/apis/provisioning/repository/github/impl_test.go +++ b/pkg/registry/apis/provisioning/repository/github/impl_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "net/http" - "strings" "testing" "time" @@ -15,1415 +14,8 @@ import ( mockhub "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" ) -func TestIsAuthenticated(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - wantErr error - }{ - { - name: "successful authentication", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatch( - mockhub.GetUser, - github.User{}, - ), - ), - wantErr: nil, - }, - { - name: "unauthorized - invalid token", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Bad credentials"})) - }), - ), - ), - wantErr: apierrors.NewUnauthorized("token is invalid or expired"), - }, - { - name: "forbidden - insufficient permissions", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Forbidden"})) - }), - ), - ), - wantErr: apierrors.NewUnauthorized("token is revoked or has insufficient permissions"), - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - wantErr: ErrServiceUnavailable, - }, - { - name: "unknown error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Internal server error"})) - }), - ), - ), - wantErr: errors.New("500 Internal server error []"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.IsAuthenticated(context.Background()) - // Check the error - if tt.wantErr == nil { - assert.NoError(t, err) - } else { - assert.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.wantErr, &statusErr) { - // For StatusError, compare status code - var actualStatusErr *apierrors.StatusError - assert.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - if actualStatusErr != nil { - assert.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - assert.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } - } else { - // For regular errors, compare error messages - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } - }) - } -} -func TestGithubClient_RepoExists(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - want bool - wantErr error - }{ - { - name: "repository exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(map[string]interface{}{ - "id": 123, - "name": "test-repo", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - want: true, - wantErr: nil, - }, - { - name: "repository does not exist", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})) - }), - ), - ), - owner: "test-owner", - repository: "non-existent-repo", - want: false, - wantErr: nil, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - want: false, - wantErr: errors.New("503 Service unavailable []"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - exists, err := client.RepoExists(context.Background(), tt.owner, tt.repository) - - // Check the result - assert.Equal(t, tt.want, exists) - - // Check the error - if tt.wantErr == nil { - assert.NoError(t, err) - } else { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - }) - } -} - -func TestGithubClient_GetContents(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - ref string - wantFile bool - wantDir bool - wantErr error - }{ - { - name: "get file contents", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - fileContent := &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - Content: github.Ptr("dGVzdCBjb250ZW50"), // base64 encoded "test content" - Encoding: github.Ptr("base64"), - Size: github.Ptr(12), - SHA: github.Ptr("abc123"), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(fileContent)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: true, - wantDir: false, - wantErr: nil, - }, - { - name: "get directory contents", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - dirContents := []*github.RepositoryContent{ - { - Type: github.Ptr("file"), - Name: github.Ptr("file1.txt"), - Path: github.Ptr("dir/file1.txt"), - Size: github.Ptr(100), - SHA: github.Ptr("abc123"), - }, - { - Type: github.Ptr("dir"), - Name: github.Ptr("subdir"), - Path: github.Ptr("dir/subdir"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(dirContents)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "dir", - ref: "main", - wantFile: false, - wantDir: true, - wantErr: nil, - }, - { - name: "resource not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "nonexistent.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: ErrServiceUnavailable, - }, - { - name: "file too large", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - fileContent := &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr("large.txt"), - Path: github.Ptr("large.txt"), - Content: github.Ptr(""), - Encoding: github.Ptr("base64"), - Size: github.Ptr(maxFileSize + 1), // Exceeds max file size - SHA: github.Ptr("abc123"), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(fileContent)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "large.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: ErrFileTooLarge, - }, - { - name: "not a github error response", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - // Return a non-GitHub error format - _, err := w.Write([]byte("not a github error")) - require.NoError(t, err) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: errors.New("409"), - }, - { - name: "directory with too many items", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Create a directory with more than maxDirectoryItems - dirContents := make([]*github.RepositoryContent, maxDirectoryItems+1) - for i := 0; i < maxDirectoryItems+1; i++ { - dirContents[i] = &github.RepositoryContent{ - Type: github.Ptr("file"), - Name: github.Ptr(fmt.Sprintf("file%d.txt", i)), - Path: github.Ptr(fmt.Sprintf("dir/file%d.txt", i)), - Size: github.Ptr(100), - SHA: github.Ptr(fmt.Sprintf("sha%d", i)), - } - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(dirContents)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "dir", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems), - }, - { - name: "error response with other status code", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusForbidden, - }, - Message: "Forbidden access", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - ref: "main", - wantFile: false, - wantDir: false, - wantErr: errors.New("Forbidden access"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - fileContent, dirContents, err := client.GetContents(context.Background(), tt.owner, tt.repository, tt.path, tt.ref) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - assert.Nil(t, fileContent) - assert.Nil(t, dirContents) - return - } - assert.NoError(t, err) - - // Check the result - if tt.wantFile { - assert.NotNil(t, fileContent) - assert.Nil(t, dirContents) - } else if tt.wantDir { - assert.Nil(t, fileContent) - assert.NotNil(t, dirContents) - assert.Greater(t, len(dirContents), 0) - } - }) - } -} - -func TestGithubClient_GetTree(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - basePath string - ref string - recursive bool - wantItems int - wantTrunc bool - wantErr error - }{ - { - name: "get tree successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("file1.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr("file1sha"), - }, - { - Path: github.Ptr("file2.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(14), - SHA: github.Ptr("file2sha"), - }, - { - Path: github.Ptr("dir"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("dirsha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 3, - wantTrunc: false, - wantErr: nil, - }, - { - name: "get tree with subpath", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if this is the first request for the root tree - if !strings.Contains(r.URL.Path, "subdirsha") { - // Verify the request URL contains the correct owner, repo, and ref - expectedPath := "/repos/test-owner/test-repo/git/trees/main" - assert.True(t, strings.Contains(r.URL.Path, expectedPath), - "Expected URL path to contain %s, got %s", expectedPath, r.URL.Path) - - // Verify query parameters for recursive flag - query := r.URL.Query() - assert.Equal(t, "", query.Get("recursive"), "Recursive parameter should not be set") - } else { - // This is the second request for the subtree - assert.True(t, strings.Contains(r.URL.Path, "subdirsha"), - "Expected URL path to contain subdirsha, got %s", r.URL.Path) - } - // First request for the root tree - tree := &github.Tree{ - SHA: github.Ptr("rootsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("subdir"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("subdirsha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Second request for the subdir tree - if strings.Contains(r.URL.Path, "subdirsha") { - tree := &github.Tree{ - SHA: github.Ptr("subdirsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("file3.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(16), - SHA: github.Ptr("file3sha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - } - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "subdir", - ref: "main", - recursive: false, - wantItems: 1, - wantTrunc: false, - wantErr: nil, - }, - { - name: "subpath not found should pretend is empty", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First request for the root tree - if !strings.Contains(r.URL.Path, "nonexistentsha") { - tree := &github.Tree{ - SHA: github.Ptr("rootsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("nonexistent"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("nonexistentsha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - } else { - // Second request for the nonexistent subtree returns 404 - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not Found", - })) - } - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "nonexistent", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: nil, - }, - { - name: "get tree fails with service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: ErrServiceUnavailable, - }, - { - name: "tree contains too many items", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Create more entries than the maxTreeItems limit - entries := make([]*github.TreeEntry, maxTreeItems+1) - for i := 0; i < maxTreeItems+1; i++ { - entries[i] = &github.TreeEntry{ - Path: github.Ptr(fmt.Sprintf("file%d.txt", i+1)), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr(fmt.Sprintf("sha%d", i+1)), - } - } - - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: entries, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems), - }, - - { - name: "tree is truncated with recursive mode", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("file1.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr("file1sha"), - }, - }, - Truncated: github.Ptr(true), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: true, - wantItems: 0, - wantTrunc: true, - wantErr: fmt.Errorf("tree is too large to fetch recursively (more than 10000 items)"), - }, - { - name: "repository not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not Found", - })) - }), - ), - ), - owner: "test-owner", - repository: "non-existent-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: ErrServiceUnavailable, - }, - { - name: "too many items in tree", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Create a tree with more than maxTreeItems entries - entries := make([]*github.TreeEntry, maxTreeItems+1) - for i := 0; i < maxTreeItems+1; i++ { - entries[i] = &github.TreeEntry{ - Path: github.Ptr(fmt.Sprintf("file%d.txt", i)), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr(fmt.Sprintf("sha%d", i)), - } - } - tree := &github.Tree{ - SHA: github.Ptr("abc123"), - Entries: entries, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: fmt.Errorf("tree contains too many items (more than 10000)"), - }, - { - name: "folder not found in tree", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Return a tree that doesn't contain the requested folder - tree := &github.Tree{ - SHA: github.Ptr("rootsha"), - Entries: []*github.TreeEntry{ - { - Path: github.Ptr("other-folder"), - Mode: github.Ptr("040000"), - Type: github.Ptr("tree"), - SHA: github.Ptr("othersha"), - }, - { - Path: github.Ptr("file.txt"), - Mode: github.Ptr("100644"), - Type: github.Ptr("blob"), - Size: github.Ptr(12), - SHA: github.Ptr("filesha"), - }, - }, - Truncated: github.Ptr(false), - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(tree)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "non-existent-folder/subpath", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: nil, - }, - { - name: "non-standard error is passed through", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposGitTreesByOwnerByRepoByTreeSha, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusForbidden, - }, - Message: "Forbidden access", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - basePath: "", - ref: "main", - recursive: false, - wantItems: 0, - wantTrunc: false, - wantErr: errors.New("Forbidden access"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - contents, truncated, err := client.GetTree(context.Background(), tt.owner, tt.repository, tt.basePath, tt.ref, tt.recursive) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - assert.Nil(t, contents) - return - } - assert.NoError(t, err) - - // Check truncated flag - assert.Equal(t, tt.wantTrunc, truncated) - - // Check the result - if tt.wantItems > 0 { - assert.NotNil(t, contents) - assert.Equal(t, tt.wantItems, len(contents)) - } else { - assert.Empty(t, contents) - } - }) - } -} - -func TestGithubClient_CreateFile(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - branch string - message string - content []byte - wantErr error - }{ - { - name: "create file successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - response := &github.RepositoryContentResponse{ - Content: &github.RepositoryContent{ - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusCreated) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Add test.txt", - content: []byte("test content"), - wantErr: nil, - }, - { - name: "file already exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnprocessableEntity) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusUnprocessableEntity, - }, - Message: "File already exists", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "existing.txt", - branch: "main", - message: "Add existing.txt", - content: []byte("test content"), - wantErr: ErrResourceAlreadyExists, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Add test.txt", - content: []byte("test content"), - wantErr: errors.New("Service unavailable"), - }, - { - name: "not a github error response", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write([]byte("not a github error")) - require.NoError(t, err) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Add test.txt", - content: []byte("test content"), - wantErr: errors.New("500"), - }, - { - name: "default commit message", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Decode the request to verify the message - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var reqData struct { - Message string `json:"message"` - } - require.NoError(t, json.Unmarshal(body, &reqData)) - assert.Equal(t, "Create test.txt", reqData.Message) - - response := &github.RepositoryContentResponse{ - Content: &github.RepositoryContent{ - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusCreated) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "", // Empty message should use default - content: []byte("test content"), - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.CreateFile(context.Background(), tt.owner, tt.repository, tt.path, tt.branch, tt.message, tt.content) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestUpdateFile(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - branch string - message string - hash string - content []byte - wantErr error - }{ - { - name: "successful update", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify request body - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var reqData struct { - Message string `json:"message"` - SHA string `json:"sha"` - } - require.NoError(t, json.Unmarshal(body, &reqData)) - assert.Equal(t, "Update test.txt", reqData.Message) - assert.Equal(t, "abc123", reqData.SHA) - - response := &github.RepositoryContentResponse{ - Content: &github.RepositoryContent{ - Name: github.Ptr("test.txt"), - Path: github.Ptr("test.txt"), - SHA: github.Ptr("def456"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "", // Empty message should use default - hash: "abc123", - content: []byte("updated content"), - wantErr: nil, - }, - { - name: "file not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Not Found"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "nonexistent.txt", - branch: "main", - message: "Update nonexistent file", - hash: "abc123", - content: []byte("content"), - wantErr: ErrResourceNotFound, - }, - { - name: "mismatched hash", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "SHA mismatch"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Update with wrong hash", - hash: "wrong-hash", - content: []byte("content"), - wantErr: ErrMismatchedHash, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Service unavailable"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Update during outage", - hash: "abc123", - content: []byte("content"), - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.PutReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Internal server error"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Update with server error", - hash: "abc123", - content: []byte("content"), - wantErr: errors.New("Internal server error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.UpdateFile(context.Background(), tt.owner, tt.repository, tt.path, tt.branch, tt.message, tt.hash, tt.content) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestGithubClient_DeleteFile(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - path string - branch string - message string - hash string - wantErr error - }{ - { - name: "delete file successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - response := &github.RepositoryContentResponse{ - Content: nil, - Commit: github.Commit{ - SHA: github.Ptr("def456"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete test.txt", - hash: "abc123", - wantErr: nil, - }, - { - name: "file not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not Found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "nonexistent.txt", - branch: "main", - message: "Delete nonexistent.txt", - hash: "abc123", - wantErr: ErrResourceNotFound, - }, - { - name: "mismatched hash", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusConflict) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusConflict, - }, - Message: "Conflict", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete test.txt", - hash: "wrong-hash", - wantErr: ErrMismatchedHash, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete test.txt", - hash: "abc123", - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(map[string]string{"message": "Internal server error"})) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "Delete with server error", - hash: "abc123", - wantErr: errors.New("Internal server error"), - }, - { - name: "default commit message", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.DeleteReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Decode the request to verify the message - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var reqData struct { - Message string `json:"message"` - } - require.NoError(t, json.Unmarshal(body, &reqData)) - assert.Equal(t, "Delete test.txt", reqData.Message) - - response := &github.RepositoryContentResponse{ - Content: nil, - Commit: github.Commit{ - SHA: github.Ptr("def456"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(response)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - path: "test.txt", - branch: "main", - message: "", - hash: "abc123", - wantErr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.DeleteFile(context.Background(), tt.owner, tt.repository, tt.path, tt.branch, tt.message, tt.hash) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } else { - assert.NoError(t, err) - } - }) - } -} - func TestGithubClient_GetCommits(t *testing.T) { tests := []struct { name string @@ -1767,621 +359,6 @@ func TestGithubClient_GetCommits(t *testing.T) { } } -func TestCompareCommits(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - base string - head string - wantFiles []CommitFile - wantErr error - }{ - { - name: "successful comparison", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - files := []*github.CommitFile{ - { - Filename: github.Ptr("file1.txt"), - Status: github.Ptr("modified"), - Additions: github.Ptr(10), - Deletions: github.Ptr(5), - Changes: github.Ptr(15), - }, - { - Filename: github.Ptr("file2.txt"), - Status: github.Ptr("added"), - Additions: github.Ptr(20), - Deletions: github.Ptr(0), - Changes: github.Ptr(20), - }, - } - - require.NoError(t, json.NewEncoder(w).Encode(github.CommitsComparison{ - Files: files, - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: []CommitFile{ - &github.CommitFile{ - Filename: github.Ptr("file1.txt"), - Status: github.Ptr("modified"), - Additions: github.Ptr(10), - Deletions: github.Ptr(5), - Changes: github.Ptr(15), - }, - &github.CommitFile{ - Filename: github.Ptr("file2.txt"), - Status: github.Ptr("added"), - Additions: github.Ptr(20), - Deletions: github.Ptr(0), - Changes: github.Ptr(20), - }, - }, - wantErr: nil, - }, - { - name: "too many files", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - // Generate more files than the max limit - files := make([]*github.CommitFile, maxCompareFiles+1) - for i := 0; i < maxCompareFiles+1; i++ { - files[i] = &github.CommitFile{ - Filename: github.Ptr(fmt.Sprintf("file%d.txt", i)), - Status: github.Ptr("modified"), - } - } - - require.NoError(t, json.NewEncoder(w).Encode(github.CommitsComparison{ - Files: files, - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles), - }, - { - name: "resource not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Not found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposCompareByOwnerByRepoByBasehead, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - base: "main", - head: "feature-branch", - wantFiles: nil, - wantErr: errors.New("Internal server error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - files, err := client.CompareCommits(context.Background(), tt.owner, tt.repository, tt.base, tt.head) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - assert.Nil(t, files) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.wantFiles, files) - } - }) - } -} - -func TestGetBranch(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - branchName string - wantBranch Branch - wantErr error - }{ - { - name: "get branch successfully", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - branch := &github.Branch{ - Name: github.Ptr("main"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "main", - wantBranch: Branch{ - Name: "main", - Sha: "abc123", - }, - wantErr: nil, - }, - { - name: "branch not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Branch not found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "non-existent", - wantBranch: Branch{}, - wantErr: ErrResourceNotFound, - }, - { - name: "service unavailable", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusServiceUnavailable, - }, - Message: "Service unavailable", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "main", - wantBranch: Branch{}, - wantErr: ErrServiceUnavailable, - }, - { - name: "other error", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "main", - wantBranch: Branch{}, - wantErr: errors.New("unexpected status code: 500 Internal Server Error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - branch, err := client.GetBranch(context.Background(), tt.owner, tt.repository, tt.branchName) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - assert.Equal(t, tt.wantBranch, branch) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.wantBranch, branch) - } - }) - } -} - -func TestGithubClient_CreateBranch(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - sourceBranch string - branchName string - wantErr error - }{ - { - name: "successful branch creation", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First call checks if branch exists (should return 404) - if strings.Contains(r.URL.Path, "/new-branch") { - w.WriteHeader(http.StatusNotFound) - return - } - - // Second call gets the source branch - if strings.Contains(r.URL.Path, "/main") { - branch := &github.Branch{ - Name: github.Ptr("main"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - } - }), - ), - mockhub.WithRequestMatchHandler( - mockhub.PostReposGitRefsByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request body contains the correct reference - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - ref := struct { - Ref string `json:"ref"` - SHA string `json:"sha"` - }{} - require.NoError(t, json.Unmarshal(body, &ref)) - assert.Equal(t, "refs/heads/new-branch", ref.Ref) - assert.Equal(t, "abc123", ref.SHA) - - w.WriteHeader(http.StatusCreated) - require.NoError(t, json.NewEncoder(w).Encode(&github.Reference{ - Ref: github.Ptr("refs/heads/new-branch"), - Object: &github.GitObject{ - SHA: github.Ptr("abc123"), - }, - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "main", - branchName: "new-branch", - wantErr: nil, - }, - { - name: "branch already exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request URL contains the correct owner, repo, and branch - expectedPath := "/repos/test-owner/test-repo/branches/existing-branch" - assert.True(t, strings.Contains(r.URL.Path, expectedPath), - "Expected URL path to contain %s, got %s", expectedPath, r.URL.Path) - // Branch exists check returns success - branch := &github.Branch{ - Name: github.Ptr("existing-branch"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "main", - branchName: "existing-branch", - wantErr: ErrResourceAlreadyExists, - }, - { - name: "source branch not found", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First call checks if branch exists (should return 404) - if strings.Contains(r.URL.Path, "/new-branch") { - w.WriteHeader(http.StatusNotFound) - return - } - - // Second call gets the source branch (not found) - if strings.Contains(r.URL.Path, "/nonexistent") { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Branch not found", - })) - } - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "nonexistent", - branchName: "new-branch", - wantErr: errors.New("get base branch"), - }, - { - name: "error creating branch ref", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // First call checks if branch exists (should return 404) - if strings.Contains(r.URL.Path, "/new-branch") { - w.WriteHeader(http.StatusNotFound) - return - } - - // Second call gets the source branch - if strings.Contains(r.URL.Path, "/main") { - branch := &github.Branch{ - Name: github.Ptr("main"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - } - }), - ), - mockhub.WithRequestMatchHandler( - mockhub.PostReposGitRefsByOwnerByRepo, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - sourceBranch: "main", - branchName: "new-branch", - wantErr: errors.New("create branch ref"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - err := client.CreateBranch(context.Background(), tt.owner, tt.repository, tt.sourceBranch, tt.branchName) - - // Check the error - if tt.wantErr != nil { - assert.Error(t, err) - if errors.Is(err, tt.wantErr) { - assert.Equal(t, tt.wantErr, err) - } else { - assert.Contains(t, err.Error(), tt.wantErr.Error()) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestGithubClient_BranchExists(t *testing.T) { - tests := []struct { - name string - mockHandler *http.Client - owner string - repository string - branchName string - want bool - wantErr bool - }{ - { - name: "branch exists", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - branch := &github.Branch{ - Name: github.Ptr("existing-branch"), - Commit: &github.RepositoryCommit{ - SHA: github.Ptr("abc123"), - }, - } - w.WriteHeader(http.StatusOK) - require.NoError(t, json.NewEncoder(w).Encode(branch)) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "existing-branch", - want: true, - wantErr: false, - }, - { - name: "branch does not exist", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusNotFound, - }, - Message: "Branch not found", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "non-existent-branch", - want: false, - wantErr: false, - }, - { - name: "error response", - mockHandler: mockhub.NewMockedHTTPClient( - mockhub.WithRequestMatchHandler( - mockhub.GetReposBranchesByOwnerByRepoByBranch, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{ - Response: &http.Response{ - StatusCode: http.StatusInternalServerError, - }, - Message: "Internal server error", - })) - }), - ), - ), - owner: "test-owner", - repository: "test-repo", - branchName: "some-branch", - want: false, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock client - factory := ProvideFactory() - factory.Client = tt.mockHandler - client := factory.New(context.Background(), "") - - // Call the method being tested - got, err := client.BranchExists(context.Background(), tt.owner, tt.repository, tt.branchName) - - // Check the error - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - - // Check the result - assert.Equal(t, tt.want, got) - }) - } -} func TestGithubClient_ListWebhooks(t *testing.T) { tests := []struct { name string @@ -3721,139 +1698,3 @@ func TestDefaultListOptions(t *testing.T) { }) } } - -func TestRealRepositoryContent(t *testing.T) { - t.Run("IsDirectory", func(t *testing.T) { - tests := []struct { - name string - repoType string - want bool - }{ - { - name: "directory type", - repoType: "dir", - want: true, - }, - { - name: "file type", - repoType: "file", - want: false, - }, - { - name: "empty type", - repoType: "", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - repoType := tt.repoType - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Type: &repoType, - }, - } - got := content.IsDirectory() - assert.Equal(t, tt.want, got) - }) - } - }) - - t.Run("GetFileContent", func(t *testing.T) { - fileContent := "test content" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Content: &fileContent, - }, - } - got, err := content.GetFileContent() - assert.NoError(t, err) - assert.Equal(t, fileContent, got) - }) - - t.Run("IsSymlink", func(t *testing.T) { - tests := []struct { - name string - target *string - want bool - }{ - { - name: "is symlink", - target: github.Ptr("target"), - want: true, - }, - { - name: "not symlink", - target: nil, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Target: tt.target, - }, - } - got := content.IsSymlink() - assert.Equal(t, tt.want, got) - }) - } - }) - - t.Run("GetPath", func(t *testing.T) { - path := "path/to/file" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Path: &path, - }, - } - got := content.GetPath() - assert.Equal(t, path, got) - }) - - t.Run("GetSHA", func(t *testing.T) { - sha := "abc123" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - SHA: &sha, - }, - } - got := content.GetSHA() - assert.Equal(t, sha, got) - }) - - t.Run("GetSize", func(t *testing.T) { - t.Run("with size field", func(t *testing.T) { - size := 42 - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Size: &size, - }, - } - got := content.GetSize() - assert.Equal(t, int64(size), got) - }) - - t.Run("with content field", func(t *testing.T) { - fileContent := "test content" - content := realRepositoryContent{ - real: &github.RepositoryContent{ - Content: &fileContent, - }, - } - got := content.GetSize() - assert.Equal(t, int64(len(fileContent)), got) - }) - - t.Run("with no size or content", func(t *testing.T) { - content := realRepositoryContent{ - real: &github.RepositoryContent{}, - } - got := content.GetSize() - assert.Equal(t, int64(0), got) - }) - }) -} diff --git a/pkg/registry/apis/provisioning/repository/github/mock_client.go b/pkg/registry/apis/provisioning/repository/github/mock_client.go index 6ea39c8a67f..f70aa9f1359 100644 --- a/pkg/registry/apis/provisioning/repository/github/mock_client.go +++ b/pkg/registry/apis/provisioning/repository/github/mock_client.go @@ -21,65 +21,6 @@ func (_m *MockClient) EXPECT() *MockClient_Expecter { return &MockClient_Expecter{mock: &_m.Mock} } -// BranchExists provides a mock function with given fields: ctx, owner, repository, branchName -func (_m *MockClient) BranchExists(ctx context.Context, owner string, repository string, branchName string) (bool, error) { - ret := _m.Called(ctx, owner, repository, branchName) - - if len(ret) == 0 { - panic("no return value specified for BranchExists") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (bool, error)); ok { - return rf(ctx, owner, repository, branchName) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) bool); ok { - r0 = rf(ctx, owner, repository, branchName) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, owner, repository, branchName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_BranchExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BranchExists' -type MockClient_BranchExists_Call struct { - *mock.Call -} - -// BranchExists is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - branchName string -func (_e *MockClient_Expecter) BranchExists(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_BranchExists_Call { - return &MockClient_BranchExists_Call{Call: _e.mock.On("BranchExists", ctx, owner, repository, branchName)} -} - -func (_c *MockClient_BranchExists_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_BranchExists_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockClient_BranchExists_Call) Return(_a0 bool, _a1 error) *MockClient_BranchExists_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_BranchExists_Call) RunAndReturn(run func(context.Context, string, string, string) (bool, error)) *MockClient_BranchExists_Call { - _c.Call.Return(run) - return _c -} - // Commits provides a mock function with given fields: ctx, owner, repository, path, branch func (_m *MockClient) Commits(ctx context.Context, owner string, repository string, path string, branch string) ([]Commit, error) { ret := _m.Called(ctx, owner, repository, path, branch) @@ -142,170 +83,6 @@ func (_c *MockClient_Commits_Call) RunAndReturn(run func(context.Context, string return _c } -// CompareCommits provides a mock function with given fields: ctx, owner, repository, base, head -func (_m *MockClient) CompareCommits(ctx context.Context, owner string, repository string, base string, head string) ([]CommitFile, error) { - ret := _m.Called(ctx, owner, repository, base, head) - - if len(ret) == 0 { - panic("no return value specified for CompareCommits") - } - - var r0 []CommitFile - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]CommitFile, error)); ok { - return rf(ctx, owner, repository, base, head) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []CommitFile); ok { - r0 = rf(ctx, owner, repository, base, head) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]CommitFile) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { - r1 = rf(ctx, owner, repository, base, head) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_CompareCommits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareCommits' -type MockClient_CompareCommits_Call struct { - *mock.Call -} - -// CompareCommits is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - base string -// - head string -func (_e *MockClient_Expecter) CompareCommits(ctx interface{}, owner interface{}, repository interface{}, base interface{}, head interface{}) *MockClient_CompareCommits_Call { - return &MockClient_CompareCommits_Call{Call: _e.mock.On("CompareCommits", ctx, owner, repository, base, head)} -} - -func (_c *MockClient_CompareCommits_Call) Run(run func(ctx context.Context, owner string, repository string, base string, head string)) *MockClient_CompareCommits_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockClient_CompareCommits_Call) Return(_a0 []CommitFile, _a1 error) *MockClient_CompareCommits_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_CompareCommits_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]CommitFile, error)) *MockClient_CompareCommits_Call { - _c.Call.Return(run) - return _c -} - -// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName -func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error { - ret := _m.Called(ctx, owner, repository, sourceBranch, branchName) - - if len(ret) == 0 { - panic("no return value specified for CreateBranch") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { - r0 = rf(ctx, owner, repository, sourceBranch, branchName) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_CreateBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBranch' -type MockClient_CreateBranch_Call struct { - *mock.Call -} - -// CreateBranch is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - sourceBranch string -// - branchName string -func (_e *MockClient_Expecter) CreateBranch(ctx interface{}, owner interface{}, repository interface{}, sourceBranch interface{}, branchName interface{}) *MockClient_CreateBranch_Call { - return &MockClient_CreateBranch_Call{Call: _e.mock.On("CreateBranch", ctx, owner, repository, sourceBranch, branchName)} -} - -func (_c *MockClient_CreateBranch_Call) Run(run func(ctx context.Context, owner string, repository string, sourceBranch string, branchName string)) *MockClient_CreateBranch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockClient_CreateBranch_Call) Return(_a0 error) *MockClient_CreateBranch_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_CreateBranch_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockClient_CreateBranch_Call { - _c.Call.Return(run) - return _c -} - -// CreateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, content -func (_m *MockClient) CreateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte) error { - ret := _m.Called(ctx, owner, repository, path, branch, message, content) - - if len(ret) == 0 { - panic("no return value specified for CreateFile") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, []byte) error); ok { - r0 = rf(ctx, owner, repository, path, branch, message, content) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_CreateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateFile' -type MockClient_CreateFile_Call struct { - *mock.Call -} - -// CreateFile is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - branch string -// - message string -// - content []byte -func (_e *MockClient_Expecter) CreateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, content interface{}) *MockClient_CreateFile_Call { - return &MockClient_CreateFile_Call{Call: _e.mock.On("CreateFile", ctx, owner, repository, path, branch, message, content)} -} - -func (_c *MockClient_CreateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte)) *MockClient_CreateFile_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].([]byte)) - }) - return _c -} - -func (_c *MockClient_CreateFile_Call) Return(_a0 error) *MockClient_CreateFile_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_CreateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, []byte) error) *MockClient_CreateFile_Call { - _c.Call.Return(run) - return _c -} - // CreatePullRequestComment provides a mock function with given fields: ctx, owner, repository, number, body func (_m *MockClient) CreatePullRequestComment(ctx context.Context, owner string, repository string, number int, body string) error { ret := _m.Called(ctx, owner, repository, number, body) @@ -415,58 +192,6 @@ func (_c *MockClient_CreateWebhook_Call) RunAndReturn(run func(context.Context, return _c } -// DeleteFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash -func (_m *MockClient) DeleteFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string) error { - ret := _m.Called(ctx, owner, repository, path, branch, message, hash) - - if len(ret) == 0 { - panic("no return value specified for DeleteFile") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string) error); ok { - r0 = rf(ctx, owner, repository, path, branch, message, hash) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_DeleteFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteFile' -type MockClient_DeleteFile_Call struct { - *mock.Call -} - -// DeleteFile is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - branch string -// - message string -// - hash string -func (_e *MockClient_Expecter) DeleteFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}) *MockClient_DeleteFile_Call { - return &MockClient_DeleteFile_Call{Call: _e.mock.On("DeleteFile", ctx, owner, repository, path, branch, message, hash)} -} - -func (_c *MockClient_DeleteFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string)) *MockClient_DeleteFile_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string)) - }) - return _c -} - -func (_c *MockClient_DeleteFile_Call) Return(_a0 error) *MockClient_DeleteFile_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_DeleteFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string) error) *MockClient_DeleteFile_Call { - _c.Call.Return(run) - return _c -} - // DeleteWebhook provides a mock function with given fields: ctx, owner, repository, webhookID func (_m *MockClient) DeleteWebhook(ctx context.Context, owner string, repository string, webhookID int64) error { ret := _m.Called(ctx, owner, repository, webhookID) @@ -565,206 +290,6 @@ func (_c *MockClient_EditWebhook_Call) RunAndReturn(run func(context.Context, st return _c } -// GetBranch provides a mock function with given fields: ctx, owner, repository, branchName -func (_m *MockClient) GetBranch(ctx context.Context, owner string, repository string, branchName string) (Branch, error) { - ret := _m.Called(ctx, owner, repository, branchName) - - if len(ret) == 0 { - panic("no return value specified for GetBranch") - } - - var r0 Branch - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (Branch, error)); ok { - return rf(ctx, owner, repository, branchName) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) Branch); ok { - r0 = rf(ctx, owner, repository, branchName) - } else { - r0 = ret.Get(0).(Branch) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, owner, repository, branchName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_GetBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBranch' -type MockClient_GetBranch_Call struct { - *mock.Call -} - -// GetBranch is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - branchName string -func (_e *MockClient_Expecter) GetBranch(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_GetBranch_Call { - return &MockClient_GetBranch_Call{Call: _e.mock.On("GetBranch", ctx, owner, repository, branchName)} -} - -func (_c *MockClient_GetBranch_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_GetBranch_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *MockClient_GetBranch_Call) Return(_a0 Branch, _a1 error) *MockClient_GetBranch_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_GetBranch_Call) RunAndReturn(run func(context.Context, string, string, string) (Branch, error)) *MockClient_GetBranch_Call { - _c.Call.Return(run) - return _c -} - -// GetContents provides a mock function with given fields: ctx, owner, repository, path, ref -func (_m *MockClient) GetContents(ctx context.Context, owner string, repository string, path string, ref string) (RepositoryContent, []RepositoryContent, error) { - ret := _m.Called(ctx, owner, repository, path, ref) - - if len(ret) == 0 { - panic("no return value specified for GetContents") - } - - var r0 RepositoryContent - var r1 []RepositoryContent - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)); ok { - return rf(ctx, owner, repository, path, ref) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) RepositoryContent); ok { - r0 = rf(ctx, owner, repository, path, ref) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(RepositoryContent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) []RepositoryContent); ok { - r1 = rf(ctx, owner, repository, path, ref) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).([]RepositoryContent) - } - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string) error); ok { - r2 = rf(ctx, owner, repository, path, ref) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// MockClient_GetContents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContents' -type MockClient_GetContents_Call struct { - *mock.Call -} - -// GetContents is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - ref string -func (_e *MockClient_Expecter) GetContents(ctx interface{}, owner interface{}, repository interface{}, path interface{}, ref interface{}) *MockClient_GetContents_Call { - return &MockClient_GetContents_Call{Call: _e.mock.On("GetContents", ctx, owner, repository, path, ref)} -} - -func (_c *MockClient_GetContents_Call) Run(run func(ctx context.Context, owner string, repository string, path string, ref string)) *MockClient_GetContents_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockClient_GetContents_Call) Return(fileContents RepositoryContent, dirContents []RepositoryContent, err error) *MockClient_GetContents_Call { - _c.Call.Return(fileContents, dirContents, err) - return _c -} - -func (_c *MockClient_GetContents_Call) RunAndReturn(run func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)) *MockClient_GetContents_Call { - _c.Call.Return(run) - return _c -} - -// GetTree provides a mock function with given fields: ctx, owner, repository, basePath, ref, recursive -func (_m *MockClient) GetTree(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool) ([]RepositoryContent, bool, error) { - ret := _m.Called(ctx, owner, repository, basePath, ref, recursive) - - if len(ret) == 0 { - panic("no return value specified for GetTree") - } - - var r0 []RepositoryContent - var r1 bool - var r2 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)); ok { - return rf(ctx, owner, repository, basePath, ref, recursive) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) []RepositoryContent); ok { - r0 = rf(ctx, owner, repository, basePath, ref, recursive) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]RepositoryContent) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool) bool); ok { - r1 = rf(ctx, owner, repository, basePath, ref, recursive) - } else { - r1 = ret.Get(1).(bool) - } - - if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string, bool) error); ok { - r2 = rf(ctx, owner, repository, basePath, ref, recursive) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// MockClient_GetTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTree' -type MockClient_GetTree_Call struct { - *mock.Call -} - -// GetTree is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - basePath string -// - ref string -// - recursive bool -func (_e *MockClient_Expecter) GetTree(ctx interface{}, owner interface{}, repository interface{}, basePath interface{}, ref interface{}, recursive interface{}) *MockClient_GetTree_Call { - return &MockClient_GetTree_Call{Call: _e.mock.On("GetTree", ctx, owner, repository, basePath, ref, recursive)} -} - -func (_c *MockClient_GetTree_Call) Run(run func(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool)) *MockClient_GetTree_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(bool)) - }) - return _c -} - -func (_c *MockClient_GetTree_Call) Return(entries []RepositoryContent, truncated bool, err error) *MockClient_GetTree_Call { - _c.Call.Return(entries, truncated, err) - return _c -} - -func (_c *MockClient_GetTree_Call) RunAndReturn(run func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)) *MockClient_GetTree_Call { - _c.Call.Return(run) - return _c -} - // GetWebhook provides a mock function with given fields: ctx, owner, repository, webhookID func (_m *MockClient) GetWebhook(ctx context.Context, owner string, repository string, webhookID int64) (WebhookConfig, error) { ret := _m.Called(ctx, owner, repository, webhookID) @@ -824,52 +349,6 @@ func (_c *MockClient_GetWebhook_Call) RunAndReturn(run func(context.Context, str return _c } -// IsAuthenticated provides a mock function with given fields: ctx -func (_m *MockClient) IsAuthenticated(ctx context.Context) error { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for IsAuthenticated") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_IsAuthenticated_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAuthenticated' -type MockClient_IsAuthenticated_Call struct { - *mock.Call -} - -// IsAuthenticated is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockClient_Expecter) IsAuthenticated(ctx interface{}) *MockClient_IsAuthenticated_Call { - return &MockClient_IsAuthenticated_Call{Call: _e.mock.On("IsAuthenticated", ctx)} -} - -func (_c *MockClient_IsAuthenticated_Call) Run(run func(ctx context.Context)) *MockClient_IsAuthenticated_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockClient_IsAuthenticated_Call) Return(_a0 error) *MockClient_IsAuthenticated_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_IsAuthenticated_Call) RunAndReturn(run func(context.Context) error) *MockClient_IsAuthenticated_Call { - _c.Call.Return(run) - return _c -} - // ListPullRequestFiles provides a mock function with given fields: ctx, owner, repository, number func (_m *MockClient) ListPullRequestFiles(ctx context.Context, owner string, repository string, number int) ([]CommitFile, error) { ret := _m.Called(ctx, owner, repository, number) @@ -991,117 +470,6 @@ func (_c *MockClient_ListWebhooks_Call) RunAndReturn(run func(context.Context, s return _c } -// RepoExists provides a mock function with given fields: ctx, owner, repository -func (_m *MockClient) RepoExists(ctx context.Context, owner string, repository string) (bool, error) { - ret := _m.Called(ctx, owner, repository) - - if len(ret) == 0 { - panic("no return value specified for RepoExists") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { - return rf(ctx, owner, repository) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { - r0 = rf(ctx, owner, repository) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, owner, repository) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockClient_RepoExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RepoExists' -type MockClient_RepoExists_Call struct { - *mock.Call -} - -// RepoExists is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -func (_e *MockClient_Expecter) RepoExists(ctx interface{}, owner interface{}, repository interface{}) *MockClient_RepoExists_Call { - return &MockClient_RepoExists_Call{Call: _e.mock.On("RepoExists", ctx, owner, repository)} -} - -func (_c *MockClient_RepoExists_Call) Run(run func(ctx context.Context, owner string, repository string)) *MockClient_RepoExists_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *MockClient_RepoExists_Call) Return(_a0 bool, _a1 error) *MockClient_RepoExists_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockClient_RepoExists_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *MockClient_RepoExists_Call { - _c.Call.Return(run) - return _c -} - -// UpdateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash, content -func (_m *MockClient) UpdateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte) error { - ret := _m.Called(ctx, owner, repository, path, branch, message, hash, content) - - if len(ret) == 0 { - panic("no return value specified for UpdateFile") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, []byte) error); ok { - r0 = rf(ctx, owner, repository, path, branch, message, hash, content) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockClient_UpdateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateFile' -type MockClient_UpdateFile_Call struct { - *mock.Call -} - -// UpdateFile is a helper method to define mock.On call -// - ctx context.Context -// - owner string -// - repository string -// - path string -// - branch string -// - message string -// - hash string -// - content []byte -func (_e *MockClient_Expecter) UpdateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}, content interface{}) *MockClient_UpdateFile_Call { - return &MockClient_UpdateFile_Call{Call: _e.mock.On("UpdateFile", ctx, owner, repository, path, branch, message, hash, content)} -} - -func (_c *MockClient_UpdateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte)) *MockClient_UpdateFile_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string), args[7].([]byte)) - }) - return _c -} - -func (_c *MockClient_UpdateFile_Call) Return(_a0 error) *MockClient_UpdateFile_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockClient_UpdateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string, []byte) error) *MockClient_UpdateFile_Call { - _c.Call.Return(run) - return _c -} - // NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockClient(t interface { diff --git a/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go b/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go deleted file mode 100644 index bddbaaf2558..00000000000 --- a/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go +++ /dev/null @@ -1,312 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package github - -import mock "github.com/stretchr/testify/mock" - -// MockRepositoryContent is an autogenerated mock type for the RepositoryContent type -type MockRepositoryContent struct { - mock.Mock -} - -type MockRepositoryContent_Expecter struct { - mock *mock.Mock -} - -func (_m *MockRepositoryContent) EXPECT() *MockRepositoryContent_Expecter { - return &MockRepositoryContent_Expecter{mock: &_m.Mock} -} - -// GetFileContent provides a mock function with no fields -func (_m *MockRepositoryContent) GetFileContent() (string, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetFileContent") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func() (string, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRepositoryContent_GetFileContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFileContent' -type MockRepositoryContent_GetFileContent_Call struct { - *mock.Call -} - -// GetFileContent is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetFileContent() *MockRepositoryContent_GetFileContent_Call { - return &MockRepositoryContent_GetFileContent_Call{Call: _e.mock.On("GetFileContent")} -} - -func (_c *MockRepositoryContent_GetFileContent_Call) Run(run func()) *MockRepositoryContent_GetFileContent_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetFileContent_Call) Return(_a0 string, _a1 error) *MockRepositoryContent_GetFileContent_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockRepositoryContent_GetFileContent_Call) RunAndReturn(run func() (string, error)) *MockRepositoryContent_GetFileContent_Call { - _c.Call.Return(run) - return _c -} - -// GetPath provides a mock function with no fields -func (_m *MockRepositoryContent) GetPath() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetPath") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// MockRepositoryContent_GetPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPath' -type MockRepositoryContent_GetPath_Call struct { - *mock.Call -} - -// GetPath is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetPath() *MockRepositoryContent_GetPath_Call { - return &MockRepositoryContent_GetPath_Call{Call: _e.mock.On("GetPath")} -} - -func (_c *MockRepositoryContent_GetPath_Call) Run(run func()) *MockRepositoryContent_GetPath_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetPath_Call) Return(_a0 string) *MockRepositoryContent_GetPath_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_GetPath_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetPath_Call { - _c.Call.Return(run) - return _c -} - -// GetSHA provides a mock function with no fields -func (_m *MockRepositoryContent) GetSHA() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetSHA") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// MockRepositoryContent_GetSHA_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSHA' -type MockRepositoryContent_GetSHA_Call struct { - *mock.Call -} - -// GetSHA is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetSHA() *MockRepositoryContent_GetSHA_Call { - return &MockRepositoryContent_GetSHA_Call{Call: _e.mock.On("GetSHA")} -} - -func (_c *MockRepositoryContent_GetSHA_Call) Run(run func()) *MockRepositoryContent_GetSHA_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetSHA_Call) Return(_a0 string) *MockRepositoryContent_GetSHA_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_GetSHA_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetSHA_Call { - _c.Call.Return(run) - return _c -} - -// GetSize provides a mock function with no fields -func (_m *MockRepositoryContent) GetSize() int64 { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetSize") - } - - var r0 int64 - if rf, ok := ret.Get(0).(func() int64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int64) - } - - return r0 -} - -// MockRepositoryContent_GetSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSize' -type MockRepositoryContent_GetSize_Call struct { - *mock.Call -} - -// GetSize is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) GetSize() *MockRepositoryContent_GetSize_Call { - return &MockRepositoryContent_GetSize_Call{Call: _e.mock.On("GetSize")} -} - -func (_c *MockRepositoryContent_GetSize_Call) Run(run func()) *MockRepositoryContent_GetSize_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_GetSize_Call) Return(_a0 int64) *MockRepositoryContent_GetSize_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_GetSize_Call) RunAndReturn(run func() int64) *MockRepositoryContent_GetSize_Call { - _c.Call.Return(run) - return _c -} - -// IsDirectory provides a mock function with no fields -func (_m *MockRepositoryContent) IsDirectory() bool { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for IsDirectory") - } - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// MockRepositoryContent_IsDirectory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDirectory' -type MockRepositoryContent_IsDirectory_Call struct { - *mock.Call -} - -// IsDirectory is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) IsDirectory() *MockRepositoryContent_IsDirectory_Call { - return &MockRepositoryContent_IsDirectory_Call{Call: _e.mock.On("IsDirectory")} -} - -func (_c *MockRepositoryContent_IsDirectory_Call) Run(run func()) *MockRepositoryContent_IsDirectory_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_IsDirectory_Call) Return(_a0 bool) *MockRepositoryContent_IsDirectory_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_IsDirectory_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsDirectory_Call { - _c.Call.Return(run) - return _c -} - -// IsSymlink provides a mock function with no fields -func (_m *MockRepositoryContent) IsSymlink() bool { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for IsSymlink") - } - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// MockRepositoryContent_IsSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsSymlink' -type MockRepositoryContent_IsSymlink_Call struct { - *mock.Call -} - -// IsSymlink is a helper method to define mock.On call -func (_e *MockRepositoryContent_Expecter) IsSymlink() *MockRepositoryContent_IsSymlink_Call { - return &MockRepositoryContent_IsSymlink_Call{Call: _e.mock.On("IsSymlink")} -} - -func (_c *MockRepositoryContent_IsSymlink_Call) Run(run func()) *MockRepositoryContent_IsSymlink_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockRepositoryContent_IsSymlink_Call) Return(_a0 bool) *MockRepositoryContent_IsSymlink_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepositoryContent_IsSymlink_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsSymlink_Call { - _c.Call.Return(run) - return _c -} - -// NewMockRepositoryContent creates a new instance of MockRepositoryContent. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockRepositoryContent(t interface { - mock.TestingT - Cleanup(func()) -}) *MockRepositoryContent { - mock := &MockRepositoryContent{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/github/mutator.go b/pkg/registry/apis/provisioning/repository/github/mutator.go new file mode 100644 index 00000000000..2c7c4a57827 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/mutator.go @@ -0,0 +1,36 @@ +package github + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" +) + +func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { + return func(ctx context.Context, obj runtime.Object) error { + repo, ok := obj.(*provisioning.Repository) + if !ok { + return nil + } + + if repo.Spec.GitHub == nil { + return nil + } + + if repo.Spec.GitHub.Token != "" { + secretName := repo.Name + githubTokenSecretSuffix + nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.GitHub.Token) + if err != nil { + return err + } + repo.Spec.GitHub.EncryptedToken = nameOrValue + repo.Spec.GitHub.Token = "" + } + + return nil + } +} diff --git a/pkg/registry/apis/provisioning/repository/github/mutator_test.go b/pkg/registry/apis/provisioning/repository/github/mutator_test.go new file mode 100644 index 00000000000..c60d53b5e88 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/mutator_test.go @@ -0,0 +1,159 @@ +package github + +import ( + "context" + "errors" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestMutator(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + token string + setupMocks func(*secrets.MockRepositorySecrets) + expectedToken string + expectedEncryptedToken string + expectedError string + }{ + { + name: "successful token encryption", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Encrypt( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + "test-repo"+githubTokenSecretSuffix, + "secret-token", + ).Return([]byte("encrypted-token"), nil) + }, + expectedToken: "", + expectedEncryptedToken: "encrypted-token", + }, + { + name: "encryption error", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Encrypt( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Token: "secret-token", + }, + }, + }, + "test-repo"+githubTokenSecretSuffix, + "secret-token", + ).Return(nil, errors.New("encryption failed")) + }, + expectedError: "encryption failed", + }, + { + name: "no github spec", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }, + setupMocks: func(_ *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + { + name: "empty token", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Token: "", + }, + }, + }, + setupMocks: func(_ *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + { + name: "non-repository object", + obj: &runtime.Unknown{}, + setupMocks: func(_ *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMocks(mockSecrets) + + mutator := Mutator(mockSecrets) + err := mutator(context.Background(), tt.obj) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + + // Check that token was cleared and encrypted token was set + if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.GitHub != nil { + if tt.expectedEncryptedToken != "" { + // Token should be cleared after encryption + assert.Empty(t, repo.Spec.GitHub.Token, "Token should be cleared after encryption") + // EncryptedToken should be set to the expected value + assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.GitHub.EncryptedToken), "EncryptedToken should match expected value") + } + } + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/github/repository.go b/pkg/registry/apis/provisioning/repository/github/repository.go new file mode 100644 index 00000000000..e7aed375242 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/repository.go @@ -0,0 +1,283 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/grafana/grafana-app-sdk/logging" + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" +) + +//nolint:gosec // This is a constant for a secret suffix +const githubTokenSecretSuffix = "-github-token" + +// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. +type githubRepository struct { + gitRepo git.GitRepository + config *provisioning.Repository + gh Client // assumes github.com base URL + secrets secrets.RepositorySecrets + + owner string + repo string +} + +// GithubRepository is an interface that combines all repository capabilities +// needed for GitHub repositories. + +//go:generate mockery --name GithubRepository --structname MockGithubRepository --inpackage --filename github_repository_mock.go --with-expecter +type GithubRepository interface { + repository.Repository + repository.Versioned + repository.Writer + repository.Reader + repository.RepositoryWithURLs + repository.StageableRepository + repository.Hooks + Owner() string + Repo() string + Client() Client +} + +func NewGitHub( + ctx context.Context, + config *provisioning.Repository, + gitRepo git.GitRepository, + factory *Factory, + token string, + secrets secrets.RepositorySecrets, +) (GithubRepository, error) { + owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL) + if err != nil { + return nil, fmt.Errorf("parse owner and repo: %w", err) + } + + return &githubRepository{ + config: config, + gitRepo: gitRepo, + gh: factory.New(ctx, token), // TODO, baseURL from config + owner: owner, + repo: repo, + secrets: secrets, + }, nil +} + +func (r *githubRepository) Config() *provisioning.Repository { + return r.gitRepo.Config() +} + +func (r *githubRepository) Owner() string { + return r.owner +} + +func (r *githubRepository) Repo() string { + return r.repo +} + +func (r *githubRepository) Client() Client { + return r.gh +} + +// Validate implements provisioning.Repository. +func (r *githubRepository) Validate() (list field.ErrorList) { + cfg := r.gitRepo.Config() + gh := cfg.Spec.GitHub + if gh == nil { + list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) + return list + } + if gh.URL == "" { + list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) + } else { + _, _, err := ParseOwnerRepoGithub(gh.URL) + if err != nil { + list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) + } else if !strings.HasPrefix(gh.URL, "https://github.com/") { + list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) + } + } + + if len(list) > 0 { + return list + } + + return r.gitRepo.Validate() +} + +func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) { + parsed, e := url.Parse(strings.TrimSuffix(giturl, ".git")) + if e != nil { + err = e + return + } + parts := strings.Split(parsed.Path, "/") + if len(parts) < 3 { + err = fmt.Errorf("unable to parse repo+owner from url") + return + } + return parts[1], parts[2], nil +} + +// Test implements provisioning.Repository. +func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { + url := r.config.Spec.GitHub.URL + _, _, err := ParseOwnerRepoGithub(url) + if err != nil { + return repository.FromFieldError(field.Invalid( + field.NewPath("spec", "github", "url"), url, err.Error())), nil + } + + return r.gitRepo.Test(ctx) +} + +// ReadResource implements provisioning.Repository. +func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { + return r.gitRepo.Read(ctx, filePath, ref) +} + +func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { + return r.gitRepo.ReadTree(ctx, ref) +} + +func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { + return r.gitRepo.Create(ctx, path, ref, data, comment) +} + +func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { + return r.gitRepo.Update(ctx, path, ref, data, comment) +} + +func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { + return r.gitRepo.Write(ctx, path, ref, data, message) +} + +func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { + return r.gitRepo.Delete(ctx, path, ref, comment) +} + +func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { + if ref == "" { + ref = r.config.Spec.GitHub.Branch + } + + finalPath := safepath.Join(r.config.Spec.GitHub.Path, path) + commits, err := r.gh.Commits(ctx, r.owner, r.repo, finalPath, ref) + if err != nil { + if errors.Is(err, ErrResourceNotFound) { + return nil, repository.ErrFileNotFound + } + + return nil, fmt.Errorf("get commits: %w", err) + } + + ret := make([]provisioning.HistoryItem, 0, len(commits)) + for _, commit := range commits { + authors := make([]provisioning.Author, 0) + if commit.Author != nil { + authors = append(authors, provisioning.Author{ + Name: commit.Author.Name, + Username: commit.Author.Username, + AvatarURL: commit.Author.AvatarURL, + }) + } + + if commit.Committer != nil && commit.Author != nil && commit.Author.Name != commit.Committer.Name { + authors = append(authors, provisioning.Author{ + Name: commit.Committer.Name, + Username: commit.Committer.Username, + AvatarURL: commit.Committer.AvatarURL, + }) + } + + ret = append(ret, provisioning.HistoryItem{ + Ref: commit.Ref, + Message: commit.Message, + Authors: authors, + CreatedAt: commit.CreatedAt.UnixMilli(), + }) + } + + return ret, nil +} + +// ListRefs list refs from the git repository and add the ref URL to the ref item +func (r *githubRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) { + refs, err := r.gitRepo.ListRefs(ctx) + if err != nil { + return nil, fmt.Errorf("list refs: %w", err) + } + + for i := range refs { + refs[i].RefURL = fmt.Sprintf("%s/tree/%s", r.config.Spec.GitHub.URL, refs[i].Name) + } + + return refs, nil +} + +func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { + return r.gitRepo.LatestRef(ctx) +} + +func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) { + return r.gitRepo.CompareFiles(ctx, base, ref) +} + +// ResourceURLs implements RepositoryWithURLs. +func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.ResourceURLs, error) { + cfg := r.config.Spec.GitHub + if file.Path == "" || cfg == nil { + return nil, nil + } + + ref := file.Ref + if ref == "" { + ref = cfg.Branch + } + + urls := &provisioning.ResourceURLs{ + RepositoryURL: cfg.URL, + SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path), + } + + if ref != cfg.Branch { + urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref) + + // Create a new pull request + urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL) + } + + return urls, nil +} + +func (r *githubRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { + return r.gitRepo.Stage(ctx, opts) +} + +func (r *githubRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) { + return nil, nil +} + +func (r *githubRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) { + return nil, nil +} + +func (r *githubRepository) OnDelete(ctx context.Context) error { + logger := logging.FromContext(ctx) + secretName := r.config.Name + githubTokenSecretSuffix + if err := r.secrets.Delete(ctx, r.config, secretName); err != nil { + return fmt.Errorf("delete github token secret: %w", err) + } + + logger.Info("Deleted github token secret", "secretName", secretName) + + return nil +} diff --git a/pkg/registry/apis/provisioning/repository/github/repository_test.go b/pkg/registry/apis/provisioning/repository/github/repository_test.go new file mode 100644 index 00000000000..2301156529e --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/repository_test.go @@ -0,0 +1,1128 @@ +package github + +import ( + "context" + "errors" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + field "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" +) + +func TestNewGitHub(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + token string + expectedError string + expectedOwner string + expectedRepo string + }{ + { + name: "successful creation", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + token: "token123", + expectedError: "", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + { + name: "invalid URL format", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + }, + }, + }, + token: "token123", + expectedError: "parse owner and repo", + }, + { + name: "URL with .git extension", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana.git", + Branch: "main", + }, + }, + }, + token: "token123", + expectedError: "", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := ProvideFactory() + factory.Client = http.DefaultClient + + gitRepo := git.NewMockGitRepository(t) + + mockSecrets := secrets.NewMockRepositorySecrets(t) + + // Call the function under test + repo, err := NewGitHub( + context.Background(), + tt.config, + gitRepo, + factory, + tt.token, + mockSecrets, + ) + + // Check results + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, repo) + } else { + require.NoError(t, err) + require.NotNil(t, repo) + assert.Equal(t, tt.expectedOwner, repo.Owner()) + assert.Equal(t, tt.expectedRepo, repo.Repo()) + concreteRepo, ok := repo.(*githubRepository) + require.True(t, ok) + assert.Equal(t, gitRepo, concreteRepo.gitRepo) + } + }) + } +} + +func TestParseOwnerRepoGithub(t *testing.T) { + tests := []struct { + name string + url string + expectedOwner string + expectedRepo string + expectedError string + }{ + { + name: "valid GitHub URL", + url: "https://github.com/grafana/grafana", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + { + name: "valid GitHub URL with .git", + url: "https://github.com/grafana/grafana.git", + expectedOwner: "grafana", + expectedRepo: "grafana", + }, + { + name: "invalid URL format", + url: "invalid-url", + expectedError: "parse", + }, + { + name: "missing repo name", + url: "https://github.com/grafana", + expectedError: "unable to parse repo+owner from url", + }, + { + name: "URL with special characters", + url: "https://github.com/user%", + expectedError: "parse", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + owner, repo, err := ParseOwnerRepoGithub(tt.url) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedOwner, owner) + assert.Equal(t, tt.expectedRepo, repo) + } + }) + } +} + +func TestGitHubRepositoryValidate(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + mockSetup func(m *git.MockGitRepository) + expectedErrors int + errorFields []string + }{ + { + name: "valid configuration", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + Path: "dashboards", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + Path: "dashboards", + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + }, + expectedErrors: 0, + }, + { + name: "missing GitHub config", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github"}, + }, + { + name: "missing URL", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "", + Branch: "main", + Token: "valid-token", + }, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github.url"}, + }, + { + name: "invalid URL format", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + Token: "valid-token", + }, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github.url"}, + }, + { + name: "non-GitHub URL", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://gitlab.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://gitlab.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + }, + }, + }) + }, + expectedErrors: 1, + errorFields: []string{"spec.github.url"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + if tt.mockSetup != nil { + tt.mockSetup(mockGitRepo) + } + + repo := &githubRepository{ + config: tt.config, + gitRepo: mockGitRepo, + } + + errors := repo.Validate() + + assert.Equal(t, tt.expectedErrors, len(errors), "Expected %d errors, got %d, errors: %v", tt.expectedErrors, len(errors), errors) + + if tt.expectedErrors > 0 { + errorFields := make([]string, 0, len(errors)) + for _, err := range errors { + errorFields = append(errorFields, err.Field) + } + for _, expectedField := range tt.errorFields { + assert.Contains(t, errorFields, expectedField, "Expected error for field %s", expectedField) + } + } + + mockGitRepo.AssertExpectations(t) + }) + } +} + +func TestGitHubRepositoryTest(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + mockSetup func(m *git.MockGitRepository) + expectedResult *provisioning.TestResults + expectedError error + }{ + { + name: "successful test", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(m *git.MockGitRepository) { + m.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil) + }, + expectedResult: &provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, + }, + { + name: "invalid URL", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "invalid-url", + Branch: "main", + Token: "valid-token", + }, + }, + }, + mockSetup: func(_ *git.MockGitRepository) { + // No mock calls expected as validation fails first + }, + expectedResult: &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "spec.github.url", + Detail: "parse \"invalid-url\": invalid URI for request", + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + if tt.mockSetup != nil { + tt.mockSetup(mockGitRepo) + } + + repo := &githubRepository{ + config: tt.config, + gitRepo: mockGitRepo, + owner: "grafana", + repo: "grafana", + } + + result, err := repo.Test(context.Background()) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + + if tt.expectedResult != nil { + assert.Equal(t, tt.expectedResult.Code, result.Code) + assert.Equal(t, tt.expectedResult.Success, result.Success) + if len(tt.expectedResult.Errors) > 0 { + assert.Equal(t, len(tt.expectedResult.Errors), len(result.Errors)) + for i, expectedError := range tt.expectedResult.Errors { + assert.Equal(t, expectedError.Type, result.Errors[i].Type) + assert.Equal(t, expectedError.Field, result.Errors[i].Field) + assert.Contains(t, result.Errors[i].Detail, "parse") + } + } + } + + mockGitRepo.AssertExpectations(t) + }) + } +} + +func TestGitHubRepositoryHistory(t *testing.T) { + tests := []struct { + name string + config *provisioning.Repository + path string + ref string + mockSetup func(m *MockClient) + expectedResult []provisioning.HistoryItem + expectedError error + }{ + { + name: "successful history retrieval", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + Committer: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{ + { + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "file not found", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "nonexistent.json", + ref: "main", + mockSetup: func(m *MockClient) { + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/nonexistent.json", "main"). + Return(nil, ErrResourceNotFound) + }, + expectedError: repository.ErrFileNotFound, + }, + { + name: "use default branch when ref is empty", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{ + { + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "committer different from author", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: &CommitAuthor{ + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + Committer: &CommitAuthor{ + Name: "Jane Smith", + Username: "janesmith", + AvatarURL: "https://example.com/avatar2.png", + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{ + { + Name: "John Doe", + Username: "johndoe", + AvatarURL: "https://example.com/avatar1.png", + }, + { + Name: "Jane Smith", + Username: "janesmith", + AvatarURL: "https://example.com/avatar2.png", + }, + }, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "commit with no author", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + commits := []Commit{ + { + Ref: "abc123", + Message: "Update dashboard", + Author: nil, + Committer: nil, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(commits, nil) + }, + expectedResult: []provisioning.HistoryItem{ + { + Ref: "abc123", + Message: "Update dashboard", + Authors: []provisioning.Author{}, + CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), + }, + }, + }, + { + name: "other API error", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + Path: "dashboards", + }, + }, + }, + path: "dashboard.json", + ref: "main", + mockSetup: func(m *MockClient) { + m.On("Commits", mock.Anything, "grafana", "grafana", "dashboards/dashboard.json", "main"). + Return(nil, errors.New("API error")) + }, + expectedError: errors.New("get commits: API error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := NewMockClient(t) + if tt.mockSetup != nil { + tt.mockSetup(mockClient) + } + + repo := &githubRepository{ + config: tt.config, + gh: mockClient, + owner: "grafana", + repo: "grafana", + } + + history, err := repo.History(context.Background(), tt.path, tt.ref) + + if tt.expectedError != nil { + require.Error(t, err) + var statusErr *apierrors.StatusError + if errors.As(tt.expectedError, &statusErr) { + var actualStatusErr *apierrors.StatusError + require.True(t, errors.As(err, &actualStatusErr)) + require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) + } else { + require.Equal(t, tt.expectedError.Error(), err.Error()) + } + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedResult, history) + } + + mockClient.AssertExpectations(t) + }) + } +} + +func TestGitHubRepositoryResourceURLs(t *testing.T) { + tests := []struct { + name string + file *repository.FileInfo + config *provisioning.Repository + expectedURLs *provisioning.ResourceURLs + expectedError error + }{ + { + name: "file with ref", + file: &repository.FileInfo{ + Path: "dashboards/test.json", + Ref: "feature-branch", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + expectedURLs: &provisioning.ResourceURLs{ + RepositoryURL: "https://github.com/grafana/grafana", + SourceURL: "https://github.com/grafana/grafana/blob/feature-branch/dashboards/test.json", + CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch", + NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana", + }, + }, + { + name: "file without ref uses default branch", + file: &repository.FileInfo{ + Path: "dashboards/test.json", + Ref: "", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + expectedURLs: &provisioning.ResourceURLs{ + RepositoryURL: "https://github.com/grafana/grafana", + SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", + }, + }, + { + name: "empty path returns nil", + file: &repository.FileInfo{ + Path: "", + Ref: "feature-branch", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, + expectedURLs: nil, + }, + { + name: "nil github config returns nil", + file: &repository.FileInfo{ + Path: "dashboards/test.json", + Ref: "feature-branch", + }, + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: nil, + }, + }, + expectedURLs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &githubRepository{ + config: tt.config, + owner: "grafana", + repo: "grafana", + } + + urls, err := repo.ResourceURLs(context.Background(), tt.file) + + if tt.expectedError != nil { + require.Error(t, err) + require.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedURLs, urls) + } + }) + } +} + +// Test simple delegation functions +func TestGitHubRepositoryDelegation(t *testing.T) { + ctx := context.Background() + + config := &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "test-token", + }, + }, + } + + t.Run("Config delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + mockGitRepo.On("Config").Return(config) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result := repo.Config() + assert.Equal(t, config, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Read delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedFileInfo := &repository.FileInfo{ + Path: "test.yaml", + Data: []byte("test data"), + Ref: "main", + Hash: "abc123", + } + mockGitRepo.On("Read", ctx, "test.yaml", "main").Return(expectedFileInfo, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.Read(ctx, "test.yaml", "main") + require.NoError(t, err) + assert.Equal(t, expectedFileInfo, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("ReadTree delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedEntries := []repository.FileTreeEntry{ + {Path: "file1.yaml", Size: 100, Hash: "hash1", Blob: true}, + } + mockGitRepo.On("ReadTree", ctx, "main").Return(expectedEntries, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.ReadTree(ctx, "main") + require.NoError(t, err) + assert.Equal(t, expectedEntries, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Create delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + data := []byte("test content") + mockGitRepo.On("Create", ctx, "new-file.yaml", "main", data, "Create new file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Update delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + data := []byte("updated content") + mockGitRepo.On("Update", ctx, "existing-file.yaml", "main", data, "Update file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Write delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + data := []byte("file content") + mockGitRepo.On("Write", ctx, "file.yaml", "main", data, "Write file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Write(ctx, "file.yaml", "main", data, "Write file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Delete delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + mockGitRepo.On("Delete", ctx, "file.yaml", "main", "Delete file").Return(nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + err := repo.Delete(ctx, "file.yaml", "main", "Delete file") + require.NoError(t, err) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("LatestRef delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedRef := "abc123def456" + mockGitRepo.On("LatestRef", ctx).Return(expectedRef, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.LatestRef(ctx) + require.NoError(t, err) + assert.Equal(t, expectedRef, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("ListRefs delegates to git repo but adds ref URL", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + // The git repo returns refs without RefURL + gitRepoRefs := []provisioning.RefItem{ + {Name: "main", Hash: "abc123def456"}, + {Name: "feature", Hash: "def456ghi789"}, + } + mockGitRepo.On("ListRefs", ctx).Return(gitRepoRefs, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.ListRefs(ctx) + require.NoError(t, err) + + // The returned refs should have RefURL set + expectedRefs := []provisioning.RefItem{ + { + Name: "main", + Hash: "abc123def456", + RefURL: "https://github.com/grafana/grafana/tree/main", + }, + { + Name: "feature", + Hash: "def456ghi789", + RefURL: "https://github.com/grafana/grafana/tree/feature", + }, + } + assert.Equal(t, expectedRefs, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("CompareFiles delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + expectedChanges := []repository.VersionedFileChange{ + { + Action: repository.FileActionCreated, + Path: "new-file.yaml", + Ref: "feature-branch", + }, + } + mockGitRepo.On("CompareFiles", ctx, "main", "feature-branch").Return(expectedChanges, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.CompareFiles(ctx, "main", "feature-branch") + require.NoError(t, err) + assert.Equal(t, expectedChanges, result) + mockGitRepo.AssertExpectations(t) + }) + + t.Run("Stage delegates to git repo", func(t *testing.T) { + mockGitRepo := git.NewMockGitRepository(t) + mockStagedRepo := repository.NewMockStagedRepository(t) + opts := repository.StageOptions{ + PushOnWrites: true, + Timeout: 10 * time.Second, + } + mockGitRepo.On("Stage", ctx, opts).Return(mockStagedRepo, nil) + + repo := &githubRepository{ + config: config, + gitRepo: mockGitRepo, + } + + result, err := repo.Stage(ctx, opts) + require.NoError(t, err) + assert.Equal(t, mockStagedRepo, result) + mockGitRepo.AssertExpectations(t) + }) +} + +// Test GitHub-specific accessor methods +func TestGitHubRepositoryAccessors(t *testing.T) { + config := &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/grafana/grafana", + Branch: "main", + Token: "test-token", + }, + }, + } + + t.Run("Owner returns correct owner", func(t *testing.T) { + repo := &githubRepository{ + config: config, + owner: "grafana", + repo: "grafana", + } + + result := repo.Owner() + assert.Equal(t, "grafana", result) + }) + + t.Run("Repo returns correct repo", func(t *testing.T) { + repo := &githubRepository{ + config: config, + owner: "grafana", + repo: "grafana", + } + + result := repo.Repo() + assert.Equal(t, "grafana", result) + }) + + t.Run("Client returns correct client", func(t *testing.T) { + mockClient := NewMockClient(t) + + repo := &githubRepository{ + config: config, + gh: mockClient, + owner: "grafana", + repo: "grafana", + } + + result := repo.Client() + assert.Equal(t, mockClient, result) + }) +} + +func TestGitHubRepository_OnDelete(t *testing.T) { + tests := []struct { + name string + setupMock func(*secrets.MockRepositorySecrets) + config *provisioning.Repository + expectedError string + }{ + { + name: "successful secret deletion", + setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Delete( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + "test-repo"+githubTokenSecretSuffix, + ).Return(nil) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + }, + { + name: "secret deletion error", + setupMock: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Delete( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + "test-repo"+githubTokenSecretSuffix, + ).Return(errors.New("failed to delete secret")) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + }, + expectedError: "delete github token secret: failed to delete secret", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMock(mockSecrets) + + githubRepo := &githubRepository{ + config: tt.config, + secrets: mockSecrets, + } + + err := githubRepo.OnDelete(context.Background()) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + } + + mockSecrets.AssertExpectations(t) + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/github_test.go b/pkg/registry/apis/provisioning/repository/github_test.go deleted file mode 100644 index 3c0e499713e..00000000000 --- a/pkg/registry/apis/provisioning/repository/github_test.go +++ /dev/null @@ -1,3143 +0,0 @@ -package repository - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - field "k8s.io/apimachinery/pkg/util/validation/field" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" -) - -func TestNewGitHub(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - setupMock func(m *secrets.MockService) - expectedError string - expectedRepo *githubRepository - }{ - { - name: "successful creation with token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Token: "token123", - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - // No mock calls expected since we're using the token directly - }, - expectedError: "", - expectedRepo: &githubRepository{ - owner: "grafana", - repo: "grafana", - }, - }, - { - name: "successful creation with encrypted token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - EncryptedToken: []byte("encrypted-token"), - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - m.On("Decrypt", mock.Anything, []byte("encrypted-token")). - Return([]byte("decrypted-token"), nil) - }, - expectedError: "", - expectedRepo: &githubRepository{ - owner: "grafana", - repo: "grafana", - }, - }, - { - name: "error decrypting token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - EncryptedToken: []byte("encrypted-token"), - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - m.On("Decrypt", mock.Anything, []byte("encrypted-token")). - Return(nil, fmt.Errorf("decryption error")) - }, - expectedError: "decrypt token: decryption error", - }, - { - name: "invalid URL format", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "invalid-url", - Token: "token123", - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - // No mock calls expected - }, - expectedError: "parse owner and repo", - expectedRepo: &githubRepository{ - owner: "", - repo: "", - }, - }, - { - name: "URL with .git extension", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana.git", - Token: "token123", - Branch: "main", - }, - }, - }, - setupMock: func(m *secrets.MockService) { - // No mock calls expected - }, - expectedError: "", - expectedRepo: &githubRepository{ - owner: "grafana", - repo: "grafana", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mocks - mockSecrets := secrets.NewMockService(t) - if tt.setupMock != nil { - tt.setupMock(mockSecrets) - } - - factory := pgh.ProvideFactory() - factory.Client = http.DefaultClient - - // Create a mock clone function - cloneFn := func(ctx context.Context, opts CloneOptions) (ClonedRepository, error) { - return nil, nil - } - - // Call the function under test - repo, err := NewGitHub( - context.Background(), - tt.config, - factory, - mockSecrets, - cloneFn, - ) - - // Check results - if tt.expectedError != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, repo) - } else { - require.NoError(t, err) - require.NotNil(t, repo) - assert.Equal(t, tt.expectedRepo.owner, repo.Owner()) - assert.Equal(t, tt.expectedRepo.repo, repo.Repo()) - assert.Equal(t, tt.config, repo.Config()) - concreteRepo, ok := repo.(*githubRepository) - require.True(t, ok) - assert.Equal(t, mockSecrets, concreteRepo.secrets) - assert.NotNil(t, concreteRepo.cloneFn) - } - - // Verify all mock expectations were met - mockSecrets.AssertExpectations(t) - }) - } -} - -func TestIsValidGitBranchName(t *testing.T) { - tests := []struct { - name string - branch string - expected bool - }{ - {"Valid branch name", "feature/add-tests", true}, - {"Valid branch name with numbers", "feature/123-add-tests", true}, - {"Valid branch name with dots", "feature.add.tests", true}, - {"Valid branch name with hyphens", "feature-add-tests", true}, - {"Valid branch name with underscores", "feature_add_tests", true}, - {"Valid branch name with mixed characters", "feature/add_tests-123", true}, - {"Starts with /", "/feature", false}, - {"Ends with /", "feature/", false}, - {"Ends with .", "feature.", false}, - {"Ends with space", "feature ", false}, - {"Contains consecutive slashes", "feature//branch", false}, - {"Contains consecutive dots", "feature..branch", false}, - {"Contains @{", "feature@{branch", false}, - {"Contains invalid character ~", "feature~branch", false}, - {"Contains invalid character ^", "feature^branch", false}, - {"Contains invalid character :", "feature:branch", false}, - {"Contains invalid character ?", "feature?branch", false}, - {"Contains invalid character *", "feature*branch", false}, - {"Contains invalid character [", "feature[branch", false}, - {"Contains invalid character ]", "feature]branch", false}, - {"Contains invalid character \\", "feature\\branch", false}, - {"Empty branch name", "", false}, - {"Only whitespace", " ", false}, - {"Single valid character", "a", true}, - {"Ends with .lock", "feature.lock", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, IsValidGitBranchName(tt.branch)) - }) - } -} -func TestGitHubRepositoryValidate(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - expectedErrors int - errorFields []string - }{ - { - name: "Valid configuration", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 0, - }, - { - name: "Valid configuration with .git suffix", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana.git", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 0, - }, - { - name: "Missing GitHub config", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: nil, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github"}, - }, - { - name: "Missing URL", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Invalid URL format", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "invalid-url", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Fail to parse URL", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/user%", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "URL not starting with https://github.com/", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://gitlab.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Missing repo name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana", - Branch: "main", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.url"}, - }, - { - name: "Missing branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.branch"}, - }, - { - name: "Invalid branch name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "feature//invalid", - Token: "valid-token", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.branch"}, - }, - { - name: "Missing token", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "", - Path: "dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.token"}, - }, - { - name: "Unsafe path", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "../dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.prefix"}, - }, - { - name: "Absolute path", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - Path: "/dashboards", - }, - }, - }, - expectedErrors: 1, - errorFields: []string{"spec.github.prefix"}, - }, - { - name: "Multiple errors", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "", - Branch: "", - Token: "", - Path: "/dashboards", - }, - }, - }, - expectedErrors: 4, - errorFields: []string{"spec.github.url", "spec.github.branch", "spec.github.token", "spec.github.prefix"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a GitHub repository with the test config - repo := &githubRepository{ - config: tt.config, - } - - // Validate the configuration - errors := repo.Validate() - - // Check the number of errors - assert.Equal(t, tt.expectedErrors, len(errors), "Expected %d errors, got %d, errors: %v", tt.expectedErrors, len(errors), errors) - - // If we expect errors, check that they are for the right fields - if tt.expectedErrors > 0 { - errorFields := make([]string, 0, len(errors)) - for _, err := range errors { - errorFields = append(errorFields, err.Field) - } - for _, expectedField := range tt.errorFields { - assert.Contains(t, errorFields, expectedField, "Expected error for field %s", expectedField) - } - } - }) - } -} - -func TestGitHubRepository_Test(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - mockSetup func(t *testing.T, client *pgh.MockClient) - expectedResult *provisioning.TestResults - expectedError error - }{ - { - name: "Authentication failure", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "invalid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(errors.New("authentication failed")) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: "spec.github.token", - Detail: "authentication failed", - }}, - }, - expectedError: nil, - }, - { - name: "Invalid URL", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/invalid", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: "spec.github.url", - Detail: "unable to parse repo+owner from url", - }}, - }, - expectedError: nil, - }, - { - name: "Failed to check if repo exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/nonexistent", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "nonexistent").Return(false, errors.New("failed to check if repo exists")) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeInvalid), - Field: "spec.github.url", - Detail: "failed to check if repo exists", - }}, - }, - expectedError: nil, - }, - { - name: "Repository does not exist", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/nonexistent", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "nonexistent").Return(false, nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeNotFound), - Field: "spec.github.url", - }}, - }, - expectedError: nil, - }, - { - name: "Branch does not exist", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "nonexistent-branch", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "grafana").Return(true, nil) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "nonexistent-branch").Return(false, nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeNotFound), - Field: "spec.github.branch", - }}, - }, - expectedError: nil, - }, - { - name: "Branch check error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "grafana").Return(true, nil) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "main").Return(false, errors.New("API rate limit exceeded")) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(field.ErrorTypeInvalid), - Field: "spec.github.branch", - Detail: "API rate limit exceeded", - }}, - }, - expectedError: nil, - }, - { - name: "Successful test", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - Token: "valid-token", - }, - }, - }, - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("IsAuthenticated", mock.Anything).Return(nil) - client.On("RepoExists", mock.Anything, "grafana", "grafana").Return(true, nil) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "main").Return(true, nil) - }, - expectedResult: &provisioning.TestResults{ - Code: http.StatusOK, - Success: true, - }, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // If the config has a different URL, parse and set the owner/repo - if tt.config.Spec.GitHub.URL != "https://github.com/grafana/grafana" { - owner, githubRepo, _ := ParseOwnerRepoGithub(tt.config.Spec.GitHub.URL) - repo.owner = owner - repo.repo = githubRepo - } - - // Test the repository - result, err := repo.Test(context.Background()) - - // Check the error - if tt.expectedError != nil { - assert.Error(t, err) - assert.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - assert.NoError(t, err) - } - - // Check the result - if tt.expectedResult != nil { - assert.Equal(t, tt.expectedResult.Code, result.Code) - assert.Equal(t, tt.expectedResult.Success, result.Success) - - if len(tt.expectedResult.Errors) > 0 { - assert.Equal(t, len(tt.expectedResult.Errors), len(result.Errors)) - - for i, expectedError := range tt.expectedResult.Errors { - assert.Equal(t, expectedError.Type, result.Errors[i].Type) - assert.Equal(t, expectedError.Field, result.Errors[i].Field) - assert.Equal(t, expectedError.Detail, result.Errors[i].Detail) - } - } - } - - // Verify all expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestReadTree(t *testing.T) { - tests := []struct { - name string - path string - ref string - expectedRef string - tree []pgh.RepositoryContent - expected []FileTreeEntry - getTreeErr error - truncated bool - expectedError error - }{ - {name: "empty ref", ref: "", expectedRef: "develop", tree: []pgh.RepositoryContent{}, expected: []FileTreeEntry{}}, - {name: "unknown error to get tree", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, getTreeErr: errors.New("unknown error"), expectedError: errors.New("get tree: unknown error")}, - {name: "tree not found error", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, getTreeErr: pgh.ErrResourceNotFound, expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "tree not found; ref=develop", - Code: http.StatusNotFound, - }, - }}, - {name: "tree truncated", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, truncated: true, expectedError: errors.New("tree truncated")}, - {name: "empty tree", ref: "develop", expectedRef: "develop", tree: []pgh.RepositoryContent{}, expected: []FileTreeEntry{}}, - {name: "single file", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - content := pgh.NewMockRepositoryContent(t) - content.EXPECT().GetPath().Return("file.txt") - content.EXPECT().GetSize().Return(int64(100)) - content.EXPECT().GetSHA().Return("abc123") - content.EXPECT().IsDirectory().Return(false) - return []pgh.RepositoryContent{content} - }(), expected: []FileTreeEntry{ - {Path: "file.txt", Size: 100, Hash: "abc123", Blob: true}, - }}, - {name: "single directory", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - content := pgh.NewMockRepositoryContent(t) - content.EXPECT().GetPath().Return("dir") - content.EXPECT().IsDirectory().Return(true) - content.EXPECT().GetSize().Return(int64(0)) - content.EXPECT().GetSHA().Return("") - - return []pgh.RepositoryContent{content} - }(), expected: []FileTreeEntry{ - {Path: "dir/", Blob: false}, - }}, - {name: "mixed content", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - file1 := pgh.NewMockRepositoryContent(t) - file1.EXPECT().GetPath().Return("file1.txt") - file1.EXPECT().GetSize().Return(int64(100)) - file1.EXPECT().GetSHA().Return("abc123") - file1.EXPECT().IsDirectory().Return(false) - - dir := pgh.NewMockRepositoryContent(t) - dir.EXPECT().GetPath().Return("dir") - dir.EXPECT().IsDirectory().Return(true) - dir.EXPECT().GetSize().Return(int64(0)) - dir.EXPECT().GetSHA().Return("") - - file2 := pgh.NewMockRepositoryContent(t) - file2.EXPECT().GetPath().Return("file2.txt") - file2.EXPECT().GetSize().Return(int64(200)) - file2.EXPECT().GetSHA().Return("def456") - file2.EXPECT().IsDirectory().Return(false) - - return []pgh.RepositoryContent{file1, dir, file2} - }(), expected: []FileTreeEntry{ - {Path: "file1.txt", Size: 100, Hash: "abc123", Blob: true}, - {Path: "dir/", Blob: false}, - {Path: "file2.txt", Size: 200, Hash: "def456", Blob: true}, - }}, - {name: "with path prefix", ref: "develop", expectedRef: "develop", tree: func() []pgh.RepositoryContent { - file := pgh.NewMockRepositoryContent(t) - file.EXPECT().GetPath().Return("file.txt") - file.EXPECT().GetSize().Return(int64(100)) - file.EXPECT().GetSHA().Return("abc123") - file.EXPECT().IsDirectory().Return(false) - - dir := pgh.NewMockRepositoryContent(t) - dir.EXPECT().GetPath().Return("dir") - dir.EXPECT().GetSize().Return(int64(0)) - dir.EXPECT().GetSHA().Return("") - dir.EXPECT().IsDirectory().Return(true) - - return []pgh.RepositoryContent{file, dir} - }(), expected: []FileTreeEntry{ - {Path: "file.txt", Size: 100, Hash: "abc123", Blob: true}, - {Path: "dir/", Blob: false}, - }}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ghMock := pgh.NewMockClient(t) - gh := &githubRepository{ - owner: "owner", - repo: "repo", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: tt.path, - Branch: "develop", - }, - }, - }, - gh: ghMock, - } - - ghMock.On("GetTree", mock.Anything, "owner", "repo", tt.path, tt.expectedRef, true).Return(tt.tree, tt.truncated, tt.getTreeErr) - tree, err := gh.ReadTree(context.Background(), tt.ref) - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, tree) - } - }) - } -} - -func TestGitHubRepository_Read(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - filePath string - ref string - mockSetup func(t *testing.T, client *pgh.MockClient) - expectedResult *FileInfo - expectedError error - }{ - { - name: "File found successfully", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("file content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "main"). - Return(fileContent, nil, nil) - }, - expectedResult: &FileInfo{ - Path: "dashboard.json", - Ref: "main", - Data: []byte("file content"), - Hash: "abc123", - }, - expectedError: nil, - }, - { - name: "Directory found successfully", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboards", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - dirContent := []pgh.RepositoryContent{ - // Directory contents not used in this test - } - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboards", "main"). - Return(nil, dirContent, nil) - }, - expectedResult: &FileInfo{ - Path: "dashboards", - Ref: "main", - }, - expectedError: nil, - }, - { - name: "File not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "nonexistent.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/nonexistent.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - }, - expectedResult: nil, - expectedError: ErrFileNotFound, - }, - { - name: "Error getting file content", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("", errors.New("failed to decode content")) - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "main"). - Return(fileContent, nil, nil) - }, - expectedResult: nil, - expectedError: fmt.Errorf("get content: %w", errors.New("failed to decode content")), - }, - { - name: "GitHub API error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "main", - }, - }, - }, - filePath: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "main"). - Return(nil, nil, errors.New("API rate limit exceeded")) - }, - expectedResult: nil, - expectedError: fmt.Errorf("get contents: %w", errors.New("API rate limit exceeded")), - }, - { - name: "Use default branch when ref is empty", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "configs", - Branch: "develop", - }, - }, - }, - filePath: "dashboard.json", - ref: "", // Empty ref should use default branch - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("file content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - client.On("GetContents", mock.Anything, "grafana", "grafana", "configs/dashboard.json", "develop"). - Return(fileContent, nil, nil) - }, - expectedResult: &FileInfo{ - Path: "dashboard.json", - Ref: "develop", - Data: []byte("file content"), - Hash: "abc123", - }, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Read method - result, err := repo.Read(context.Background(), tt.filePath, tt.ref) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - } - - // Check the result - if tt.expectedResult != nil { - require.Equal(t, tt.expectedResult.Path, result.Path) - require.Equal(t, tt.expectedResult.Ref, result.Ref) - require.Equal(t, tt.expectedResult.Data, result.Data) - require.Equal(t, tt.expectedResult.Hash, result.Hash) - } else { - require.Nil(t, result) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Create(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - data []byte - comment string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expectedError error - }{ - { - name: "successful file creation", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "feature-branch", "Add new dashboard", []byte("dashboard content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "create with default branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", "Add new dashboard", []byte("dashboard content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "branch already exists error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(pgh.ErrResourceAlreadyExists) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusConflict, - Message: "branch already exists", - }, - }, - }, - { - name: "branch does not exist error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(fmt.Errorf("failed to create branch")) - }, - expectedError: fmt.Errorf("create branch: %w", fmt.Errorf("failed to create branch")), - }, - { - name: "branch does not exist but it's created", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "feature-branch", "Add new dashboard", []byte("dashboard content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "invalid branch name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature//branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - // No mock expectations needed as validation should fail before any GitHub API calls - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusBadRequest, - Message: "invalid branch name", - }, - }, - }, - { - name: "branch exists check fails", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature-branch", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature-branch").Return(false, fmt.Errorf("failed to check branch")) - }, - expectedError: fmt.Errorf("check branch exists: %w", fmt.Errorf("failed to check branch")), - }, - { - name: "file already exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("dashboard content"), - comment: "Add new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", "Add new dashboard", []byte("dashboard content")).Return(pgh.ErrResourceAlreadyExists) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file already exists", - Code: http.StatusConflict, - }, - }, - }, - { - name: "create directory with .keep file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards/", - ref: "main", - data: nil, - comment: "Add dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/.keep", "main", "Add dashboards directory", []byte{}).Return(nil) - }, - expectedError: nil, - }, - { - name: "error when providing data for directory", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards/", - ref: "main", - data: []byte("some data"), - comment: "Add dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - }, - expectedError: apierrors.NewBadRequest("data cannot be provided for a directory"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Create method - err := repo.Create(context.Background(), tt.path, tt.ref, tt.data, tt.comment) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Update(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - data []byte - comment string - mockSetup func(t *testing.T, client *pgh.MockClient) - expectedError error - }{ - { - name: "Successfully update file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch", - "Update test file", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "Use default branch when ref is empty", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "main").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "main"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "main", - "Update test file", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "Branch does not exist", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - client.On("CreateBranch", mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(errors.New("failed to create branch")) - }, - expectedError: errors.New("create branch: failed to create branch"), - }, - { - name: "Invalid branch name", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "invalid//branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - // No mock calls expected - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusBadRequest, - Message: "invalid branch name", - }, - }, - }, - { - name: "Branch exists check fails", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, errors.New("failed to check branch")) - }, - expectedError: errors.New("check branch exists: failed to check branch"), - }, - { - name: "Branch does not exist but it's created successfully", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - client.On("CreateBranch", mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(nil) - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch", - "Update test file", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "Branch already exists error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(false, nil) - client.On("CreateBranch", mock.Anything, "grafana", "grafana", "main", "feature-branch").Return(pgh.ErrResourceAlreadyExists) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusConflict, - Message: "branch already exists", - }, - }, - }, - { - name: "File not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(nil, nil, pgh.ErrResourceNotFound) - }, - expectedError: &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "file not found", - Code: http.StatusNotFound, - }, - }, - }, - { - name: "Error getting file contents", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(nil, nil, errors.New("API error")) - }, - expectedError: errors.New("get content before file update: API error"), - }, - { - name: "Cannot update directory", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/directory", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test directory", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - - // Create a directory file - dirFile := pgh.NewMockRepositoryContent(t) - dirFile.EXPECT().IsDirectory().Return(true) - - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/directory", "feature-branch"). - Return(dirFile, nil, nil) - }, - expectedError: apierrors.NewBadRequest("cannot update a directory"), - }, - { - name: "Error updating file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "base/path", - }, - }, - }, - path: "test/file.txt", - ref: "feature-branch", - data: []byte("updated content"), - comment: "Update test file", - mockSetup: func(t *testing.T, client *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - client.On("BranchExists", mock.Anything, "grafana", "grafana", "feature-branch").Return(true, nil) - client.On("GetContents", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch"). - Return(fileContent, nil, nil) - client.On("UpdateFile", mock.Anything, "grafana", "grafana", "base/path/test/file.txt", "feature-branch", - "Update test file", "abc123", []byte("updated content")).Return(errors.New("update failed")) - }, - expectedError: errors.New("update file: update failed"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Update method - err := repo.Update(context.Background(), tt.path, tt.ref, tt.data, tt.comment) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Write(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - data []byte - message string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expectedError error - }{ - { - name: "write to existing file (update)", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("updated content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("existing content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().UpdateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Update dashboard", "abc123", []byte("updated content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "write to non-existing file (create)", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "new-dashboard.json", - ref: "main", - data: []byte("new content"), - message: "Create new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main", - "Create new dashboard", []byte("new content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "write with default branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - data: []byte("content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("existing content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().UpdateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Update dashboard", "abc123", []byte("content")).Return(nil) - }, - expectedError: nil, - }, - { - name: "error checking if file exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(nil, nil, errors.New("connection error")) - }, - expectedError: errors.New("check if file exists before writing: get contents: connection error"), - }, - { - name: "error during update", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - data: []byte("updated content"), - message: "Update dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().GetFileContent().Return("existing content", nil) - fileContent.EXPECT().GetSHA().Return("abc123") - fileContent.EXPECT().IsDirectory().Return(false) - - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().UpdateFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Update dashboard", "abc123", []byte("updated content")).Return(errors.New("update failed")) - }, - expectedError: errors.New("update file: update failed"), - }, - { - name: "error during create", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "new-dashboard.json", - ref: "main", - data: []byte("new content"), - message: "Create new dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().CreateFile(mock.Anything, "grafana", "grafana", "grafana/new-dashboard.json", "main", - "Create new dashboard", []byte("new content")).Return(errors.New("create failed")) - }, - expectedError: errors.New("create failed"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Write method - err := repo.Write(context.Background(), tt.path, tt.ref, tt.data, tt.message) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_Delete(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - comment string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expectedError error - }{ - { - name: "delete existing file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().IsDirectory().Return(false) - fileContent.EXPECT().GetSHA().Return("abc123") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Delete dashboard", "abc123").Return(nil) - }, - expectedError: nil, - }, - { - name: "delete with default branch", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().IsDirectory().Return(false) - fileContent.EXPECT().GetSHA().Return("abc123") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Delete dashboard", "abc123").Return(nil) - }, - expectedError: nil, - }, - { - name: "delete directory recursively", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards", - ref: "main", - comment: "Delete dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - dirContent := pgh.NewMockRepositoryContent(t) - dirContent.EXPECT().IsDirectory().Return(true) - - // Directory contents - file1Content := pgh.NewMockRepositoryContent(t) - file1Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard1.json") - file1Content.EXPECT().IsDirectory().Return(false) - file1Content.EXPECT().GetSHA().Return("file1-sha") - - file2Content := pgh.NewMockRepositoryContent(t) - file2Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard2.json") - file2Content.EXPECT().IsDirectory().Return(false) - file2Content.EXPECT().GetSHA().Return("file2-sha") - - subDirContent := pgh.NewMockRepositoryContent(t) - subDirContent.EXPECT().GetPath().Return("grafana/dashboards/subfolder") - subDirContent.EXPECT().IsDirectory().Return(true) - - // Subfolder contents - subFile1Content := pgh.NewMockRepositoryContent(t) - subFile1Content.EXPECT().GetPath().Return("grafana/dashboards/subfolder/subdashboard.json") - subFile1Content.EXPECT().IsDirectory().Return(false) - subFile1Content.EXPECT().GetSHA().Return("subfile-sha") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - - // Get main directory - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards", "main"). - Return(dirContent, []pgh.RepositoryContent{file1Content, file2Content, subDirContent}, nil) - - // Get subfolder contents - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder", "main"). - Return(subDirContent, []pgh.RepositoryContent{subFile1Content}, nil) - - // Delete files in reverse order (depth-first) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder/subdashboard.json", "main", - "Delete dashboards directory", "subfile-sha").Return(nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard2.json", "main", - "Delete dashboards directory", "file2-sha").Return(nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard1.json", "main", - "Delete dashboards directory", "file1-sha").Return(nil) - }, - expectedError: nil, - }, - { - name: "delete directory recursively fails in the middle", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboards", - ref: "main", - comment: "Delete dashboards directory", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - dirContent := pgh.NewMockRepositoryContent(t) - dirContent.EXPECT().IsDirectory().Return(true) - - // Directory contents - file1Content := pgh.NewMockRepositoryContent(t) - file1Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard1.json") - file1Content.EXPECT().IsDirectory().Return(false) - file1Content.EXPECT().GetSHA().Return("file1-sha") - - file2Content := pgh.NewMockRepositoryContent(t) - file2Content.EXPECT().GetPath().Return("grafana/dashboards/dashboard2.json") - file2Content.EXPECT().IsDirectory().Return(false) - file2Content.EXPECT().GetSHA().Return("file2-sha") - - subDirContent := pgh.NewMockRepositoryContent(t) - subDirContent.EXPECT().IsDirectory().Return(true) - subDirContent.EXPECT().GetPath().Return("grafana/dashboards/subfolder") - - // Subfolder contents - subFile1Content := pgh.NewMockRepositoryContent(t) - subFile1Content.EXPECT().GetPath().Return("grafana/dashboards/subfolder/subdashboard.json") - subFile1Content.EXPECT().IsDirectory().Return(false) - subFile1Content.EXPECT().GetSHA().Return("subfile-sha") - - subFile2Content := pgh.NewMockRepositoryContent(t) - subFile2Content.EXPECT().GetPath().Return("grafana/dashboards/subfolder/subdashboard2.json") - subFile2Content.EXPECT().IsDirectory().Return(false) - subFile2Content.EXPECT().GetSHA().Return("subfile2-sha") - - subFile3Content := pgh.NewMockRepositoryContent(t) - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - - // Get main directory - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards", "main"). - Return(dirContent, []pgh.RepositoryContent{file1Content, file2Content, subDirContent}, nil) - - // Get subfolder contents - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder", "main"). - Return(subDirContent, []pgh.RepositoryContent{subFile1Content, subFile2Content, subFile3Content}, nil) - - // Delete first file successfully - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard1.json", "main", - "Delete dashboards directory", "file1-sha").Return(nil) - - // Second file deletion fails - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/dashboard2.json", "main", - "Delete dashboards directory", "file2-sha").Return(nil) - - // Delete subfolder files - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder/subdashboard.json", "main", - "Delete dashboards directory", "subfile-sha").Return(nil) - - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboards/subfolder/subdashboard2.json", "main", - "Delete dashboards directory", "subfile2-sha").Return(errors.New("permission denied")) - }, - expectedError: errors.New("delete directory recursively: delete file: permission denied"), - }, - { - name: "file not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "nonexistent.json", - ref: "main", - comment: "Delete nonexistent file", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/nonexistent.json", "main"). - Return(nil, nil, pgh.ErrResourceNotFound) - }, - expectedError: ErrFileNotFound, - }, - { - name: "branch does not exist and creation fails", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "feature", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "feature").Return(false, nil) - mockClient.EXPECT().CreateBranch(mock.Anything, "grafana", "grafana", "main", "feature"). - Return(errors.New("failed to create branch")) - }, - expectedError: errors.New("create branch: failed to create branch"), - }, - { - name: "error checking if branch exists", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main"). - Return(false, errors.New("API error")) - }, - expectedError: errors.New("check branch exists: API error"), - }, - { - name: "error getting file content", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(nil, nil, errors.New("API rate limit exceeded")) - }, - expectedError: fmt.Errorf("find file to delete: %w", errors.New("API rate limit exceeded")), - }, - { - name: "error deleting file", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - comment: "Delete dashboard", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - fileContent := pgh.NewMockRepositoryContent(t) - fileContent.EXPECT().IsDirectory().Return(false) - fileContent.EXPECT().GetSHA().Return("abc123") - - mockClient.EXPECT().BranchExists(mock.Anything, "grafana", "grafana", "main").Return(true, nil) - mockClient.EXPECT().GetContents(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(fileContent, nil, nil) - mockClient.EXPECT().DeleteFile(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main", - "Delete dashboard", "abc123").Return(errors.New("delete failed")) - }, - expectedError: errors.New("delete failed"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the Delete method - err := repo.Delete(context.Background(), tt.path, tt.ref, tt.comment) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_History(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - path string - ref string - mockSetup func(t *testing.T, mockClient *pgh.MockClient) - expected []provisioning.HistoryItem - expectedError error - }{ - { - name: "successful history retrieval", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - Committer: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - { - Ref: "def456", - Message: "Initial commit", - Author: &pgh.CommitAuthor{ - Name: "Jane Smith", - Username: "janesmith", - AvatarURL: "https://example.com/avatar2.png", - }, - Committer: &pgh.CommitAuthor{ - Name: "Bob Johnson", - Username: "bjohnson", - AvatarURL: "https://example.com/avatar3.png", - }, - CreatedAt: time.Date(2022, 12, 31, 10, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - { - Ref: "def456", - Message: "Initial commit", - Authors: []provisioning.Author{ - { - Name: "Jane Smith", - Username: "janesmith", - AvatarURL: "https://example.com/avatar2.png", - }, - { - Name: "Bob Johnson", - Username: "bjohnson", - AvatarURL: "https://example.com/avatar3.png", - }, - }, - CreatedAt: time.Date(2022, 12, 31, 10, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - { - name: "committer same as author", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - Committer: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - { - name: "file not found", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "nonexistent.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/nonexistent.json", "main"). - Return(nil, pgh.ErrResourceNotFound) - }, - expectedError: ErrFileNotFound, - }, - { - name: "prefixed path", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "custom/prefix", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "custom/prefix/dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - { - name: "other error", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "main", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(nil, errors.New("api error")) - }, - expectedError: errors.New("get commits: api error"), - }, - { - name: "use default branch when ref is empty", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Path: "grafana", - Branch: "main", - }, - }, - }, - path: "dashboard.json", - ref: "", - mockSetup: func(t *testing.T, mockClient *pgh.MockClient) { - commits := []pgh.Commit{ - { - Ref: "abc123", - Message: "Update dashboard", - Author: &pgh.CommitAuthor{ - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), - }, - } - - mockClient.EXPECT().Commits(mock.Anything, "grafana", "grafana", "grafana/dashboard.json", "main"). - Return(commits, nil) - }, - expected: []provisioning.HistoryItem{ - { - Ref: "abc123", - Message: "Update dashboard", - Authors: []provisioning.Author{ - { - Name: "John Doe", - Username: "johndoe", - AvatarURL: "https://example.com/avatar1.png", - }, - }, - CreatedAt: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(), - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a mock GitHub client - mockClient := pgh.NewMockClient(t) - - // Set up the mock expectations - if tt.mockSetup != nil { - tt.mockSetup(t, mockClient) - } - - // Create a GitHub repository with the test config and mock client - repo := &githubRepository{ - config: tt.config, - gh: mockClient, - owner: "grafana", - repo: "grafana", - } - - // Call the History method - history, err := repo.History(context.Background(), tt.path, tt.ref) - - // Check the error - if tt.expectedError != nil { - require.Error(t, err) - var statusErr *apierrors.StatusError - if errors.As(tt.expectedError, &statusErr) { - var actualStatusErr *apierrors.StatusError - require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type") - require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message) - require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code) - } else { - require.Equal(t, tt.expectedError.Error(), err.Error()) - } - } else { - require.NoError(t, err) - require.Equal(t, tt.expected, history) - } - - // Verify all mock expectations were met - mockClient.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_LatestRef(t *testing.T) { - tests := []struct { - name string - setupMock func(mock *pgh.MockClient) - expectedRef string - expectedError error - }{ - { - name: "successful retrieval of latest ref", - setupMock: func(m *pgh.MockClient) { - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{Sha: "abc123"}, nil) - }, - expectedRef: "abc123", - expectedError: nil, - }, - { - name: "error getting branch", - setupMock: func(m *pgh.MockClient) { - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{}, fmt.Errorf("branch not found")) - }, - expectedRef: "", - expectedError: fmt.Errorf("get branch: branch not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) - tt.setupMock(mockGH) - - // Create repository with mock - repo := &githubRepository{ - gh: mockGH, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - owner: "grafana", - repo: "grafana", - } - - // Call the LatestRef method - ref, err := repo.LatestRef(context.Background()) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedRef, ref) - } - - // Verify all mock expectations were met - mockGH.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_CompareFiles(t *testing.T) { - tests := []struct { - name string - setupMock func(m *pgh.MockClient) - base string - ref string - expectedFiles []VersionedFileChange - expectedError error - shouldGetLatest bool - }{ - { - name: "successfully compare files", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/test.json") - commitFile1.On("GetStatus").Return("added") - - commitFile2 := pgh.NewMockCommitFile(t) - commitFile2.On("GetFilename").Return("dashboards/modified.json") - commitFile2.On("GetStatus").Return("modified") - - commitFile3 := pgh.NewMockCommitFile(t) - commitFile3.On("GetFilename").Return("dashboards/renamed.json") - commitFile3.On("GetStatus").Return("renamed") - commitFile3.On("GetPreviousFilename").Return("dashboards/old.json") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - commitFile2, - commitFile3, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "test.json", - Ref: "def456", - Action: FileActionCreated, - }, - { - Path: "modified.json", - Ref: "def456", - Action: FileActionUpdated, - }, - { - Path: "renamed.json", - Ref: "def456", - Action: FileActionRenamed, - PreviousPath: "old.json", - }, - }, - expectedError: nil, - }, - { - name: "error comparing commits", - setupMock: func(m *pgh.MockClient) { - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return(nil, fmt.Errorf("failed to compare commits")) - }, - base: "abc123", - ref: "def456", - expectedFiles: nil, - expectedError: fmt.Errorf("compare commits: failed to compare commits"), - }, - { - name: "file outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/path.json") - commitFile1.On("GetStatus").Return("added") - - commitFile2 := pgh.NewMockCommitFile(t) - commitFile2.On("GetFilename").Return("dashboards/valid.json") - commitFile2.On("GetStatus").Return("added") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - commitFile2, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "valid.json", - Ref: "def456", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "modified file outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/modified.json") - commitFile1.On("GetStatus").Return("modified") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "copied file status", - setupMock: func(m *pgh.MockClient) { - // File inside configured path - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/copied.json") - commitFile1.On("GetStatus").Return("copied") - - // File outside configured path - commitFile2 := pgh.NewMockCommitFile(t) - commitFile2.On("GetFilename").Return("../outside/copied.json") - commitFile2.On("GetStatus").Return("copied") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - commitFile2, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "copied.json", - Ref: "def456", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "removed file status - inside path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/removed.json") - commitFile1.On("GetStatus").Return("removed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "removed.json", - PreviousPath: "removed.json", - Ref: "def456", - PreviousRef: "abc123", - Action: FileActionDeleted, - }, - }, - expectedError: nil, - }, - { - name: "renamed file status - both paths outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/renamed.json") - commitFile1.On("GetPreviousFilename").Return("../outside/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "renamed file status - both paths inside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/renamed.json") - commitFile1.On("GetPreviousFilename").Return("dashboards/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "renamed.json", - PreviousPath: "original.json", - Ref: "def456", - PreviousRef: "abc123", - Action: FileActionRenamed, - }, - }, - expectedError: nil, - }, - { - name: "renamed file status - moving out of configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/renamed.json") - commitFile1.On("GetPreviousFilename").Return("dashboards/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "original.json", - Ref: "abc123", - Action: FileActionDeleted, - }, - }, - expectedError: nil, - }, - { - name: "renamed file status - moving into configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/renamed.json") - commitFile1.On("GetPreviousFilename").Return("../outside/original.json") - commitFile1.On("GetStatus").Return("renamed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{ - { - Path: "renamed.json", - Ref: "def456", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "removed file status - outside path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/removed.json") - commitFile1.On("GetStatus").Return("removed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "changed file outside configured path", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("../outside/changed.json") - commitFile1.On("GetStatus").Return("changed") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "get latest ref when ref is empty", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/test.json") - commitFile1.On("GetStatus").Return("added") - - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{Sha: "latest123"}, nil) - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "latest123"). - Return([]pgh.CommitFile{commitFile1}, nil) - }, - base: "abc123", - ref: "", - shouldGetLatest: true, - expectedFiles: []VersionedFileChange{ - { - Path: "test.json", - Ref: "latest123", - Action: FileActionCreated, - }, - }, - expectedError: nil, - }, - { - name: "unchanged file status", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetStatus").Return("unchanged") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "unknown file status", - setupMock: func(m *pgh.MockClient) { - commitFile1 := pgh.NewMockCommitFile(t) - commitFile1.On("GetFilename").Return("dashboards/unknown.json") - commitFile1.On("GetStatus").Return("unknown_status") - - m.On("CompareCommits", mock.Anything, "grafana", "grafana", "abc123", "def456"). - Return([]pgh.CommitFile{ - commitFile1, - }, nil) - }, - base: "abc123", - ref: "def456", - expectedFiles: []VersionedFileChange{}, - expectedError: nil, - }, - { - name: "error getting latest ref", - setupMock: func(m *pgh.MockClient) { - m.On("GetBranch", mock.Anything, "grafana", "grafana", "main"). - Return(pgh.Branch{}, fmt.Errorf("branch not found")) - }, - base: "abc123", - ref: "", - shouldGetLatest: true, - expectedFiles: nil, - expectedError: fmt.Errorf("get latest ref: get branch: branch not found"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) - tt.setupMock(mockGH) - - // Create repository with mock - repo := &githubRepository{ - gh: mockGH, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - Path: "dashboards", - }, - }, - }, - owner: "grafana", - repo: "grafana", - } - - // Call the CompareFiles method - files, err := repo.CompareFiles(context.Background(), tt.base, tt.ref) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, len(tt.expectedFiles), len(files)) - - for i, expectedFile := range tt.expectedFiles { - require.Equal(t, expectedFile.Path, files[i].Path) - require.Equal(t, expectedFile.Ref, files[i].Ref) - require.Equal(t, expectedFile.Action, files[i].Action) - require.Equal(t, expectedFile.PreviousPath, files[i].PreviousPath) - } - } - - // Verify all mock expectations were met - mockGH.AssertExpectations(t) - }) - } -} - -func TestGitHubRepository_ResourceURLs(t *testing.T) { - tests := []struct { - name string - file *FileInfo - config *provisioning.Repository - expectedURLs *provisioning.ResourceURLs - expectedError error - }{ - { - name: "file with ref", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "feature-branch", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: &provisioning.ResourceURLs{ - RepositoryURL: "https://github.com/grafana/grafana", - SourceURL: "https://github.com/grafana/grafana/blob/feature-branch/dashboards/test.json", - CompareURL: "https://github.com/grafana/grafana/compare/main...feature-branch", - NewPullRequestURL: "https://github.com/grafana/grafana/compare/main...feature-branch?quick_pull=1&labels=grafana", - }, - expectedError: nil, - }, - { - name: "file without ref uses default branch", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: &provisioning.ResourceURLs{ - RepositoryURL: "https://github.com/grafana/grafana", - SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", - }, - expectedError: nil, - }, - { - name: "file with ref same as branch", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "main", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: &provisioning.ResourceURLs{ - RepositoryURL: "https://github.com/grafana/grafana", - SourceURL: "https://github.com/grafana/grafana/blob/main/dashboards/test.json", - }, - expectedError: nil, - }, - { - name: "empty path returns nil", - file: &FileInfo{ - Path: "", - Ref: "feature-branch", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/grafana", - Branch: "main", - }, - }, - }, - expectedURLs: nil, - expectedError: nil, - }, - { - name: "nil github config returns nil", - file: &FileInfo{ - Path: "dashboards/test.json", - Ref: "feature-branch", - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: nil, - }, - }, - expectedURLs: nil, - expectedError: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create repository - repo := &githubRepository{ - config: tt.config, - owner: "grafana", - repo: "grafana", - } - - // Call the ResourceURLs method - urls, err := repo.ResourceURLs(context.Background(), tt.file) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedURLs, urls) - } - }) - } -} - -func TestGitHubRepository_Clone(t *testing.T) { - tests := []struct { - name string - setupMock func(m *MockCloneFn) - config *provisioning.Repository - expectedError error - }{ - { - name: "successfully clone repository", - setupMock: func(m *MockCloneFn) { - m.On("Execute", mock.Anything, CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - MaxSize: 1024 * 1024 * 10, // 10MB - Timeout: 10 * time.Second, - Progress: io.Discard, - BeforeFn: nil, - }).Return(nil, nil) - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - expectedError: nil, - }, - { - name: "error cloning repository", - setupMock: func(m *MockCloneFn) { - m.On("Execute", mock.Anything, CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - MaxSize: 1024 * 1024 * 10, // 10MB - Timeout: 10 * time.Second, - Progress: io.Discard, - BeforeFn: nil, - }).Return(nil, fmt.Errorf("failed to clone repository")) - }, - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - expectedError: fmt.Errorf("failed to clone repository"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockCloneFn := NewMockCloneFn(t) - - tt.setupMock(mockCloneFn) - - // Create repository with mock - repo := &githubRepository{ - cloneFn: mockCloneFn.Execute, - config: tt.config, - owner: "grafana", - repo: "grafana", - } - - // Call the Clone method with a placeholder directory path - _, err := repo.Clone(context.Background(), CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - MaxSize: 1024 * 1024 * 10, // 10MB - Timeout: 10 * time.Second, - Progress: io.Discard, - BeforeFn: nil, - }) - - // Check results - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - } else { - require.NoError(t, err) - } - - // Verify all mock expectations were met - mockCloneFn.AssertExpectations(t) - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/progress.go b/pkg/registry/apis/provisioning/repository/go-git/progress.go deleted file mode 100644 index 29e840499ad..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/progress.go +++ /dev/null @@ -1,47 +0,0 @@ -package gogit - -import ( - "bufio" - "bytes" - "io" -) - -func Progress(lines func(line string), final string) io.WriteCloser { - reader, writer := io.Pipe() - scanner := bufio.NewScanner(reader) - scanner.Split(scanLines) - go func() { - for scanner.Scan() { - line := scanner.Text() - if line != "" { - lines(line) - } - } - lines(final) - }() - return writer -} - -// Copied from bufio.ScanLines and modifed to accept standalone \r as input -func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - if i := bytes.IndexByte(data, '\r'); i >= 0 { - // We have a full newline-terminated line. - return i + 1, data[0:i], nil - } - - // Support standalone newlines also - if i := bytes.IndexByte(data, '\n'); i >= 0 { - // We have a full newline-terminated line. - return i + 1, data[0:i], nil - } - - // If we're at EOF, we have a final, non-terminated line. Return it. - if atEOF { - return len(data), data, nil - } - // Request more data. - return 0, nil, nil -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/progress_test.go b/pkg/registry/apis/provisioning/repository/go-git/progress_test.go deleted file mode 100644 index d52168c8cd5..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/progress_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package gogit - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestProgressParsing(t *testing.T) { - tests := []struct { - name string - input string - expect []string - }{ - { - name: "no breaks", - input: "some text", - expect: []string{"some text"}, - }, - { - name: "with cr", - input: "hello\rworld", - expect: []string{"hello", "world"}, - }, - { - name: "with nl", - input: "hello\nworld", - expect: []string{"hello", "world"}, - }, - { - name: "with cr+nl", - input: "hello\r\nworld", - expect: []string{"hello", "world"}, - }, - } - for _, tt := range tests { - lastLine := "***LAST*LINE***" - t.Run(tt.name, func(t *testing.T) { - lines := []string{} - writer := Progress(func(line string) { - lines = append(lines, line) - }, lastLine) - _, _ = writer.Write([]byte(tt.input)) - err := writer.Close() - require.NoError(t, err) - - assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.NotEmpty(c, lines) - assert.Equal(c, lastLine, lines[len(lines)-1]) - - // Compare the results - require.Equal(c, tt.expect, lines[0:len(lines)-1]) - }, time.Millisecond*100, time.Microsecond*50) - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/repository_mock.go b/pkg/registry/apis/provisioning/repository/go-git/repository_mock.go deleted file mode 100644 index da1f2a005cf..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/repository_mock.go +++ /dev/null @@ -1,84 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package gogit - -import ( - context "context" - - git "github.com/go-git/go-git/v5" - mock "github.com/stretchr/testify/mock" -) - -// MockRepository is an autogenerated mock type for the Repository type -type MockRepository struct { - mock.Mock -} - -type MockRepository_Expecter struct { - mock *mock.Mock -} - -func (_m *MockRepository) EXPECT() *MockRepository_Expecter { - return &MockRepository_Expecter{mock: &_m.Mock} -} - -// PushContext provides a mock function with given fields: ctx, o -func (_m *MockRepository) PushContext(ctx context.Context, o *git.PushOptions) error { - ret := _m.Called(ctx, o) - - if len(ret) == 0 { - panic("no return value specified for PushContext") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *git.PushOptions) error); ok { - r0 = rf(ctx, o) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockRepository_PushContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PushContext' -type MockRepository_PushContext_Call struct { - *mock.Call -} - -// PushContext is a helper method to define mock.On call -// - ctx context.Context -// - o *git.PushOptions -func (_e *MockRepository_Expecter) PushContext(ctx interface{}, o interface{}) *MockRepository_PushContext_Call { - return &MockRepository_PushContext_Call{Call: _e.mock.On("PushContext", ctx, o)} -} - -func (_c *MockRepository_PushContext_Call) Run(run func(ctx context.Context, o *git.PushOptions)) *MockRepository_PushContext_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*git.PushOptions)) - }) - return _c -} - -func (_c *MockRepository_PushContext_Call) Return(_a0 error) *MockRepository_PushContext_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockRepository_PushContext_Call) RunAndReturn(run func(context.Context, *git.PushOptions) error) *MockRepository_PushContext_Call { - _c.Call.Return(run) - return _c -} - -// NewMockRepository creates a new instance of MockRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *MockRepository { - mock := &MockRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport.go b/pkg/registry/apis/provisioning/repository/go-git/transport.go deleted file mode 100644 index 03d9feeb798..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/transport.go +++ /dev/null @@ -1,73 +0,0 @@ -package gogit - -import ( - "fmt" - "io" - "net/http" - "sync/atomic" - - "github.com/grafana/grafana/pkg/util/httpclient" -) - -var errBytesLimitExceeded = fmt.Errorf("bytes limit exceeded") - -// ByteLimitedTransport wraps http.RoundTripper to enforce a max byte limit -type ByteLimitedTransport struct { - Transport http.RoundTripper - Limit int64 - Bytes int64 -} - -// NewByteLimitedTransport creates a new ByteLimitedTransport with the specified transport and byte limit. -// If transport is nil, a new http.Transport modeled after http.DefaultTransport will be used. -func NewByteLimitedTransport(transport http.RoundTripper, limit int64) *ByteLimitedTransport { - if transport == nil { - transport = httpclient.NewHTTPTransport() - } - return &ByteLimitedTransport{ - Transport: transport, - Limit: limit, - Bytes: 0, - } -} - -// RoundTrip tracks downloaded bytes and aborts if limit is exceeded -func (b *ByteLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { - resp, err := b.Transport.RoundTrip(req) - if err != nil { - return nil, err - } - - // Wrap response body to track bytes read - resp.Body = &byteLimitedReader{ - reader: resp.Body, - limit: b.Limit, - bytes: &b.Bytes, - } - - return resp, nil -} - -// byteLimitedReader tracks and enforces a download limit -type byteLimitedReader struct { - reader io.ReadCloser - limit int64 - bytes *int64 -} - -func (r *byteLimitedReader) Read(p []byte) (int, error) { - n, err := r.reader.Read(p) - if err != nil { - return n, err - } - - if atomic.AddInt64(r.bytes, int64(n)) > r.limit { - return 0, errBytesLimitExceeded - } - - return n, nil -} - -func (r *byteLimitedReader) Close() error { - return r.reader.Close() -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go b/pkg/registry/apis/provisioning/repository/go-git/transport_test.go deleted file mode 100644 index 292046cf42f..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/transport_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package gogit - -import ( - "bytes" - "errors" - "io" - "net/http" - "sync/atomic" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type mockTransport struct { - response *http.Response - err error -} - -func (m *mockTransport) RoundTrip(*http.Request) (*http.Response, error) { - return m.response, m.err -} - -func TestNewByteLimitedTransport(t *testing.T) { - tests := []struct { - name string - transport http.RoundTripper - limit int64 - }{ - { - name: "with custom transport", - transport: &mockTransport{}, - limit: 1000, - }, - { - name: "with nil transport", - transport: nil, - limit: 1000, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - blt := NewByteLimitedTransport(tt.transport, tt.limit) - assert.NotNil(t, blt) - assert.Equal(t, tt.limit, blt.Limit) - assert.Equal(t, int64(0), blt.Bytes) - - if tt.transport == nil { - assert.NotNil(t, blt.Transport) - assert.NotEqual(t, http.DefaultTransport, blt.Transport) - } else { - assert.Equal(t, tt.transport, blt.Transport) - } - }) - } -} - -func TestByteLimitedTransport_RoundTrip(t *testing.T) { - tests := []struct { - name string - responseBody string - limit int64 - expectedError error - }{ - { - name: "under limit", - responseBody: "small response", - limit: 100, - expectedError: nil, - }, - { - name: "exceeds limit", - responseBody: "this response will exceed the byte limit", - limit: 10, - expectedError: errBytesLimitExceeded, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockResp := &http.Response{ - Body: io.NopCloser(bytes.NewBufferString(tt.responseBody)), - } - mockTransport := &mockTransport{response: mockResp} - - blt := NewByteLimitedTransport(mockTransport, tt.limit) - resp, err := blt.RoundTrip(&http.Request{}) - require.NoError(t, err) - defer func() { - closeErr := resp.Body.Close() - assert.NoError(t, closeErr, "failed to close response body") - }() - - data, err := io.ReadAll(resp.Body) - if tt.expectedError != nil { - assert.True(t, errors.Is(err, tt.expectedError), "expected error %v, got %v", tt.expectedError, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.responseBody, string(data)) - } - }) - } -} - -func TestByteLimitedReader_Close(t *testing.T) { - mockBody := io.NopCloser(bytes.NewBufferString("test")) - var byteCount int64 - reader := &byteLimitedReader{ - reader: mockBody, - limit: 100, - bytes: &byteCount, - } - - err := reader.Close() - assert.NoError(t, err) -} - -func TestByteLimitedReader_AtomicCounting(t *testing.T) { - var byteCount int64 - reader := &byteLimitedReader{ - reader: io.NopCloser(bytes.NewBufferString("test data")), - limit: 5, - bytes: &byteCount, - } - - // First read should succeed - buf := make([]byte, 4) - n, err := reader.Read(buf) - assert.NoError(t, err) - assert.Equal(t, 4, n) - - // Second read should fail due to limit - n, err = reader.Read(buf) - assert.True(t, errors.Is(err, errBytesLimitExceeded), "expected error %v, got %v", errBytesLimitExceeded, err) - assert.Equal(t, 0, n) - - // Verify atomic counter - assert.Greater(t, atomic.LoadInt64(&byteCount), int64(5)) -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/worktree_mock.go b/pkg/registry/apis/provisioning/repository/go-git/worktree_mock.go deleted file mode 100644 index 0a85a14e12b..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/worktree_mock.go +++ /dev/null @@ -1,261 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package gogit - -import ( - billy "github.com/go-git/go-billy/v5" - git "github.com/go-git/go-git/v5" - - mock "github.com/stretchr/testify/mock" - - plumbing "github.com/go-git/go-git/v5/plumbing" -) - -// MockWorktree is an autogenerated mock type for the Worktree type -type MockWorktree struct { - mock.Mock -} - -type MockWorktree_Expecter struct { - mock *mock.Mock -} - -func (_m *MockWorktree) EXPECT() *MockWorktree_Expecter { - return &MockWorktree_Expecter{mock: &_m.Mock} -} - -// Add provides a mock function with given fields: path -func (_m *MockWorktree) Add(path string) (plumbing.Hash, error) { - ret := _m.Called(path) - - if len(ret) == 0 { - panic("no return value specified for Add") - } - - var r0 plumbing.Hash - var r1 error - if rf, ok := ret.Get(0).(func(string) (plumbing.Hash, error)); ok { - return rf(path) - } - if rf, ok := ret.Get(0).(func(string) plumbing.Hash); ok { - r0 = rf(path) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(plumbing.Hash) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(path) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockWorktree_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' -type MockWorktree_Add_Call struct { - *mock.Call -} - -// Add is a helper method to define mock.On call -// - path string -func (_e *MockWorktree_Expecter) Add(path interface{}) *MockWorktree_Add_Call { - return &MockWorktree_Add_Call{Call: _e.mock.On("Add", path)} -} - -func (_c *MockWorktree_Add_Call) Run(run func(path string)) *MockWorktree_Add_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *MockWorktree_Add_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Add_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockWorktree_Add_Call) RunAndReturn(run func(string) (plumbing.Hash, error)) *MockWorktree_Add_Call { - _c.Call.Return(run) - return _c -} - -// Commit provides a mock function with given fields: message, opts -func (_m *MockWorktree) Commit(message string, opts *git.CommitOptions) (plumbing.Hash, error) { - ret := _m.Called(message, opts) - - if len(ret) == 0 { - panic("no return value specified for Commit") - } - - var r0 plumbing.Hash - var r1 error - if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) (plumbing.Hash, error)); ok { - return rf(message, opts) - } - if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) plumbing.Hash); ok { - r0 = rf(message, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(plumbing.Hash) - } - } - - if rf, ok := ret.Get(1).(func(string, *git.CommitOptions) error); ok { - r1 = rf(message, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockWorktree_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' -type MockWorktree_Commit_Call struct { - *mock.Call -} - -// Commit is a helper method to define mock.On call -// - message string -// - opts *git.CommitOptions -func (_e *MockWorktree_Expecter) Commit(message interface{}, opts interface{}) *MockWorktree_Commit_Call { - return &MockWorktree_Commit_Call{Call: _e.mock.On("Commit", message, opts)} -} - -func (_c *MockWorktree_Commit_Call) Run(run func(message string, opts *git.CommitOptions)) *MockWorktree_Commit_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(*git.CommitOptions)) - }) - return _c -} - -func (_c *MockWorktree_Commit_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Commit_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockWorktree_Commit_Call) RunAndReturn(run func(string, *git.CommitOptions) (plumbing.Hash, error)) *MockWorktree_Commit_Call { - _c.Call.Return(run) - return _c -} - -// Filesystem provides a mock function with no fields -func (_m *MockWorktree) Filesystem() billy.Filesystem { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Filesystem") - } - - var r0 billy.Filesystem - if rf, ok := ret.Get(0).(func() billy.Filesystem); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(billy.Filesystem) - } - } - - return r0 -} - -// MockWorktree_Filesystem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Filesystem' -type MockWorktree_Filesystem_Call struct { - *mock.Call -} - -// Filesystem is a helper method to define mock.On call -func (_e *MockWorktree_Expecter) Filesystem() *MockWorktree_Filesystem_Call { - return &MockWorktree_Filesystem_Call{Call: _e.mock.On("Filesystem")} -} - -func (_c *MockWorktree_Filesystem_Call) Run(run func()) *MockWorktree_Filesystem_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockWorktree_Filesystem_Call) Return(_a0 billy.Filesystem) *MockWorktree_Filesystem_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockWorktree_Filesystem_Call) RunAndReturn(run func() billy.Filesystem) *MockWorktree_Filesystem_Call { - _c.Call.Return(run) - return _c -} - -// Remove provides a mock function with given fields: path -func (_m *MockWorktree) Remove(path string) (plumbing.Hash, error) { - ret := _m.Called(path) - - if len(ret) == 0 { - panic("no return value specified for Remove") - } - - var r0 plumbing.Hash - var r1 error - if rf, ok := ret.Get(0).(func(string) (plumbing.Hash, error)); ok { - return rf(path) - } - if rf, ok := ret.Get(0).(func(string) plumbing.Hash); ok { - r0 = rf(path) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(plumbing.Hash) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(path) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockWorktree_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' -type MockWorktree_Remove_Call struct { - *mock.Call -} - -// Remove is a helper method to define mock.On call -// - path string -func (_e *MockWorktree_Expecter) Remove(path interface{}) *MockWorktree_Remove_Call { - return &MockWorktree_Remove_Call{Call: _e.mock.On("Remove", path)} -} - -func (_c *MockWorktree_Remove_Call) Run(run func(path string)) *MockWorktree_Remove_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) - }) - return _c -} - -func (_c *MockWorktree_Remove_Call) Return(_a0 plumbing.Hash, _a1 error) *MockWorktree_Remove_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockWorktree_Remove_Call) RunAndReturn(run func(string) (plumbing.Hash, error)) *MockWorktree_Remove_Call { - _c.Call.Return(run) - return _c -} - -// NewMockWorktree creates a new instance of MockWorktree. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockWorktree(t interface { - mock.TestingT - Cleanup(func()) -}) *MockWorktree { - mock := &MockWorktree{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper.go deleted file mode 100644 index e6954d27b81..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/wrapper.go +++ /dev/null @@ -1,468 +0,0 @@ -package gogit - -import ( - "context" - "errors" - "fmt" - "io" - "io/fs" - "net/http" - "os" - "strings" - "time" - - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/util" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/client" - githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/validation/field" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" - "github.com/grafana/grafana/pkg/util/httpclient" -) - -const ( - // maxOperationBytes is the maximum size of a git operation in bytes (1 GB) - maxOperationBytes = int64(1 << 30) - maxOperationTimeout = 10 * time.Minute -) - -func init() { - // Create a size-limited writer that will cancel the context if size is exceeded - limitedTransport := NewByteLimitedTransport(httpclient.NewHTTPTransport(), maxOperationBytes) - httpClient := githttp.NewClient(&http.Client{ - Transport: limitedTransport, - }) - client.InstallProtocol("https", httpClient) - client.InstallProtocol("http", httpClient) -} - -//go:generate mockery --name=Worktree --output=mocks --inpackage --filename=worktree_mock.go --with-expecter -type Worktree interface { - Commit(message string, opts *git.CommitOptions) (plumbing.Hash, error) - Remove(path string) (plumbing.Hash, error) - Add(path string) (plumbing.Hash, error) - Filesystem() billy.Filesystem -} - -type worktree struct { - *git.Worktree -} - -//go:generate mockery --name=Repository --output=mocks --inpackage --filename=repository_mock.go --with-expecter -type Repository interface { - PushContext(ctx context.Context, o *git.PushOptions) error -} - -func (w *worktree) Filesystem() billy.Filesystem { - return w.Worktree.Filesystem -} - -var _ repository.Repository = (*GoGitRepo)(nil) - -type GoGitRepo struct { - config *provisioning.Repository - decryptedPassword string - opts repository.CloneOptions - - repo Repository - tree Worktree - dir string // file path to worktree root (necessary? should use billy) -} - -// This will create a new clone every time -// As structured, it is valid for one context and should not be shared across multiple requests -func Clone( - ctx context.Context, - root string, - config *provisioning.Repository, - opts repository.CloneOptions, - secrets secrets.Service, -) (repository.ClonedRepository, error) { - if root == "" { - return nil, fmt.Errorf("missing root config") - } - - if config.Namespace == "" { - return nil, fmt.Errorf("config is missing namespace") - } - - if config.Name == "" { - return nil, fmt.Errorf("config is missing name") - } - - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return nil, err - } - } - - // add a timeout to the operation - timeout := maxOperationTimeout - if opts.Timeout > 0 { - timeout = opts.Timeout - } - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) - if err != nil { - return nil, fmt.Errorf("error decrypting token: %w", err) - } - - if err := os.MkdirAll(root, 0700); err != nil { - return nil, fmt.Errorf("create root dir: %w", err) - } - - dir, err := os.MkdirTemp(root, fmt.Sprintf("clone-%s-%s-", config.Namespace, config.Name)) - if err != nil { - return nil, fmt.Errorf("create temp clone dir: %w", err) - } - - progress := opts.Progress - if progress == nil { - progress = io.Discard - } - - repo, tree, err := clone(ctx, config, opts, decrypted, dir, progress) - if err != nil { - if err := os.RemoveAll(dir); err != nil { - return nil, fmt.Errorf("remove temp clone dir after clone failed: %w", err) - } - - return nil, fmt.Errorf("clone: %w", err) - } - - return &GoGitRepo{ - config: config, - tree: &worktree{Worktree: tree}, - opts: opts, - decryptedPassword: string(decrypted), - repo: repo, - dir: dir, - }, nil -} - -func clone(ctx context.Context, config *provisioning.Repository, opts repository.CloneOptions, decrypted []byte, dir string, progress io.Writer) (*git.Repository, *git.Worktree, error) { - gitcfg := config.Spec.GitHub - url := gitcfg.URL - if !strings.HasPrefix(url, "file://") { - url = fmt.Sprintf("%s.git", url) - } - - branch := plumbing.NewBranchReferenceName(gitcfg.Branch) - cloneOpts := &git.CloneOptions{ - ReferenceName: branch, - Auth: &githttp.BasicAuth{ - Username: "grafana", // this can be anything except an empty string for PAT - Password: string(decrypted), // TODO... will need to get from a service! - }, - URL: url, - Progress: progress, - } - - repo, err := git.PlainCloneContext(ctx, dir, false, cloneOpts) - if errors.Is(err, plumbing.ErrReferenceNotFound) && opts.CreateIfNotExists { - cloneOpts.ReferenceName = "" // empty - repo, err = git.PlainCloneContext(ctx, dir, false, cloneOpts) - if err == nil { - worktree, err := repo.Worktree() - if err != nil { - return nil, nil, err - } - err = worktree.Checkout(&git.CheckoutOptions{ - Branch: branch, - Force: true, - Create: true, - }) - if err != nil { - return nil, nil, fmt.Errorf("unable to create new branch: %w", err) - } - } - } else if err != nil { - return nil, nil, fmt.Errorf("clone error: %w", err) - } - - rcfg, err := repo.Config() - if err != nil { - return nil, nil, fmt.Errorf("error reading repository config %w", err) - } - - origin := rcfg.Remotes["origin"] - if origin == nil { - return nil, nil, fmt.Errorf("missing origin remote %w", err) - } - - if url != origin.URLs[0] { - return nil, nil, fmt.Errorf("unexpected remote (expected: %s, found: %s)", url, origin.URLs[0]) - } - - worktree, err := repo.Worktree() - if err != nil { - return nil, nil, fmt.Errorf("get worktree: %w", err) - } - - return repo, worktree, nil -} - -// After making changes to the worktree, push changes -func (g *GoGitRepo) Push(ctx context.Context, opts repository.PushOptions) error { - timeout := maxOperationTimeout - if opts.Timeout > 0 { - timeout = opts.Timeout - } - - progress := opts.Progress - if progress == nil { - progress = io.Discard - } - - if opts.BeforeFn != nil { - if err := opts.BeforeFn(); err != nil { - return err - } - } - - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - if !g.opts.PushOnWrites { - _, err := g.tree.Commit("exported from grafana", &git.CommitOptions{ - All: true, // Add everything that changed - }) - if err != nil { - // empty commit is fine -- no change - if !errors.Is(err, git.ErrEmptyCommit) { - return err - } - } - } - - err := g.repo.PushContext(ctx, &git.PushOptions{ - Progress: progress, - Force: true, // avoid fast-forward-errors - Auth: &githttp.BasicAuth{ // reuse logic from clone? - Username: "grafana", - Password: g.decryptedPassword, - }, - }) - if errors.Is(err, git.NoErrAlreadyUpToDate) { - return nil // same as the target - } - return err -} - -func (g *GoGitRepo) Remove(ctx context.Context) error { - return os.RemoveAll(g.dir) -} - -// Config implements repository.Repository. -func (g *GoGitRepo) Config() *provisioning.Repository { - return g.config -} - -// ReadTree implements repository.Repository. -func (g *GoGitRepo) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - var treePath string - if g.config.Spec.GitHub.Path != "" { - treePath = g.config.Spec.GitHub.Path - } - treePath = safepath.Clean(treePath) - - entries := make([]repository.FileTreeEntry, 0, 100) - err := util.Walk(g.tree.Filesystem(), treePath, func(path string, info fs.FileInfo, err error) error { - // We already have an error, just pass it onwards. - if err != nil || - // This is the root of the repository (or should pretend to be) - safepath.Clean(path) == "" || path == treePath || - // This is the Git data - (treePath == "" && (strings.HasPrefix(path, ".git/") || path == ".git")) { - return err - } - if treePath != "" { - path = strings.TrimPrefix(path, treePath) - } - entry := repository.FileTreeEntry{ - Path: strings.TrimLeft(path, "/"), - Size: info.Size(), - } - if !info.IsDir() { - entry.Blob = true - // For a real instance, this will likely be based on: - // https://github.com/go-git/go-git/blob/main/_examples/ls/main.go#L25 - entry.Hash = fmt.Sprintf("TODO/%d", info.Size()) // but not used for - } - entries = append(entries, entry) - return err - }) - if errors.Is(err, fs.ErrNotExist) { - // We intentionally ignore this case, as it is expected - } else if err != nil { - return nil, fmt.Errorf("walk tree for ref '%s': %w", ref, err) - } - return entries, nil -} - -func (g *GoGitRepo) Test(ctx context.Context) (*provisioning.TestResults, error) { - return &provisioning.TestResults{ - Success: g.tree != nil, - }, nil -} - -// Update implements repository.Repository. -func (g *GoGitRepo) Update(ctx context.Context, path string, ref string, data []byte, message string) error { - return g.Write(ctx, path, ref, data, message) -} - -// Create implements repository.Repository. -func (g *GoGitRepo) Create(ctx context.Context, path string, ref string, data []byte, message string) error { - // FIXME: this means we would override files - return g.Write(ctx, path, ref, data, message) -} - -// Write implements repository.Repository. -func (g *GoGitRepo) Write(ctx context.Context, fpath string, ref string, data []byte, message string) error { - if err := verifyPathWithoutRef(fpath, ref); err != nil { - return err - } - fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath) - - // FIXME: this means that won't export empty folders - // should we create them with a .keep file? - // For folders, just create the folder and ignore the commit - if safepath.IsDir(fpath) { - return g.tree.Filesystem().MkdirAll(fpath, 0750) - } - - dir := safepath.Dir(fpath) - if dir != "" { - err := g.tree.Filesystem().MkdirAll(dir, 0750) - if err != nil { - return err - } - } - - file, err := g.tree.Filesystem().Create(fpath) - if err != nil { - return err - } - _, err = file.Write(data) - if err != nil { - return err - } - - _, err = g.tree.Add(fpath) - if err != nil { - return err - } - return g.maybeCommit(ctx, message) -} - -func (g *GoGitRepo) maybeCommit(ctx context.Context, message string) error { - // Skip commit for each file - if !g.opts.PushOnWrites { - return nil - } - - opts := &git.CommitOptions{ - Author: &object.Signature{ - Name: "grafana", - }, - } - sig := repository.GetAuthorSignature(ctx) - if sig != nil && sig.Name != "" { - opts.Author.Name = sig.Name - opts.Author.Email = sig.Email - opts.Author.When = sig.When - } - if opts.Author.When.IsZero() { - opts.Author.When = time.Now() - } - - _, err := g.tree.Commit(message, opts) - if errors.Is(err, git.ErrEmptyCommit) { - return nil // empty commit is fine -- no change - } - return err -} - -// Delete implements repository.Repository. -func (g *GoGitRepo) Delete(ctx context.Context, fpath string, ref string, message string) error { - if err := verifyPathWithoutRef(fpath, ref); err != nil { - return err - } - - fpath = safepath.Join(g.config.Spec.GitHub.Path, fpath) - if _, err := g.tree.Remove(fpath); err != nil { - if errors.Is(err, fs.ErrNotExist) { - return repository.ErrFileNotFound - } - - return err - } - return g.maybeCommit(ctx, message) -} - -// Read implements repository.Repository. -func (g *GoGitRepo) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { - if err := verifyPathWithoutRef(path, ref); err != nil { - return nil, err - } - readPath := safepath.Join(g.config.Spec.GitHub.Path, path) - stat, err := g.tree.Filesystem().Lstat(readPath) - if errors.Is(err, fs.ErrNotExist) { - return nil, repository.ErrFileNotFound - } else if err != nil { - return nil, fmt.Errorf("stat path '%s': %w", readPath, err) - } - info := &repository.FileInfo{ - Path: path, - Modified: &metav1.Time{ - Time: stat.ModTime(), - }, - } - if !stat.IsDir() { - f, err := g.tree.Filesystem().Open(readPath) - if err != nil { - return nil, fmt.Errorf("open file '%s': %w", readPath, err) - } - info.Data, err = io.ReadAll(f) - if err != nil { - return nil, fmt.Errorf("read file '%s': %w", readPath, err) - } - } - return info, err -} - -func verifyPathWithoutRef(path string, ref string) error { - if path == "" { - return fmt.Errorf("expected path") - } - if ref != "" { - return fmt.Errorf("ref unsupported") - } - return nil -} - -// History implements repository.Repository. -func (g *GoGitRepo) History(ctx context.Context, path string, ref string) ([]provisioning.HistoryItem, error) { - return nil, &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Message: "history is not yet implemented", - Code: http.StatusNotImplemented, - }, - } -} - -// Validate implements repository.Repository. -func (g *GoGitRepo) Validate() field.ErrorList { - return nil -} diff --git a/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go b/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go deleted file mode 100644 index 79a2951e8c8..00000000000 --- a/pkg/registry/apis/provisioning/repository/go-git/wrapper_test.go +++ /dev/null @@ -1,1642 +0,0 @@ -package gogit - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "sort" - "testing" - "time" - - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - plumbing "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport/client" - githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/plumbing/transport/server" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/go-git/go-git/v5/storage/memory" - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" -) - -type dummySecret struct{} - -func (d *dummySecret) Decrypt(ctx context.Context, encrypted []byte) ([]byte, error) { - token, ok := os.LookupEnv("gitwraptoken") - if !ok { - return nil, fmt.Errorf("missing token in environment") - } - return []byte(token), nil -} - -func (d *dummySecret) Encrypt(ctx context.Context, plain []byte) ([]byte, error) { - panic("not implemented") -} - -// FIXME!! NOTE!!!!! -// This is really just a sketchpad while trying to get things working -// the test makes destructive changes to a real git repository :) -// this should be removed before committing to main (likely sooner) -// and replaced with integration tests that check the more specific results -func TestGoGitWrapper(t *testing.T) { - _, ok := os.LookupEnv("gitwraptoken") - if !ok { - t.Skipf("no token found in environment") - } - - ctx := context.Background() - wrap, err := Clone(ctx, "testdata/clone", &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "ns", - Name: "unit-tester", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/git-ui-sync-demo", - Branch: "ryan-test", - }, - }, - }, - repository.CloneOptions{ - PushOnWrites: false, - CreateIfNotExists: true, - Progress: os.Stdout, - }, - &dummySecret{}, - ) - require.NoError(t, err) - - tree, err := wrap.ReadTree(ctx, "") - require.NoError(t, err) - - jj, err := json.MarshalIndent(tree, "", " ") - require.NoError(t, err) - - fmt.Printf("TREE:%s\n", string(jj)) - - ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ - Name: "xxxxx", - Email: "rrr@yyyy.zzz", - When: time.Now(), - }) - - for i := 0; i < 10; i++ { - fname := fmt.Sprintf("deep/path/in/test_%d.txt", i) - fmt.Printf("Write:%s\n", fname) - err = wrap.Write(ctx, fname, "", []byte(fmt.Sprintf("body/%d %s", i, time.Now())), "the commit message") - require.NoError(t, err) - } - - fmt.Printf("push...\n") - err = wrap.Push(ctx, repository.PushOptions{ - Timeout: 10, - Progress: os.Stdout, - }) - require.NoError(t, err) -} - -func TestReadTree(t *testing.T) { - dir := t.TempDir() - gitRepo, err := git.PlainInit(dir, false) - require.NoError(t, err, "failed to init a new git repository") - tree, err := gitRepo.Worktree() - require.NoError(t, err, "failed to get worktree") - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: v0alpha1.RepositorySpec{ - Title: "test", - Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, - Type: v0alpha1.GitHubRepositoryType, - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/grafana/__unit-test", - Path: "grafana/", - Branch: "main", - }, - }, - Status: v0alpha1.RepositoryStatus{}, - }, - decryptedPassword: "password", - - repo: gitRepo, - tree: &worktree{ - Worktree: tree, - }, - dir: dir, - } - - err = os.WriteFile(filepath.Join(dir, "test.txt"), []byte("test"), 0644) - require.NoError(t, err, "failed to write test file") - - err = os.Mkdir(filepath.Join(dir, "grafana"), 0750) - require.NoError(t, err, "failed to mkdir grafana") - - err = os.WriteFile(filepath.Join(dir, "grafana", "test2.txt"), []byte("test"), 0644) - require.NoError(t, err, "failed to write grafana/test2 file") - - ctx := context.Background() - entries, err := repo.ReadTree(ctx, "HEAD") - require.NoError(t, err, "failed to read tree") - - // Here is the meat of why this test exists: the ReadTree call should only read the config.Spec.GitHub.Path files. - // All prefixes are removed (i.e. a file is just its name, not ${Path}/${Name}). - // And it does not include the directory in the listing, as it pretends to be the root. - require.Len(t, entries, 1, "entries from ReadTree") - require.Equal(t, entries[0].Path, "test2.txt", "entry path") -} - -func TestGoGitRepo_History(t *testing.T) { - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - } - - // Test History method - ctx := context.Background() - _, err := repo.History(ctx, "test.txt", "") - require.Error(t, err, "History should return an error as it's not implemented") - require.Contains(t, err.Error(), "history is not yet implemented") -} - -func TestGoGitRepo_Validate(t *testing.T) { - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - } - - // Test Validate method - errs := repo.Validate() - require.Empty(t, errs, "Validate should return no errors") -} - -func TestGoGitRepo_Read(t *testing.T) { - // Setup test cases - tests := []struct { - name string - path string - ref string - setupMock func(fs billy.Filesystem) - expectError bool - errorType error - checkResult func(t *testing.T, info *repository.FileInfo) - }{ - { - name: "successfully read file", - path: "test.txt", - ref: "", - setupMock: func(fs billy.Filesystem) { - // Create a test file - f, err := fs.Create("grafana/test.txt") - require.NoError(t, err, "failed to create test file") - _, err = f.Write([]byte("test content")) - require.NoError(t, err, "failed to write test content") - err = f.Close() - require.NoError(t, err, "failed to close test file") - }, - expectError: false, - checkResult: func(t *testing.T, info *repository.FileInfo) { - require.Equal(t, "test.txt", info.Path) - require.Equal(t, "test content", string(info.Data)) - require.NotNil(t, info.Modified) - }, - }, - { - name: "empty path", - path: "", - ref: "", - setupMock: func(fs billy.Filesystem) {}, - expectError: true, - errorType: fmt.Errorf("expected path"), - }, - { - name: "ref not supported", - path: "test.txt", - ref: "main", - setupMock: func(fs billy.Filesystem) {}, - expectError: true, - errorType: fmt.Errorf("ref unsupported"), - }, - { - name: "file not found", - path: "nonexistent.txt", - ref: "", - setupMock: func(fs billy.Filesystem) { - // Don't create the file - }, - expectError: true, - errorType: repository.ErrFileNotFound, - }, - { - name: "read directory", - path: "testdir", - ref: "", - setupMock: func(fs billy.Filesystem) { - // Create a test directory - err := fs.MkdirAll("grafana/testdir", 0755) - require.NoError(t, err, "failed to create test directory") - }, - expectError: false, - checkResult: func(t *testing.T, info *repository.FileInfo) { - require.Equal(t, "testdir", info.Path) - require.Nil(t, info.Data) - require.NotNil(t, info.Modified) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup filesystem and repo - fs := memfs.New() - tt.setupMock(fs) - - // Create a worktree with the filesystem - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: &worktree{ - Worktree: &git.Worktree{ - Filesystem: fs, - }, - }, - } - - // Test Read method - ctx := context.Background() - info, err := repo.Read(ctx, tt.path, tt.ref) - - // Check results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - if errors.Is(tt.errorType, repository.ErrFileNotFound) { - require.ErrorIs(t, err, repository.ErrFileNotFound) - } else { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } - } else { - require.NoError(t, err) - require.NotNil(t, info) - tt.checkResult(t, info) - } - }) - } -} - -func TestGoGitRepo_Delete(t *testing.T) { - tests := []struct { - name string - path string - ref string - pushOnWrite bool - setupMock func(mockTree *MockWorktree) - expectError bool - errorType error - }{ - { - name: "delete existing file", - path: "testfile.txt", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil) - }, - expectError: false, - }, - { - name: "delete non-existent file", - path: "nonexistent.txt", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/nonexistent.txt").Return(plumbing.Hash{}, fs.ErrNotExist) - }, - expectError: true, - errorType: repository.ErrFileNotFound, - }, - { - name: "delete with other error", - path: "testfile.txt", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, fmt.Errorf("some other error")) - }, - expectError: true, - errorType: fmt.Errorf("some other error"), - }, - { - name: "empty path", - path: "", - ref: "", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) {}, - expectError: true, - errorType: fmt.Errorf("expected path"), - }, - { - name: "with ref", - path: "testfile.txt", - ref: "main", - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - }, - expectError: true, - errorType: fmt.Errorf("ref unsupported"), - }, - { - name: "delete with push on write enabled", - path: "testfile.txt", - ref: "", - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil) - mockTree.On("Commit", "test delete", mock.MatchedBy(func(opts *git.CommitOptions) bool { - return opts.Author != nil && - opts.Author.Name == "Test User" && - opts.Author.Email == "test@example.com" && - opts.Author.When.After(time.Now().Add(-time.Minute)) && - opts.Author.When.Before(time.Now().Add(time.Minute)) - })).Return(plumbing.Hash{}, nil) - }, - expectError: false, - }, - { - name: "delete with empty commit", - path: "testfile.txt", - ref: "", - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - mockTree.On("Remove", "grafana/testfile.txt").Return(plumbing.Hash{}, nil) - mockTree.On("Commit", "test delete", mock.MatchedBy(func(opts *git.CommitOptions) bool { - return opts.Author != nil && - opts.Author.Name == "Test User" && - opts.Author.Email == "test@example.com" && - opts.Author.When.After(time.Now().Add(-time.Minute)) && - opts.Author.When.Before(time.Now().Add(time.Minute)) - })).Return(plumbing.Hash{}, git.ErrEmptyCommit) - }, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup filesystem and repo - - mockTree := NewMockWorktree(t) - tt.setupMock(mockTree) - - // Create a worktree with the filesystem - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: mockTree, - opts: repository.CloneOptions{ - PushOnWrites: tt.pushOnWrite, - }, - } - - // Test Delete method - ctx := context.Background() - // Set author signature for the test - ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ - Name: "Test User", - Email: "test@example.com", - When: time.Now(), - }) - - err := repo.Delete(ctx, tt.path, tt.ref, "test delete") - - // Check results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - if errors.Is(tt.errorType, repository.ErrFileNotFound) { - require.ErrorIs(t, err, repository.ErrFileNotFound) - } else { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } - } else { - require.NoError(t, err) - } - - mockTree.AssertExpectations(t) - }) - } -} - -// FIXME: missing coverage for Update / Create because we use Write for both -// when I think it shouldn't be the case as it's inconsistent with the other repository implementations -func TestGoGitRepo_Write(t *testing.T) { - tests := []struct { - name string - path string - ref string - data []byte - pushOnWrite bool - setupMock func(mockTree *MockWorktree) - expectError bool - errorType error - }{ - { - name: "successful write", - path: "test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash("abc123"), nil) - mockTree.On("Commit", "test write", mock.MatchedBy(func(opts *git.CommitOptions) bool { - return opts.Author != nil && - opts.Author.Name == "Test User" && - opts.Author.Email == "test@example.com" && - opts.Author.When.After(time.Now().Add(-time.Minute)) && - opts.Author.When.Before(time.Now().Add(time.Minute)) - })).Return(plumbing.NewHash("def456"), nil) - }, - expectError: false, - }, - { - name: "create folder only", - path: "testdir/", - ref: "", - data: []byte{}, - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - // No Add or Commit calls expected for directory creation - }, - expectError: false, - }, - { - name: "successful write without commit", - path: "test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: false, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash("abc123"), nil) - }, - expectError: false, - }, - { - name: "write with directory creation", - path: "dir/test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/dir/test.txt").Return(plumbing.NewHash("abc123"), nil) - mockTree.On("Commit", "test write", mock.Anything).Return(plumbing.NewHash("def456"), nil) - }, - expectError: false, - }, - { - name: "error on add", - path: "test.txt", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - fs := memfs.New() - mockTree.On("Filesystem").Return(fs) - mockTree.On("Add", "grafana/test.txt").Return(plumbing.NewHash(""), fmt.Errorf("add error")) - }, - expectError: true, - errorType: fmt.Errorf("add error"), - }, - { - name: "error with ref", - path: "test.txt", - ref: "main", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - // No mock setup needed as it should fail before using the mock - }, - expectError: true, - errorType: fmt.Errorf("ref unsupported"), - }, - { - name: "empty path", - path: "", - ref: "", - data: []byte("test content"), - pushOnWrite: true, - setupMock: func(mockTree *MockWorktree) { - // No mock setup needed as it should fail before using the mock - }, - expectError: true, - errorType: fmt.Errorf("expected path"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup filesystem and repo - mockTree := NewMockWorktree(t) - tt.setupMock(mockTree) - - // Create a worktree with the filesystem - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: mockTree, - opts: repository.CloneOptions{ - PushOnWrites: tt.pushOnWrite, - }, - } - - // Test Write method - ctx := context.Background() - // Set author signature for the test - ctx = repository.WithAuthorSignature(ctx, repository.CommitSignature{ - Name: "Test User", - Email: "test@example.com", - When: time.Now(), - }) - - err := repo.Update(ctx, tt.path, tt.ref, tt.data, "test write") - - // Check results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } else { - require.NoError(t, err) - } - - mockTree.AssertExpectations(t) - }) - } -} - -func TestGoGitRepo_Test(t *testing.T) { - tests := []struct { - name string - treeInitialized bool - expectedResult bool - }{ - { - name: "tree is initialized", - treeInitialized: true, - expectedResult: true, - }, - { - name: "tree is not initialized", - treeInitialized: false, - expectedResult: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mock tree - mockTree := NewMockWorktree(t) - - // Create repo with or without initialized tree - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: nil, - } - - if tt.treeInitialized { - repo.tree = mockTree - } - - // Test the Test method - ctx := context.Background() - result, err := repo.Test(ctx) - - // Verify results - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, tt.expectedResult, result.Success) - }) - } -} - -func TestGoGitRepo_Config(t *testing.T) { - // Create a test repository configuration - testConfig := &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - } - - // Create a repository instance with the test configuration - repo := &GoGitRepo{ - config: testConfig, - tree: NewMockWorktree(t), - } - - // Call the Config method - result := repo.Config() - - // Verify the result - require.NotNil(t, result) - require.Equal(t, testConfig, result) - require.Equal(t, "test-repo", result.Name) - require.Equal(t, "test-namespace", result.Namespace) - require.Equal(t, "grafana/", result.Spec.GitHub.Path) -} - -func TestGoGitRepo_Remove(t *testing.T) { - tests := []struct { - name string - setupMock func(t *testing.T) (*GoGitRepo, string) - expectError bool - expectedErrMsg string - }{ - { - name: "successful removal", - setupMock: func(t *testing.T) (*GoGitRepo, string) { - // Create a temporary directory that will be removed - tempDir, err := os.MkdirTemp("", "test-repo-*") - require.NoError(t, err) - - // Create a repository instance - repo := &GoGitRepo{ - dir: tempDir, - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - }, - } - - return repo, tempDir - }, - expectError: false, - }, - { - name: "directory already removed", - setupMock: func(t *testing.T) (*GoGitRepo, string) { - // Create a temporary directory - tempDir, err := os.MkdirTemp("", "test-repo-*") - require.NoError(t, err) - - // Remove it immediately to simulate it being already gone - err = os.RemoveAll(tempDir) - require.NoError(t, err) - - // Create a repository instance pointing to the removed directory - repo := &GoGitRepo{ - dir: tempDir, - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - }, - } - - return repo, tempDir - }, - expectError: false, // RemoveAll doesn't error if directory doesn't exist - }, - { - name: "invalid directory path", - setupMock: func(t *testing.T) (*GoGitRepo, string) { - // Create a repository instance with an invalid directory path - // that should cause an error when trying to remove - invalidPath := string([]byte{0}) - - repo := &GoGitRepo{ - dir: invalidPath, - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - }, - } - - return repo, invalidPath - }, - expectError: true, - expectedErrMsg: "invalid argument", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup the test - repo, _ := tt.setupMock(t) - - // Test the Remove method - ctx := context.Background() - err := repo.Remove(ctx) - - // Verify results - if tt.expectError { - require.Error(t, err) - if tt.expectedErrMsg != "" { - require.Contains(t, err.Error(), tt.expectedErrMsg) - } - } else { - require.NoError(t, err) - // Verify the directory no longer exists - _, statErr := os.Stat(repo.dir) - require.True(t, os.IsNotExist(statErr), "Directory should not exist after removal") - } - }) - } -} - -func TestGoGitRepo_Push(t *testing.T) { - tests := []struct { - name string - setupMock func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) - pushOpts repository.PushOptions - expectError bool - errorType error - }{ - { - name: "successful push", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.MatchedBy(func(o *git.PushOptions) bool { - if o.Auth == nil { - return false - } - // Verify we're using basic auth with expected credentials - basicAuth, ok := o.Auth.(*githttp.BasicAuth) - if !ok { - return false - } - return basicAuth.Username == "grafana" && basicAuth.Password == "test-token" - })).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push error", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(fmt.Errorf("network error")) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{}, - expectError: true, - errorType: fmt.Errorf("network error"), - }, - { - name: "already up to date", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(git.NoErrAlreadyUpToDate) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push with custom timeout", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{ - Timeout: 5 * time.Minute, - }, - expectError: false, - }, - { - name: "push with custom progress writer", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.MatchedBy(func(o *git.PushOptions) bool { - return o.Progress != nil && o.Progress != io.Discard - })).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{ - Progress: &bytes.Buffer{}, - }, - expectError: false, - }, - { - name: "push with BeforeFn success", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, mockRepo, nil - }, - pushOpts: repository.PushOptions{ - BeforeFn: func() error { - return nil - }, - }, - expectError: false, - }, - { - name: "push with BeforeFn error", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - // No mock expectations since BeforeFn will fail before PushContext is called - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: NewMockRepository(t), - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: true, - }, - } - - return repo, repo.repo.(*MockRepository), nil - }, - pushOpts: repository.PushOptions{ - BeforeFn: func() error { - return fmt.Errorf("before function failed") - }, - }, - expectError: true, - errorType: fmt.Errorf("before function failed"), - }, - { - name: "push with PushOnWrites=false commits changes", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - mockTree := NewMockWorktree(t) - mockTree.On("Commit", "exported from grafana", mock.MatchedBy(func(o *git.CommitOptions) bool { - return o.All == true - })).Return(plumbing.NewHash("abc123"), nil) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - tree: mockTree, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - } - - return repo, mockRepo, mockTree - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push with PushOnWrites=false and empty commit", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockRepo := NewMockRepository(t) - mockRepo.On("PushContext", mock.Anything, mock.Anything).Return(nil) - - mockTree := NewMockWorktree(t) - mockTree.On("Commit", "exported from grafana", mock.Anything).Return(plumbing.ZeroHash, git.ErrEmptyCommit) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: mockRepo, - tree: mockTree, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - } - - return repo, mockRepo, mockTree - }, - pushOpts: repository.PushOptions{}, - expectError: false, - }, - { - name: "push with PushOnWrites=false and commit error", - setupMock: func(t *testing.T) (*GoGitRepo, *MockRepository, *MockWorktree) { - mockTree := NewMockWorktree(t) - mockTree.On("Commit", "exported from grafana", mock.Anything).Return(plumbing.ZeroHash, fmt.Errorf("commit error")) - - repo := &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - repo: NewMockRepository(t), - tree: mockTree, - decryptedPassword: "test-token", - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - } - - return repo, repo.repo.(*MockRepository), mockTree - }, - pushOpts: repository.PushOptions{}, - expectError: true, - errorType: fmt.Errorf("commit error"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup the test - repo, mockRepo, mockTree := tt.setupMock(t) - - // Test the Push method - ctx := context.Background() - err := repo.Push(ctx, tt.pushOpts) - - // Verify results - if tt.expectError { - require.Error(t, err) - if tt.errorType != nil { - require.Contains(t, err.Error(), tt.errorType.Error()) - } - } else { - require.NoError(t, err) - } - - // Verify mock expectations if mocks were created - if mockRepo != nil { - mockRepo.AssertExpectations(t) - } - if mockTree != nil { - mockTree.AssertExpectations(t) - } - }) - } -} - -func TestGoGitRepo_ReadTree(t *testing.T) { - tests := []struct { - name string - setupMock func(t *testing.T) *GoGitRepo - ref string - expectError bool - expectedErrMsg string - expectedFiles []repository.FileTreeEntry - }{ - { - name: "successful read with files", - setupMock: func(t *testing.T) *GoGitRepo { - mockFS := memfs.New() - - // Create test files in the mock filesystem - require.NoError(t, mockFS.MkdirAll("grafana/folder1", 0750)) - file1, err := mockFS.Create("grafana/file1.txt") - require.NoError(t, err) - _, err = file1.Write([]byte("test content")) - require.NoError(t, err) - require.NoError(t, file1.Close()) - - file2, err := mockFS.Create("grafana/folder1/file2.txt") - require.NoError(t, err) - _, err = file2.Write([]byte("nested file content")) - require.NoError(t, err) - require.NoError(t, file2.Close()) - - mockTree := NewMockWorktree(t) - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "grafana/", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, - expectedFiles: []repository.FileTreeEntry{ - {Path: "file1.txt", Size: 12, Blob: true, Hash: "TODO/12"}, - {Path: "folder1", Size: 0, Blob: false}, - {Path: "folder1/file2.txt", Size: 19, Blob: true, Hash: "TODO/19"}, - }, - }, - { - name: "filesystem error", - setupMock: func(t *testing.T) *GoGitRepo { - mockTree := NewMockWorktree(t) - mockFS := memfs.New() - - // Create a filesystem that will return an error when accessed - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "non-existent-path/", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, // ReadTree handles fs.ErrNotExist by returning empty entries - expectedFiles: []repository.FileTreeEntry{}, - }, - { - name: "successful read with empty path", - setupMock: func(t *testing.T) *GoGitRepo { - mockFS := memfs.New() - - // Create test files in the mock filesystem - file1, err := mockFS.Create("file1.txt") - require.NoError(t, err) - _, err = file1.Write([]byte("test content")) - require.NoError(t, err) - require.NoError(t, file1.Close()) - - require.NoError(t, mockFS.MkdirAll("folder1", 0750)) - file2, err := mockFS.Create("folder1/file2.txt") - require.NoError(t, err) - _, err = file2.Write([]byte("nested file content")) - require.NoError(t, err) - require.NoError(t, file2.Close()) - - // Create .git directory which should be ignored - require.NoError(t, mockFS.MkdirAll(".git", 0750)) - gitFile, err := mockFS.Create(".git/config") - require.NoError(t, err) - _, err = gitFile.Write([]byte("git config")) - require.NoError(t, err) - require.NoError(t, gitFile.Close()) - - mockTree := NewMockWorktree(t) - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, - expectedFiles: []repository.FileTreeEntry{ - {Path: "file1.txt", Size: 12, Blob: true, Hash: "TODO/12"}, - {Path: "folder1", Size: 0, Blob: false}, - {Path: "folder1/file2.txt", Size: 19, Blob: true, Hash: "TODO/19"}, - }, - }, - { - name: "filesystem error", - setupMock: func(t *testing.T) *GoGitRepo { - mockTree := NewMockWorktree(t) - mockFS := memfs.New() - - // Create a filesystem that will return an error when accessed - mockTree.On("Filesystem").Return(mockFS) - - return &GoGitRepo{ - config: &v0alpha1.Repository{ - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - Path: "non-existent-path/", - }, - }, - }, - tree: mockTree, - } - }, - ref: "main", - expectError: false, // ReadTree handles fs.ErrNotExist by returning empty entries - expectedFiles: []repository.FileTreeEntry{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup the test - repo := tt.setupMock(t) - - // Test the ReadTree method - ctx := context.Background() - entries, err := repo.ReadTree(ctx, tt.ref) - - // Verify results - if tt.expectError { - require.Error(t, err) - if tt.expectedErrMsg != "" { - require.Contains(t, err.Error(), tt.expectedErrMsg) - } - } else { - require.NoError(t, err) - - // Sort entries for consistent comparison - sort.Slice(entries, func(i, j int) bool { - return entries[i].Path < entries[j].Path - }) - sort.Slice(tt.expectedFiles, func(i, j int) bool { - return tt.expectedFiles[i].Path < tt.expectedFiles[j].Path - }) - - require.Equal(t, len(tt.expectedFiles), len(entries), "Number of entries should match") - for i, expected := range tt.expectedFiles { - require.Equal(t, expected.Path, entries[i].Path, "Path should match") - require.Equal(t, expected.Size, entries[i].Size, "Size should match") - require.Equal(t, expected.Blob, entries[i].Blob, "Blob flag should match") - if expected.Blob { - require.Equal(t, expected.Hash, entries[i].Hash, "Hash should match") - } - } - } - - // Verify mock expectations - repo.tree.(*MockWorktree).AssertExpectations(t) - }) - } -} - -func TestClone(t *testing.T) { - tests := []struct { - name string - root string - config *v0alpha1.Repository - createRepo bool - opts repository.CloneOptions - setupMock func(secrets *secrets.MockService) - expectError bool - errorMsg string - }{ - { - name: "successful clone", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - createRepo: true, - opts: repository.CloneOptions{ - PushOnWrites: false, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - }, - expectError: false, - }, - { - name: "successful clone with create if not exists", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "non-existent-branch", - }, - }, - }, - createRepo: true, - opts: repository.CloneOptions{ - PushOnWrites: false, - CreateIfNotExists: true, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - }, - expectError: false, - }, - { - name: "timeout cancellation", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - opts: repository.CloneOptions{ - Timeout: 1 * time.Millisecond, // Very short timeout to trigger cancellation - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - // Simulate a slow operation that will be cancelled by timeout - time.Sleep(20 * time.Millisecond) - }, - expectError: true, - errorMsg: "context deadline exceeded", - }, - { - name: "empty root", - root: "", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "missing root config", - }, - { - name: "missing namespace", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-repo", - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "missing namespace", - }, - { - name: "missing name", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "missing name", - }, - { - name: "beforeFn error", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - }, - opts: repository.CloneOptions{ - BeforeFn: func() error { - return fmt.Errorf("beforeFn error") - }, - }, - setupMock: func(mockSecrets *secrets.MockService) {}, - expectError: true, - errorMsg: "beforeFn error", - }, - { - name: "secret decryption error", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - EncryptedToken: []byte("test-token"), - }, - }, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), fmt.Errorf("error decrypting token")) - }, - expectError: true, - errorMsg: "error decrypting token", - }, - { - name: "clone error", - root: "testdata/clone", - config: &v0alpha1.Repository{ - ObjectMeta: v1.ObjectMeta{ - Namespace: "test-ns", - Name: "test-repo", - }, - Spec: v0alpha1.RepositorySpec{ - GitHub: &v0alpha1.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - setupMock: func(mockSecrets *secrets.MockService) { - mockSecrets.On("Decrypt", mock.Anything, mock.Anything).Return([]byte("test-token"), nil) - }, - expectError: true, - errorMsg: "clone error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup test environment - mockSecrets := secrets.NewMockService(t) - tt.setupMock(mockSecrets) - - // Create a temporary directory for each test - if tt.root != "" { - tempDir := t.TempDir() - tt.root = tempDir - } - if tt.createRepo { - tt.config.Spec.GitHub.URL = createTestRepo(t) - } - - // Execute the test - ctx := context.Background() - repo, err := Clone(ctx, tt.root, tt.config, tt.opts, mockSecrets) - - // Verify results - if tt.expectError { - require.Error(t, err) - require.Contains(t, err.Error(), tt.errorMsg) - require.Nil(t, repo) - } else { - require.NoError(t, err) - require.NotNil(t, repo) - - // Verify the returned repository - gitRepo, ok := repo.(*GoGitRepo) - require.True(t, ok) - require.Equal(t, tt.config, gitRepo.config) - require.NotEmpty(t, gitRepo.dir) - require.NotNil(t, gitRepo.tree) - require.NotNil(t, gitRepo.repo) - - // Clean up - err = repo.Remove(ctx) - require.NoError(t, err) - } - mockSecrets.AssertExpectations(t) - }) - } -} - -func createTestRepo(t *testing.T) string { - // Create memory filesystem - fs := memfs.New() - - // Initialize new repo - repo, err := git.Init(memory.NewStorage(), fs) - require.NoError(t, err, "Failed to init test repo") - - w, err := repo.Worktree() - require.NoError(t, err, "Failed to get worktree") - - // Create a dummy file - f, err := fs.Create("README.md") - require.NoError(t, err, "Failed to create file") - _, err = f.Write([]byte("Hello, world!")) - require.NoError(t, err, "Failed to write content") - err = f.Close() - require.NoError(t, err, "Failed to close file") - - // Add and commit the file - _, err = w.Add("README.md") - require.NoError(t, err, "Failed to add file") - - // Create initial commit - _, err = w.Commit("initial commit", &git.CommitOptions{ - Author: &object.Signature{ - Name: "Test User", - Email: "test@example.com", - When: time.Now(), - }, - }) - require.NoError(t, err, "Failed to commit") - // Create a branch - headRef, err := repo.Head() - require.NoError(t, err, "Failed to get HEAD reference") - - // Create a new branch reference pointing to the current HEAD commit - branchRef := plumbing.NewBranchReferenceName("main") - ref := plumbing.NewHashReference(branchRef, headRef.Hash()) - - // Save the reference to create the branch - err = repo.Storer.SetReference(ref) - require.NoError(t, err, "Failed to create branch") - - // Checkout the new branch - err = w.Checkout(&git.CheckoutOptions{ - Branch: branchRef, - }) - require.NoError(t, err, "Failed to checkout branch") - - // Create a map of repositories for the server - repos := make(map[string]*git.Repository) - repos["test-repo.git"] = repo - - // Create and install the server - loader := server.MapLoader{ - "file://test-repo.git": repo.Storer, - } - srv := server.NewServer(loader) - client.InstallProtocol("file", srv) - - return "file://test-repo.git" -} diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local/local.go similarity index 87% rename from pkg/registry/apis/provisioning/repository/local.go rename to pkg/registry/apis/provisioning/repository/local/local.go index fc7bd18c99f..5f63b83cfa1 100644 --- a/pkg/registry/apis/provisioning/repository/local.go +++ b/pkg/registry/apis/provisioning/repository/local/local.go @@ -1,4 +1,4 @@ -package repository +package local import ( "context" @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) @@ -75,9 +76,9 @@ func (r *LocalFolderResolver) LocalPath(p string) (string, error) { } var ( - _ Repository = (*localRepository)(nil) - _ Writer = (*localRepository)(nil) - _ Reader = (*localRepository)(nil) + _ repository.Repository = (*localRepository)(nil) + _ repository.Writer = (*localRepository)(nil) + _ repository.Reader = (*localRepository)(nil) ) type localRepository struct { @@ -147,17 +148,17 @@ func (r *localRepository) Validate() field.ErrorList { func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { path := field.NewPath("spec", "local", "path") if r.config.Spec.Local.Path == "" { - return fromFieldError(field.Required(path, "no path is configured")), nil + return repository.FromFieldError(field.Required(path, "no path is configured")), nil } _, err := r.resolver.LocalPath(r.config.Spec.Local.Path) if err != nil { - return fromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil + return repository.FromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil } _, err = os.Stat(r.path) if errors.Is(err, os.ErrNotExist) { - return fromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil + return repository.FromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil } return &provisioning.TestResults{ @@ -176,7 +177,7 @@ func (r *localRepository) validateRequest(ref string) error { } // ReadResource implements provisioning.Repository. -func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*FileInfo, error) { +func (r *localRepository) Read(ctx context.Context, filePath string, ref string) (*repository.FileInfo, error) { if err := r.validateRequest(ref); err != nil { return nil, err } @@ -184,13 +185,13 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string) actualPath := safepath.Join(r.path, filePath) info, err := os.Stat(actualPath) if errors.Is(err, os.ErrNotExist) { - return nil, ErrFileNotFound + return nil, repository.ErrFileNotFound } else if err != nil { return nil, fmt.Errorf("stat file: %w", err) } if info.IsDir() { - return &FileInfo{ + return &repository.FileInfo{ Path: filePath, Modified: &metav1.Time{ Time: info.ModTime(), @@ -209,7 +210,7 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string) return nil, fmt.Errorf("calculate hash of file: %w", err) } - return &FileInfo{ + return &repository.FileInfo{ Path: filePath, Data: data, Hash: hash, @@ -220,7 +221,7 @@ func (r *localRepository) Read(ctx context.Context, filePath string, ref string) } // ReadResource implements provisioning.Repository. -func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { if err := r.validateRequest(ref); err != nil { return nil, err } @@ -228,16 +229,16 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE // Return an empty list when folder does not exist _, err := os.Stat(r.path) if errors.Is(err, fs.ErrNotExist) { - return []FileTreeEntry{}, nil + return []repository.FileTreeEntry{}, nil } rootlen := len(r.path) - entries := make([]FileTreeEntry, 0, 100) + entries := make([]repository.FileTreeEntry, 0, 100) err = filepath.Walk(r.path, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } - entry := FileTreeEntry{ + entry := repository.FileTreeEntry{ Path: strings.TrimLeft(path[rootlen:], "/"), Size: info.Size(), } @@ -251,8 +252,10 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE if err != nil { return fmt.Errorf("read and calculate hash of path %s: %w", path, err) } + } else if !strings.HasSuffix(entry.Path, "/") { + // ensure trailing slash for directories + entry.Path = entry.Path + "/" } - // TODO: do folders have a trailing slash? entries = append(entries, entry) return err }) @@ -263,7 +266,7 @@ func (r *localRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeE func (r *localRepository) calculateFileHash(path string) (string, int64, error) { // Treats https://securego.io/docs/rules/g304.html if !safepath.InDir(path, r.path) { - return "", 0, ErrFileNotFound + return "", 0, repository.ErrFileNotFound } // We've already made sure the path is safe, so we'll ignore the gosec lint. @@ -329,7 +332,7 @@ func (r *localRepository) Update(ctx context.Context, path string, ref string, d f, err := os.Stat(path) if err != nil && errors.Is(err, os.ErrNotExist) { - return ErrFileNotFound + return repository.ErrFileNotFound } if f.IsDir() { return apierrors.NewBadRequest("path exists but it is a directory") @@ -360,5 +363,12 @@ func (r *localRepository) Delete(ctx context.Context, path string, ref string, c return err } - return os.Remove(safepath.Join(r.path, path)) + fullPath := safepath.Join(r.path, path) + + if safepath.IsDir(path) { + // if it is a folder, delete all of its contents + return os.RemoveAll(fullPath) + } + + return os.Remove(fullPath) } diff --git a/pkg/registry/apis/provisioning/repository/local_test.go b/pkg/registry/apis/provisioning/repository/local/local_test.go similarity index 96% rename from pkg/registry/apis/provisioning/repository/local_test.go rename to pkg/registry/apis/provisioning/repository/local/local_test.go index 7aabc542402..7895da24995 100644 --- a/pkg/registry/apis/provisioning/repository/local_test.go +++ b/pkg/registry/apis/provisioning/repository/local/local_test.go @@ -1,4 +1,4 @@ -package repository +package local import ( "context" @@ -19,6 +19,7 @@ import ( field "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" ) func TestLocalResolver(t *testing.T) { @@ -91,14 +92,14 @@ func TestLocalResolver(t *testing.T) { // Verify all directories and files are present expectedPaths := []string{ - "another", - "another/path", + "another/", + "another/path/", "another/path/file.txt", - "level1", + "level1/", "level1/file1.txt", - "level1/level2", + "level1/level2/", "level1/level2/file2.txt", - "level1/level2/level3", + "level1/level2/level3/", "level1/level2/level3/file3.txt", "root.txt", } @@ -542,6 +543,41 @@ func TestLocalRepository_Delete(t *testing.T) { comment: "test delete with ref", expectedErr: apierrors.NewBadRequest("local repository does not support ref"), }, + { + name: "delete folder with nested files", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + nestedFolderPath := filepath.Join(tempDir, "folder") + err := os.MkdirAll(nestedFolderPath, 0700) + require.NoError(t, err) + subFolderPath := filepath.Join(nestedFolderPath, "nested-folder") + err = os.MkdirAll(subFolderPath, 0700) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(nestedFolderPath, "nested-dash.txt"), []byte("content1"), 0600) + require.NoError(t, err) + + // Create repository with the temp directory as permitted prefix + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + path: "folder/", + ref: "", + comment: "test delete folder with nested content", + expectedErr: nil, + }, } for _, tc := range testCases { @@ -668,7 +704,7 @@ func TestLocalRepository_Update(t *testing.T) { ref: "", data: []byte("content"), comment: "", - expectedErr: ErrFileNotFound, + expectedErr: repository.ErrFileNotFound, }, { name: "update directory", @@ -1139,7 +1175,7 @@ func TestLocalRepository_Read(t *testing.T) { path string ref string expectedErr error - expected *FileInfo + expected *repository.FileInfo }{ { name: "read existing file", @@ -1168,7 +1204,7 @@ func TestLocalRepository_Read(t *testing.T) { return tempDir, repo }, path: "test-file.txt", - expected: &FileInfo{ + expected: &repository.FileInfo{ Path: "test-file.txt", Modified: &metav1.Time{Time: time.Now()}, Data: []byte("test content"), @@ -1196,7 +1232,7 @@ func TestLocalRepository_Read(t *testing.T) { return tempDir, repo }, path: "non-existent-file.txt", - expectedErr: ErrFileNotFound, + expectedErr: repository.ErrFileNotFound, }, { name: "read with ref should fail", @@ -1254,7 +1290,7 @@ func TestLocalRepository_Read(t *testing.T) { return tempDir, repo }, path: "test-dir", - expected: &FileInfo{ + expected: &repository.FileInfo{ Path: "test-dir", Modified: &metav1.Time{Time: time.Now()}, }, @@ -1292,7 +1328,7 @@ func TestLocalRepository_ReadTree(t *testing.T) { setup func(t *testing.T) (string, *localRepository) ref string expectedErr error - expected []FileTreeEntry + expected []repository.FileTreeEntry }{ { name: "read empty directory", @@ -1314,7 +1350,7 @@ func TestLocalRepository_ReadTree(t *testing.T) { return tempDir, repo }, - expected: []FileTreeEntry{}, + expected: []repository.FileTreeEntry{}, expectedErr: nil, }, { @@ -1344,10 +1380,10 @@ func TestLocalRepository_ReadTree(t *testing.T) { return tempDir, repo }, - expected: []FileTreeEntry{ + expected: []repository.FileTreeEntry{ {Path: "file1.txt", Blob: true, Size: 8}, {Path: "file2.txt", Blob: true, Size: 8}, - {Path: "subdir", Blob: false}, + {Path: "subdir/", Blob: false}, {Path: "subdir/file3.txt", Blob: true, Size: 8}, }, expectedErr: nil, @@ -1397,7 +1433,7 @@ func TestLocalRepository_ReadTree(t *testing.T) { return tempDir, repo }, - expected: []FileTreeEntry{}, + expected: []repository.FileTreeEntry{}, expectedErr: nil, }, } diff --git a/pkg/registry/apis/provisioning/repository/nanogit/github.go b/pkg/registry/apis/provisioning/repository/nanogit/github.go deleted file mode 100644 index 1305a79a014..00000000000 --- a/pkg/registry/apis/provisioning/repository/nanogit/github.go +++ /dev/null @@ -1,125 +0,0 @@ -package nanogit - -import ( - "context" - "strings" - - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "k8s.io/apimachinery/pkg/util/validation/field" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" -) - -// githubRepository is a repository implementation that integrates both a GitHub API-backed repository and a nanogit-based repository. -// It combines the features of the GitHub API with those of a standard Git repository. -// This is an interim solution to support both backends within a single repository abstraction. -// Once nanogit is fully integrated, functionality from GithubRepository should be migrated here, and this type should extend the nanogit.GitRepository interface. -type githubRepository struct { - apiRepo repository.GithubRepository - nanogitRepo repository.GitRepository -} - -func NewGithubRepository( - apiRepo repository.GithubRepository, - nanogitRepo repository.GitRepository, -) repository.GithubRepository { - return &githubRepository{ - apiRepo: apiRepo, - nanogitRepo: nanogitRepo, - } -} - -func (r *githubRepository) Config() *provisioning.Repository { - return r.nanogitRepo.Config() -} - -func (r *githubRepository) Owner() string { - return r.apiRepo.Owner() -} - -func (r *githubRepository) Repo() string { - return r.apiRepo.Repo() -} - -func (r *githubRepository) Client() pgh.Client { - return r.apiRepo.Client() -} - -// Validate extends the nanogit repo validation with github specific validation -func (r *githubRepository) Validate() (list field.ErrorList) { - cfg := r.nanogitRepo.Config() - gh := cfg.Spec.GitHub - if gh == nil { - list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) - return list - } - if gh.URL == "" { - list = append(list, field.Required(field.NewPath("spec", "github", "url"), "a github url is required")) - } else { - _, _, err := repository.ParseOwnerRepoGithub(gh.URL) - if err != nil { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, err.Error())) - } else if !strings.HasPrefix(gh.URL, "https://github.com/") { - list = append(list, field.Invalid(field.NewPath("spec", "github", "url"), gh.URL, "URL must start with https://github.com/")) - } - } - - if len(list) > 0 { - return list - } - - return r.nanogitRepo.Validate() -} - -// Test implements provisioning.Repository. -func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { - return r.apiRepo.Test(ctx) -} - -// ReadResource implements provisioning.Repository. -func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { - return r.nanogitRepo.Read(ctx, filePath, ref) -} - -func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - return r.nanogitRepo.ReadTree(ctx, ref) -} - -func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { - return r.nanogitRepo.Create(ctx, path, ref, data, comment) -} - -func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { - return r.nanogitRepo.Update(ctx, path, ref, data, comment) -} - -func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { - return r.nanogitRepo.Write(ctx, path, ref, data, message) -} - -func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { - return r.nanogitRepo.Delete(ctx, path, ref, comment) -} - -func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { - // Github API provides avatar URLs which nanogit does not, so we delegate to the github repo. - return r.apiRepo.History(ctx, path, ref) -} - -func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { - return r.nanogitRepo.LatestRef(ctx) -} - -func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) { - return r.nanogitRepo.CompareFiles(ctx, base, ref) -} - -// ResourceURLs implements RepositoryWithURLs. -func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.ResourceURLs, error) { - return r.apiRepo.ResourceURLs(ctx, file) -} - -func (r *githubRepository) Clone(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { - return r.nanogitRepo.Clone(ctx, opts) -} diff --git a/pkg/registry/apis/provisioning/repository/nanogit/github_test.go b/pkg/registry/apis/provisioning/repository/nanogit/github_test.go deleted file mode 100644 index ef878a27c5c..00000000000 --- a/pkg/registry/apis/provisioning/repository/nanogit/github_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package nanogit - -import ( - "context" - "testing" - - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -func TestGithubRepository(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - // Create a proper config for testing - expectedConfig := &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - } - - // Set up mock expectations for the methods that exist - gitRepo.EXPECT().Config().Return(expectedConfig) - apiRepo.EXPECT().Owner().Return("test") - apiRepo.EXPECT().Repo().Return("repo") - mockClient := pgh.NewMockClient(t) - apiRepo.EXPECT().Client().Return(mockClient) - - repo := NewGithubRepository(apiRepo, gitRepo) - - t.Run("delegates config to nanogit repo", func(t *testing.T) { - result := repo.Config() - require.Equal(t, expectedConfig, result) - }) - - t.Run("delegates owner to api repo", func(t *testing.T) { - result := repo.Owner() - require.Equal(t, "test", result) - }) - - t.Run("delegates repo to api repo", func(t *testing.T) { - result := repo.Repo() - require.Equal(t, "repo", result) - }) - - t.Run("delegates client to api repo", func(t *testing.T) { - result := repo.Client() - require.Equal(t, mockClient, result) - }) -} - -func TestGithubRepositoryDelegation(t *testing.T) { - ctx := context.Background() - - t.Run("delegates test to api repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedResult := &provisioning.TestResults{ - Code: 200, - Success: true, - } - - apiRepo.EXPECT().Test(ctx).Return(expectedResult, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.Test(ctx) - - require.NoError(t, err) - require.Equal(t, expectedResult, result) - }) - - t.Run("delegates read to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedFileInfo := &repository.FileInfo{ - Path: "test.yaml", - Data: []byte("test data"), - Ref: "main", - Hash: "abc123", - } - - gitRepo.EXPECT().Read(ctx, "test.yaml", "main").Return(expectedFileInfo, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.Read(ctx, "test.yaml", "main") - - require.NoError(t, err) - require.Equal(t, expectedFileInfo, result) - }) - - t.Run("delegates read tree to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedEntries := []repository.FileTreeEntry{ - {Path: "file1.yaml", Size: 100, Hash: "hash1", Blob: true}, - {Path: "dir/", Size: 0, Hash: "hash2", Blob: false}, - } - - gitRepo.EXPECT().ReadTree(ctx, "main").Return(expectedEntries, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.ReadTree(ctx, "main") - - require.NoError(t, err) - require.Equal(t, expectedEntries, result) - }) - - t.Run("delegates create to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - data := []byte("test content") - gitRepo.EXPECT().Create(ctx, "new-file.yaml", "main", data, "Create new file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file") - - require.NoError(t, err) - }) - - t.Run("delegates update to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - data := []byte("updated content") - gitRepo.EXPECT().Update(ctx, "existing-file.yaml", "main", data, "Update file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file") - - require.NoError(t, err) - }) - - t.Run("delegates write to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - data := []byte("file content") - gitRepo.EXPECT().Write(ctx, "file.yaml", "main", data, "Write file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Write(ctx, "file.yaml", "main", data, "Write file") - - require.NoError(t, err) - }) - - t.Run("delegates delete to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - gitRepo.EXPECT().Delete(ctx, "file.yaml", "main", "Delete file").Return(nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - err := repo.Delete(ctx, "file.yaml", "main", "Delete file") - - require.NoError(t, err) - }) - - t.Run("delegates history to api repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedHistory := []provisioning.HistoryItem{ - { - Ref: "commit1", - Message: "First commit", - Authors: []provisioning.Author{{Name: "Test User"}}, - }, - } - - apiRepo.EXPECT().History(ctx, "file.yaml", "main").Return(expectedHistory, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.History(ctx, "file.yaml", "main") - - require.NoError(t, err) - require.Equal(t, expectedHistory, result) - }) - - t.Run("delegates latest ref to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedRef := "abc123def456" - gitRepo.EXPECT().LatestRef(ctx).Return(expectedRef, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.LatestRef(ctx) - - require.NoError(t, err) - require.Equal(t, expectedRef, result) - }) - - t.Run("delegates compare files to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - expectedChanges := []repository.VersionedFileChange{ - { - Action: repository.FileActionCreated, - Path: "new-file.yaml", - Ref: "feature-branch", - }, - } - - gitRepo.EXPECT().CompareFiles(ctx, "main", "feature-branch").Return(expectedChanges, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.CompareFiles(ctx, "main", "feature-branch") - - require.NoError(t, err) - require.Equal(t, expectedChanges, result) - }) - - t.Run("delegates resource URLs to api repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - fileInfo := &repository.FileInfo{ - Path: "dashboard.json", - Ref: "main", - Hash: "hash123", - } - - expectedURLs := &provisioning.ResourceURLs{ - SourceURL: "https://github.com/test/repo/blob/main/dashboard.json", - RepositoryURL: "https://github.com/test/repo", - NewPullRequestURL: "https://github.com/test/repo/compare/main...feature", - } - - apiRepo.EXPECT().ResourceURLs(ctx, fileInfo).Return(expectedURLs, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.ResourceURLs(ctx, fileInfo) - - require.NoError(t, err) - require.Equal(t, expectedURLs, result) - }) - - t.Run("delegates clone to nanogit repo", func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - mockClonedRepo := repository.NewMockClonedRepository(t) - - opts := repository.CloneOptions{ - CreateIfNotExists: true, - PushOnWrites: true, - } - - gitRepo.EXPECT().Clone(ctx, opts).Return(mockClonedRepo, nil) - - repo := NewGithubRepository(apiRepo, gitRepo) - result, err := repo.Clone(ctx, opts) - - require.NoError(t, err) - require.Equal(t, mockClonedRepo, result) - }) -} - -func TestGithubRepositoryValidation(t *testing.T) { - tests := []struct { - name string - config *provisioning.Repository - expectedErrors int - }{ - { - name: "missing github config", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - }, - }, - expectedErrors: 1, - }, - { - name: "missing github url", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - Branch: "main", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "invalid github url", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "invalid-url", - Branch: "main", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "non-github url", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://gitlab.com/test/repo", - Branch: "main", - }, - }, - }, - expectedErrors: 1, - }, - { - name: "valid github config", - config: &provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Type: provisioning.GitHubRepositoryType, - GitHub: &provisioning.GitHubRepositoryConfig{ - URL: "https://github.com/test/repo", - Branch: "main", - }, - }, - }, - expectedErrors: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - apiRepo := repository.NewMockGithubRepository(t) - gitRepo := repository.NewMockGitRepository(t) - - // Set up mock expectations - gitRepo.EXPECT().Config().Return(tt.config) - if tt.expectedErrors == 0 { - // If no validation errors expected, nanogit validation should be called - gitRepo.EXPECT().Validate().Return(field.ErrorList{}) - } - - repo := NewGithubRepository(apiRepo, gitRepo) - - result := repo.Validate() - require.Len(t, result, tt.expectedErrors) - }) - } -} diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index fcd40d5ba80..0e72dabbf43 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -2,9 +2,7 @@ package repository import ( "context" - "io" "net/http" - "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -67,47 +65,6 @@ type FileInfo struct { Modified *metav1.Time } -//go:generate mockery --name CloneFn --structname MockCloneFn --inpackage --filename clone_fn_mock.go --with-expecter -type CloneFn func(ctx context.Context, opts CloneOptions) (ClonedRepository, error) - -type CloneOptions struct { - // If the branch does not exist, create it - CreateIfNotExists bool - - // Push on every write - PushOnWrites bool - - // Maximum allowed size for repository clone in bytes (0 means no limit) - MaxSize int64 - - // Maximum time allowed for clone operation in seconds (0 means no limit) - Timeout time.Duration - - // Progress is the writer to report progress to - Progress io.Writer - - // BeforeFn is called before the clone operation starts - BeforeFn func() error -} - -//go:generate mockery --name ClonableRepository --structname MockClonableRepository --inpackage --filename clonable_repository_mock.go --with-expecter -type ClonableRepository interface { - Clone(ctx context.Context, opts CloneOptions) (ClonedRepository, error) -} - -type PushOptions struct { - Timeout time.Duration - Progress io.Writer - BeforeFn func() error -} - -//go:generate mockery --name ClonedRepository --structname MockClonedRepository --inpackage --filename cloned_repository_mock.go --with-expecter -type ClonedRepository interface { - ReaderWriter - Push(ctx context.Context, opts PushOptions) error - Remove(ctx context.Context) error -} - // An entry in the file tree, as returned by 'ReadFileTree'. Like FileInfo, but contains less information. type FileTreeEntry struct { // The path to the file from the base path given (if any). @@ -210,5 +167,6 @@ type Versioned interface { // History of changes for a path History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) LatestRef(ctx context.Context) (string, error) + ListRefs(ctx context.Context) ([]provisioning.RefItem, error) CompareFiles(ctx context.Context, base, ref string) ([]VersionedFileChange, error) } diff --git a/pkg/registry/apis/provisioning/repository/stageable_repository_mock.go b/pkg/registry/apis/provisioning/repository/stageable_repository_mock.go new file mode 100644 index 00000000000..e1f7d619823 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/stageable_repository_mock.go @@ -0,0 +1,95 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package repository + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockStageableRepository is an autogenerated mock type for the StageableRepository type +type MockStageableRepository struct { + mock.Mock +} + +type MockStageableRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *MockStageableRepository) EXPECT() *MockStageableRepository_Expecter { + return &MockStageableRepository_Expecter{mock: &_m.Mock} +} + +// Stage provides a mock function with given fields: ctx, opts +func (_m *MockStageableRepository) Stage(ctx context.Context, opts StageOptions) (StagedRepository, error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Stage") + } + + var r0 StagedRepository + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, StageOptions) (StagedRepository, error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, StageOptions) StagedRepository); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(StagedRepository) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, StageOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStageableRepository_Stage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stage' +type MockStageableRepository_Stage_Call struct { + *mock.Call +} + +// Stage is a helper method to define mock.On call +// - ctx context.Context +// - opts StageOptions +func (_e *MockStageableRepository_Expecter) Stage(ctx interface{}, opts interface{}) *MockStageableRepository_Stage_Call { + return &MockStageableRepository_Stage_Call{Call: _e.mock.On("Stage", ctx, opts)} +} + +func (_c *MockStageableRepository_Stage_Call) Run(run func(ctx context.Context, opts StageOptions)) *MockStageableRepository_Stage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(StageOptions)) + }) + return _c +} + +func (_c *MockStageableRepository_Stage_Call) Return(_a0 StagedRepository, _a1 error) *MockStageableRepository_Stage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStageableRepository_Stage_Call) RunAndReturn(run func(context.Context, StageOptions) (StagedRepository, error)) *MockStageableRepository_Stage_Call { + _c.Call.Return(run) + return _c +} + +// NewMockStageableRepository creates a new instance of MockStageableRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockStageableRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockStageableRepository { + mock := &MockStageableRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/repository/staged.go b/pkg/registry/apis/provisioning/repository/staged.go new file mode 100644 index 00000000000..6eb0aed357a --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/staged.go @@ -0,0 +1,71 @@ +package repository + +import ( + context "context" + "errors" + "fmt" + "time" + + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/nanogit" +) + +type StageOptions struct { + // Push on every write + PushOnWrites bool + // Maximum time allowed for clone operation in seconds (0 means no limit) + Timeout time.Duration +} + +//go:generate mockery --name StageableRepository --structname MockStageableRepository --inpackage --filename stageable_repository_mock.go --with-expecter +type StageableRepository interface { + Stage(ctx context.Context, opts StageOptions) (StagedRepository, error) +} + +//go:generate mockery --name StagedRepository --structname MockStagedRepository --inpackage --filename staged_repository_mock.go --with-expecter +type StagedRepository interface { + ReaderWriter + Push(ctx context.Context) error + Remove(ctx context.Context) error +} + +// WrapWithStageAndPushIfPossible attempts to stage the given repository. If staging is supported, +// it runs the provided function on the staged repository, then pushes any changes and cleans up the staged repository. +// If staging is not supported, it runs the function on the original repository without pushing. +// The 'staged' argument to the function indicates whether a staged repository was used. +func WrapWithStageAndPushIfPossible( + ctx context.Context, + repo Repository, + stageOptions StageOptions, + fn func(repo Repository, staged bool) error, +) error { + stageable, ok := repo.(StageableRepository) + if !ok { + return fn(repo, false) + } + + staged, err := stageable.Stage(ctx, stageOptions) + if err != nil { + return fmt.Errorf("stage repository: %w", err) + } + + // We don't, we simply log it + // FIXME: should we handle this differently? + defer func() { + if err := staged.Remove(ctx); err != nil { + logging.FromContext(ctx).Error("failed to remove staged repository after export", "err", err) + } + }() + + if err := fn(staged, true); err != nil { + return err + } + + if err = staged.Push(ctx); err != nil { + if errors.Is(err, nanogit.ErrNothingToPush) { + return nil // OK, already pushed + } + return fmt.Errorf("wrapped push error: %w", err) + } + return nil +} diff --git a/pkg/registry/apis/provisioning/repository/cloned_repository_mock.go b/pkg/registry/apis/provisioning/repository/staged_repository_mock.go similarity index 55% rename from pkg/registry/apis/provisioning/repository/cloned_repository_mock.go rename to pkg/registry/apis/provisioning/repository/staged_repository_mock.go index 7dc46ddeb77..916b6cc678e 100644 --- a/pkg/registry/apis/provisioning/repository/cloned_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/staged_repository_mock.go @@ -11,21 +11,21 @@ import ( v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) -// MockClonedRepository is an autogenerated mock type for the ClonedRepository type -type MockClonedRepository struct { +// MockStagedRepository is an autogenerated mock type for the StagedRepository type +type MockStagedRepository struct { mock.Mock } -type MockClonedRepository_Expecter struct { +type MockStagedRepository_Expecter struct { mock *mock.Mock } -func (_m *MockClonedRepository) EXPECT() *MockClonedRepository_Expecter { - return &MockClonedRepository_Expecter{mock: &_m.Mock} +func (_m *MockStagedRepository) EXPECT() *MockStagedRepository_Expecter { + return &MockStagedRepository_Expecter{mock: &_m.Mock} } // Config provides a mock function with no fields -func (_m *MockClonedRepository) Config() *v0alpha1.Repository { +func (_m *MockStagedRepository) Config() *v0alpha1.Repository { ret := _m.Called() if len(ret) == 0 { @@ -44,35 +44,35 @@ func (_m *MockClonedRepository) Config() *v0alpha1.Repository { return r0 } -// MockClonedRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' -type MockClonedRepository_Config_Call struct { +// MockStagedRepository_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' +type MockStagedRepository_Config_Call struct { *mock.Call } // Config is a helper method to define mock.On call -func (_e *MockClonedRepository_Expecter) Config() *MockClonedRepository_Config_Call { - return &MockClonedRepository_Config_Call{Call: _e.mock.On("Config")} +func (_e *MockStagedRepository_Expecter) Config() *MockStagedRepository_Config_Call { + return &MockStagedRepository_Config_Call{Call: _e.mock.On("Config")} } -func (_c *MockClonedRepository_Config_Call) Run(run func()) *MockClonedRepository_Config_Call { +func (_c *MockStagedRepository_Config_Call) Run(run func()) *MockStagedRepository_Config_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockClonedRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockClonedRepository_Config_Call { +func (_c *MockStagedRepository_Config_Call) Return(_a0 *v0alpha1.Repository) *MockStagedRepository_Config_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockClonedRepository_Config_Call { +func (_c *MockStagedRepository_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockStagedRepository_Config_Call { _c.Call.Return(run) return _c } // Create provides a mock function with given fields: ctx, path, ref, data, message -func (_m *MockClonedRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error { +func (_m *MockStagedRepository) Create(ctx context.Context, path string, ref string, data []byte, message string) error { ret := _m.Called(ctx, path, ref, data, message) if len(ret) == 0 { @@ -89,8 +89,8 @@ func (_m *MockClonedRepository) Create(ctx context.Context, path string, ref str return r0 } -// MockClonedRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type MockClonedRepository_Create_Call struct { +// MockStagedRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockStagedRepository_Create_Call struct { *mock.Call } @@ -100,29 +100,29 @@ type MockClonedRepository_Create_Call struct { // - ref string // - data []byte // - message string -func (_e *MockClonedRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Create_Call { - return &MockClonedRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)} +func (_e *MockStagedRepository_Expecter) Create(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Create_Call { + return &MockStagedRepository_Create_Call{Call: _e.mock.On("Create", ctx, path, ref, data, message)} } -func (_c *MockClonedRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Create_Call { +func (_c *MockStagedRepository_Create_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_Create_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string)) }) return _c } -func (_c *MockClonedRepository_Create_Call) Return(_a0 error) *MockClonedRepository_Create_Call { +func (_c *MockStagedRepository_Create_Call) Return(_a0 error) *MockStagedRepository_Create_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Create_Call { +func (_c *MockStagedRepository_Create_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Create_Call { _c.Call.Return(run) return _c } // Delete provides a mock function with given fields: ctx, path, ref, message -func (_m *MockClonedRepository) Delete(ctx context.Context, path string, ref string, message string) error { +func (_m *MockStagedRepository) Delete(ctx context.Context, path string, ref string, message string) error { ret := _m.Called(ctx, path, ref, message) if len(ret) == 0 { @@ -139,8 +139,8 @@ func (_m *MockClonedRepository) Delete(ctx context.Context, path string, ref str return r0 } -// MockClonedRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type MockClonedRepository_Delete_Call struct { +// MockStagedRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockStagedRepository_Delete_Call struct { *mock.Call } @@ -149,38 +149,38 @@ type MockClonedRepository_Delete_Call struct { // - path string // - ref string // - message string -func (_e *MockClonedRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockClonedRepository_Delete_Call { - return &MockClonedRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)} +func (_e *MockStagedRepository_Expecter) Delete(ctx interface{}, path interface{}, ref interface{}, message interface{}) *MockStagedRepository_Delete_Call { + return &MockStagedRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, path, ref, message)} } -func (_c *MockClonedRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockClonedRepository_Delete_Call { +func (_c *MockStagedRepository_Delete_Call) Run(run func(ctx context.Context, path string, ref string, message string)) *MockStagedRepository_Delete_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) }) return _c } -func (_c *MockClonedRepository_Delete_Call) Return(_a0 error) *MockClonedRepository_Delete_Call { +func (_c *MockStagedRepository_Delete_Call) Return(_a0 error) *MockStagedRepository_Delete_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockClonedRepository_Delete_Call { +func (_c *MockStagedRepository_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockStagedRepository_Delete_Call { _c.Call.Return(run) return _c } -// Push provides a mock function with given fields: ctx, opts -func (_m *MockClonedRepository) Push(ctx context.Context, opts PushOptions) error { - ret := _m.Called(ctx, opts) +// Push provides a mock function with given fields: ctx +func (_m *MockStagedRepository) Push(ctx context.Context) error { + ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for Push") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, PushOptions) error); ok { - r0 = rf(ctx, opts) + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) } else { r0 = ret.Error(0) } @@ -188,37 +188,36 @@ func (_m *MockClonedRepository) Push(ctx context.Context, opts PushOptions) erro return r0 } -// MockClonedRepository_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' -type MockClonedRepository_Push_Call struct { +// MockStagedRepository_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' +type MockStagedRepository_Push_Call struct { *mock.Call } // Push is a helper method to define mock.On call // - ctx context.Context -// - opts PushOptions -func (_e *MockClonedRepository_Expecter) Push(ctx interface{}, opts interface{}) *MockClonedRepository_Push_Call { - return &MockClonedRepository_Push_Call{Call: _e.mock.On("Push", ctx, opts)} +func (_e *MockStagedRepository_Expecter) Push(ctx interface{}) *MockStagedRepository_Push_Call { + return &MockStagedRepository_Push_Call{Call: _e.mock.On("Push", ctx)} } -func (_c *MockClonedRepository_Push_Call) Run(run func(ctx context.Context, opts PushOptions)) *MockClonedRepository_Push_Call { +func (_c *MockStagedRepository_Push_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Push_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(PushOptions)) + run(args[0].(context.Context)) }) return _c } -func (_c *MockClonedRepository_Push_Call) Return(_a0 error) *MockClonedRepository_Push_Call { +func (_c *MockStagedRepository_Push_Call) Return(_a0 error) *MockStagedRepository_Push_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Push_Call) RunAndReturn(run func(context.Context, PushOptions) error) *MockClonedRepository_Push_Call { +func (_c *MockStagedRepository_Push_Call) RunAndReturn(run func(context.Context) error) *MockStagedRepository_Push_Call { _c.Call.Return(run) return _c } // Read provides a mock function with given fields: ctx, path, ref -func (_m *MockClonedRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { +func (_m *MockStagedRepository) Read(ctx context.Context, path string, ref string) (*FileInfo, error) { ret := _m.Called(ctx, path, ref) if len(ret) == 0 { @@ -247,8 +246,8 @@ func (_m *MockClonedRepository) Read(ctx context.Context, path string, ref strin return r0, r1 } -// MockClonedRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' -type MockClonedRepository_Read_Call struct { +// MockStagedRepository_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockStagedRepository_Read_Call struct { *mock.Call } @@ -256,29 +255,29 @@ type MockClonedRepository_Read_Call struct { // - ctx context.Context // - path string // - ref string -func (_e *MockClonedRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockClonedRepository_Read_Call { - return &MockClonedRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)} +func (_e *MockStagedRepository_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockStagedRepository_Read_Call { + return &MockStagedRepository_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)} } -func (_c *MockClonedRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockClonedRepository_Read_Call { +func (_c *MockStagedRepository_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockStagedRepository_Read_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string)) }) return _c } -func (_c *MockClonedRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockClonedRepository_Read_Call { +func (_c *MockStagedRepository_Read_Call) Return(_a0 *FileInfo, _a1 error) *MockStagedRepository_Read_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClonedRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockClonedRepository_Read_Call { +func (_c *MockStagedRepository_Read_Call) RunAndReturn(run func(context.Context, string, string) (*FileInfo, error)) *MockStagedRepository_Read_Call { _c.Call.Return(run) return _c } // ReadTree provides a mock function with given fields: ctx, ref -func (_m *MockClonedRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { +func (_m *MockStagedRepository) ReadTree(ctx context.Context, ref string) ([]FileTreeEntry, error) { ret := _m.Called(ctx, ref) if len(ret) == 0 { @@ -307,37 +306,37 @@ func (_m *MockClonedRepository) ReadTree(ctx context.Context, ref string) ([]Fil return r0, r1 } -// MockClonedRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree' -type MockClonedRepository_ReadTree_Call struct { +// MockStagedRepository_ReadTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadTree' +type MockStagedRepository_ReadTree_Call struct { *mock.Call } // ReadTree is a helper method to define mock.On call // - ctx context.Context // - ref string -func (_e *MockClonedRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockClonedRepository_ReadTree_Call { - return &MockClonedRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)} +func (_e *MockStagedRepository_Expecter) ReadTree(ctx interface{}, ref interface{}) *MockStagedRepository_ReadTree_Call { + return &MockStagedRepository_ReadTree_Call{Call: _e.mock.On("ReadTree", ctx, ref)} } -func (_c *MockClonedRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockClonedRepository_ReadTree_Call { +func (_c *MockStagedRepository_ReadTree_Call) Run(run func(ctx context.Context, ref string)) *MockStagedRepository_ReadTree_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) return _c } -func (_c *MockClonedRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockClonedRepository_ReadTree_Call { +func (_c *MockStagedRepository_ReadTree_Call) Return(_a0 []FileTreeEntry, _a1 error) *MockStagedRepository_ReadTree_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClonedRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockClonedRepository_ReadTree_Call { +func (_c *MockStagedRepository_ReadTree_Call) RunAndReturn(run func(context.Context, string) ([]FileTreeEntry, error)) *MockStagedRepository_ReadTree_Call { _c.Call.Return(run) return _c } // Remove provides a mock function with given fields: ctx -func (_m *MockClonedRepository) Remove(ctx context.Context) error { +func (_m *MockStagedRepository) Remove(ctx context.Context) error { ret := _m.Called(ctx) if len(ret) == 0 { @@ -354,36 +353,36 @@ func (_m *MockClonedRepository) Remove(ctx context.Context) error { return r0 } -// MockClonedRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' -type MockClonedRepository_Remove_Call struct { +// MockStagedRepository_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' +type MockStagedRepository_Remove_Call struct { *mock.Call } // Remove is a helper method to define mock.On call // - ctx context.Context -func (_e *MockClonedRepository_Expecter) Remove(ctx interface{}) *MockClonedRepository_Remove_Call { - return &MockClonedRepository_Remove_Call{Call: _e.mock.On("Remove", ctx)} +func (_e *MockStagedRepository_Expecter) Remove(ctx interface{}) *MockStagedRepository_Remove_Call { + return &MockStagedRepository_Remove_Call{Call: _e.mock.On("Remove", ctx)} } -func (_c *MockClonedRepository_Remove_Call) Run(run func(ctx context.Context)) *MockClonedRepository_Remove_Call { +func (_c *MockStagedRepository_Remove_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Remove_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *MockClonedRepository_Remove_Call) Return(_a0 error) *MockClonedRepository_Remove_Call { +func (_c *MockStagedRepository_Remove_Call) Return(_a0 error) *MockStagedRepository_Remove_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Remove_Call) RunAndReturn(run func(context.Context) error) *MockClonedRepository_Remove_Call { +func (_c *MockStagedRepository_Remove_Call) RunAndReturn(run func(context.Context) error) *MockStagedRepository_Remove_Call { _c.Call.Return(run) return _c } // Test provides a mock function with given fields: ctx -func (_m *MockClonedRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) { +func (_m *MockStagedRepository) Test(ctx context.Context) (*v0alpha1.TestResults, error) { ret := _m.Called(ctx) if len(ret) == 0 { @@ -412,36 +411,36 @@ func (_m *MockClonedRepository) Test(ctx context.Context) (*v0alpha1.TestResults return r0, r1 } -// MockClonedRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test' -type MockClonedRepository_Test_Call struct { +// MockStagedRepository_Test_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Test' +type MockStagedRepository_Test_Call struct { *mock.Call } // Test is a helper method to define mock.On call // - ctx context.Context -func (_e *MockClonedRepository_Expecter) Test(ctx interface{}) *MockClonedRepository_Test_Call { - return &MockClonedRepository_Test_Call{Call: _e.mock.On("Test", ctx)} +func (_e *MockStagedRepository_Expecter) Test(ctx interface{}) *MockStagedRepository_Test_Call { + return &MockStagedRepository_Test_Call{Call: _e.mock.On("Test", ctx)} } -func (_c *MockClonedRepository_Test_Call) Run(run func(ctx context.Context)) *MockClonedRepository_Test_Call { +func (_c *MockStagedRepository_Test_Call) Run(run func(ctx context.Context)) *MockStagedRepository_Test_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) return _c } -func (_c *MockClonedRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockClonedRepository_Test_Call { +func (_c *MockStagedRepository_Test_Call) Return(_a0 *v0alpha1.TestResults, _a1 error) *MockStagedRepository_Test_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClonedRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockClonedRepository_Test_Call { +func (_c *MockStagedRepository_Test_Call) RunAndReturn(run func(context.Context) (*v0alpha1.TestResults, error)) *MockStagedRepository_Test_Call { _c.Call.Return(run) return _c } // Update provides a mock function with given fields: ctx, path, ref, data, message -func (_m *MockClonedRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error { +func (_m *MockStagedRepository) Update(ctx context.Context, path string, ref string, data []byte, message string) error { ret := _m.Called(ctx, path, ref, data, message) if len(ret) == 0 { @@ -458,8 +457,8 @@ func (_m *MockClonedRepository) Update(ctx context.Context, path string, ref str return r0 } -// MockClonedRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type MockClonedRepository_Update_Call struct { +// MockStagedRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockStagedRepository_Update_Call struct { *mock.Call } @@ -469,29 +468,29 @@ type MockClonedRepository_Update_Call struct { // - ref string // - data []byte // - message string -func (_e *MockClonedRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Update_Call { - return &MockClonedRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)} +func (_e *MockStagedRepository_Expecter) Update(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Update_Call { + return &MockStagedRepository_Update_Call{Call: _e.mock.On("Update", ctx, path, ref, data, message)} } -func (_c *MockClonedRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Update_Call { +func (_c *MockStagedRepository_Update_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_Update_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string)) }) return _c } -func (_c *MockClonedRepository_Update_Call) Return(_a0 error) *MockClonedRepository_Update_Call { +func (_c *MockStagedRepository_Update_Call) Return(_a0 error) *MockStagedRepository_Update_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Update_Call { +func (_c *MockStagedRepository_Update_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Update_Call { _c.Call.Return(run) return _c } // Validate provides a mock function with no fields -func (_m *MockClonedRepository) Validate() field.ErrorList { +func (_m *MockStagedRepository) Validate() field.ErrorList { ret := _m.Called() if len(ret) == 0 { @@ -510,35 +509,35 @@ func (_m *MockClonedRepository) Validate() field.ErrorList { return r0 } -// MockClonedRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' -type MockClonedRepository_Validate_Call struct { +// MockStagedRepository_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' +type MockStagedRepository_Validate_Call struct { *mock.Call } // Validate is a helper method to define mock.On call -func (_e *MockClonedRepository_Expecter) Validate() *MockClonedRepository_Validate_Call { - return &MockClonedRepository_Validate_Call{Call: _e.mock.On("Validate")} +func (_e *MockStagedRepository_Expecter) Validate() *MockStagedRepository_Validate_Call { + return &MockStagedRepository_Validate_Call{Call: _e.mock.On("Validate")} } -func (_c *MockClonedRepository_Validate_Call) Run(run func()) *MockClonedRepository_Validate_Call { +func (_c *MockStagedRepository_Validate_Call) Run(run func()) *MockStagedRepository_Validate_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockClonedRepository_Validate_Call) Return(_a0 field.ErrorList) *MockClonedRepository_Validate_Call { +func (_c *MockStagedRepository_Validate_Call) Return(_a0 field.ErrorList) *MockStagedRepository_Validate_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockClonedRepository_Validate_Call { +func (_c *MockStagedRepository_Validate_Call) RunAndReturn(run func() field.ErrorList) *MockStagedRepository_Validate_Call { _c.Call.Return(run) return _c } // Write provides a mock function with given fields: ctx, path, ref, data, message -func (_m *MockClonedRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { +func (_m *MockStagedRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { ret := _m.Called(ctx, path, ref, data, message) if len(ret) == 0 { @@ -555,8 +554,8 @@ func (_m *MockClonedRepository) Write(ctx context.Context, path string, ref stri return r0 } -// MockClonedRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' -type MockClonedRepository_Write_Call struct { +// MockStagedRepository_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type MockStagedRepository_Write_Call struct { *mock.Call } @@ -566,34 +565,34 @@ type MockClonedRepository_Write_Call struct { // - ref string // - data []byte // - message string -func (_e *MockClonedRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockClonedRepository_Write_Call { - return &MockClonedRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)} +func (_e *MockStagedRepository_Expecter) Write(ctx interface{}, path interface{}, ref interface{}, data interface{}, message interface{}) *MockStagedRepository_Write_Call { + return &MockStagedRepository_Write_Call{Call: _e.mock.On("Write", ctx, path, ref, data, message)} } -func (_c *MockClonedRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockClonedRepository_Write_Call { +func (_c *MockStagedRepository_Write_Call) Run(run func(ctx context.Context, path string, ref string, data []byte, message string)) *MockStagedRepository_Write_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].(string)) }) return _c } -func (_c *MockClonedRepository_Write_Call) Return(_a0 error) *MockClonedRepository_Write_Call { +func (_c *MockStagedRepository_Write_Call) Return(_a0 error) *MockStagedRepository_Write_Call { _c.Call.Return(_a0) return _c } -func (_c *MockClonedRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockClonedRepository_Write_Call { +func (_c *MockStagedRepository_Write_Call) RunAndReturn(run func(context.Context, string, string, []byte, string) error) *MockStagedRepository_Write_Call { _c.Call.Return(run) return _c } -// NewMockClonedRepository creates a new instance of MockClonedRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockStagedRepository creates a new instance of MockStagedRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockClonedRepository(t interface { +func NewMockStagedRepository(t interface { mock.TestingT Cleanup(func()) -}) *MockClonedRepository { - mock := &MockClonedRepository{} +}) *MockStagedRepository { + mock := &MockStagedRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/pkg/registry/apis/provisioning/repository/staged_test.go b/pkg/registry/apis/provisioning/repository/staged_test.go new file mode 100644 index 00000000000..3efe8ebbd5e --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/staged_test.go @@ -0,0 +1,144 @@ +package repository + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockStagedRepo struct { + *MockStageableRepository + *MockStagedRepository +} + +func Test_WrapWithStageAndPushIfPossible_NonStageableRepository(t *testing.T) { + nonStageable := NewMockRepository(t) + var called bool + fn := func(repo Repository, staged bool) error { + called = true + return errors.New("operation failed") + } + + err := WrapWithStageAndPushIfPossible(context.Background(), nonStageable, StageOptions{}, fn) + require.EqualError(t, err, "operation failed") + require.True(t, called) +} + +func TestWrapWithStageAndPushIfPossible(t *testing.T) { + tests := []struct { + name string + setupMocks func(t *testing.T) *mockStagedRepo + operation func(repo Repository, staged bool) error + expectedError string + }{ + { + name: "successful stage, operation, and push", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Push(mock.Anything).Return(nil) + mockStaged.EXPECT().Remove(mock.Anything).Return(nil) + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + require.True(t, staged) + return nil + }, + }, + { + name: "stage failure", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(nil, errors.New("stage failed")) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + } + }, + operation: func(repo Repository, staged bool) error { + return nil + }, + expectedError: "stage repository: stage failed", + }, + { + name: "operation failure", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Remove(mock.Anything).Return(nil) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + return errors.New("operation failed") + }, + expectedError: "operation failed", + }, + { + name: "push failure", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Push(mock.Anything).Return(errors.New("push failed")) + mockStaged.EXPECT().Remove(mock.Anything).Return(nil) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + return nil + }, + expectedError: "wrapped push error: push failed", + }, + { + name: "remove failure should only log", + setupMocks: func(t *testing.T) *mockStagedRepo { + mockRepo := NewMockStageableRepository(t) + mockStaged := NewMockStagedRepository(t) + + mockRepo.EXPECT().Stage(mock.Anything, StageOptions{}).Return(mockStaged, nil) + mockStaged.EXPECT().Push(mock.Anything).Return(nil) + mockStaged.EXPECT().Remove(mock.Anything).Return(errors.New("remove failed")) + + return &mockStagedRepo{ + MockStageableRepository: mockRepo, + MockStagedRepository: mockStaged, + } + }, + operation: func(repo Repository, staged bool) error { + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := tt.setupMocks(t) + err := WrapWithStageAndPushIfPossible(context.Background(), repo, StageOptions{}, tt.operation) + + if tt.expectedError != "" { + require.EqualError(t, err, tt.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/test.go b/pkg/registry/apis/provisioning/repository/test.go index 0e5778f76b3..f8b72d5b2af 100644 --- a/pkg/registry/apis/provisioning/repository/test.go +++ b/pkg/registry/apis/provisioning/repository/test.go @@ -96,7 +96,7 @@ func ValidateRepository(repo Repository) field.ErrorList { return list } -func fromFieldError(err *field.Error) *provisioning.TestResults { +func FromFieldError(err *field.Error) *provisioning.TestResults { return &provisioning.TestResults{ Code: http.StatusBadRequest, Success: false, diff --git a/pkg/registry/apis/provisioning/repository/test_test.go b/pkg/registry/apis/provisioning/repository/test_test.go index 3121bcc4110..9168d529ea0 100644 --- a/pkg/registry/apis/provisioning/repository/test_test.go +++ b/pkg/registry/apis/provisioning/repository/test_test.go @@ -431,7 +431,7 @@ func TestFromFieldError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := fromFieldError(tt.fieldError) + result := FromFieldError(tt.fieldError) require.NotNil(t, result) require.Equal(t, tt.expectedCode, result.Code) diff --git a/pkg/registry/apis/provisioning/repository/versioned_mock.go b/pkg/registry/apis/provisioning/repository/versioned_mock.go index cdd59b288e1..dc711f10f30 100644 --- a/pkg/registry/apis/provisioning/repository/versioned_mock.go +++ b/pkg/registry/apis/provisioning/repository/versioned_mock.go @@ -198,6 +198,64 @@ func (_c *MockVersioned_LatestRef_Call) RunAndReturn(run func(context.Context) ( return _c } +// ListRefs provides a mock function with given fields: ctx +func (_m *MockVersioned) ListRefs(ctx context.Context) ([]v0alpha1.RefItem, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListRefs") + } + + var r0 []v0alpha1.RefItem + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]v0alpha1.RefItem, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []v0alpha1.RefItem); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]v0alpha1.RefItem) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockVersioned_ListRefs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRefs' +type MockVersioned_ListRefs_Call struct { + *mock.Call +} + +// ListRefs is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockVersioned_Expecter) ListRefs(ctx interface{}) *MockVersioned_ListRefs_Call { + return &MockVersioned_ListRefs_Call{Call: _e.mock.On("ListRefs", ctx)} +} + +func (_c *MockVersioned_ListRefs_Call) Run(run func(ctx context.Context)) *MockVersioned_ListRefs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockVersioned_ListRefs_Call) Return(_a0 []v0alpha1.RefItem, _a1 error) *MockVersioned_ListRefs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockVersioned_ListRefs_Call) RunAndReturn(run func(context.Context) ([]v0alpha1.RefItem, error)) *MockVersioned_ListRefs_Call { + _c.Call.Return(run) + return _c +} + // NewMockVersioned creates a new instance of MockVersioned. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockVersioned(t interface { diff --git a/pkg/registry/apis/provisioning/resources/client_factory_mock.go b/pkg/registry/apis/provisioning/resources/client_factory_mock.go index b8aa174e04f..d118582d35c 100644 --- a/pkg/registry/apis/provisioning/resources/client_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/client_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/clients_mock.go b/pkg/registry/apis/provisioning/resources/clients_mock.go index d78496c2183..4af4559943a 100644 --- a/pkg/registry/apis/provisioning/resources/clients_mock.go +++ b/pkg/registry/apis/provisioning/resources/clients_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index 7d7b46e0406..2145285d52e 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -6,6 +6,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" @@ -76,9 +77,8 @@ func (r *DualReadWriter) Delete(ctx context.Context, opts DualWriteOptions) (*Pa return nil, err } - // TODO: implement this if safepath.IsDir(opts.Path) { - return nil, fmt.Errorf("folder delete not supported") + return r.deleteFolder(ctx, opts) } // Read the file from the default branch as it won't exist in the possibly new branch @@ -166,6 +166,12 @@ func (r *DualReadWriter) CreateFolder(ctx context.Context, opts DualWriteOptions }, } + urls, err := getFolderURLs(ctx, opts.Path, opts.Ref, r.repo) + if err != nil { + return nil, err + } + wrap.URLs = urls + if opts.Ref == "" { folderName, err := r.folders.EnsureFolderPathExist(ctx, opts.Path) if err != nil { @@ -317,3 +323,141 @@ func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) er return apierrors.NewForbidden(FolderResource.GroupResource(), "", fmt.Errorf("must be admin or editor to access folders with provisioning")) } + +func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { + // if the ref is set, it is not the active branch, so just delete the files from the branch + // and do not delete the items from grafana itself + if opts.Ref != "" { + err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) + if err != nil { + return nil, fmt.Errorf("error deleting folder from repository: %w", err) + } + + return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo) + } + + // before deleting from the repo, first get all children resources to delete from grafana afterwards + treeEntries, err := r.repo.ReadTree(ctx, "") + if err != nil { + return nil, fmt.Errorf("read repository tree: %w", err) + } + // note: parsedFolders will include the folder itself + parsedResources, parsedFolders, err := r.getChildren(ctx, opts.Path, treeEntries) + if err != nil { + return nil, fmt.Errorf("parse resources in folder: %w", err) + } + + // delete from the repo + err = r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message) + if err != nil { + return nil, fmt.Errorf("delete folder from repository: %w", err) + } + + // delete from grafana + ctx, _, err = identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace) + if err != nil { + return nil, err + } + if err := r.deleteChildren(ctx, parsedResources, parsedFolders); err != nil { + return nil, fmt.Errorf("delete folder from grafana: %w", err) + } + + return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo) +} + +func getFolderURLs(ctx context.Context, path, ref string, repo repository.Repository) (*provisioning.ResourceURLs, error) { + if urlRepo, ok := repo.(repository.RepositoryWithURLs); ok && ref != "" { + urls, err := urlRepo.ResourceURLs(ctx, &repository.FileInfo{Path: path, Ref: ref}) + if err != nil { + return nil, err + } + return urls, nil + } + return nil, nil +} + +func folderDeleteResponse(ctx context.Context, path, ref string, repo repository.Repository) (*ParsedResource, error) { + urls, err := getFolderURLs(ctx, path, ref, repo) + if err != nil { + return nil, err + } + + parsed := &ParsedResource{ + Action: provisioning.ResourceActionDelete, + Info: &repository.FileInfo{ + Path: path, + Ref: ref, + }, + GVK: schema.GroupVersionKind{ + Group: FolderResource.Group, + Version: FolderResource.Version, + Kind: "Folder", + }, + GVR: FolderResource, + Repo: provisioning.ResourceRepositoryInfo{ + Type: repo.Config().Spec.Type, + Namespace: repo.Config().Namespace, + Name: repo.Config().Name, + Title: repo.Config().Spec.Title, + }, + URLs: urls, + } + + return parsed, nil +} + +func (r *DualReadWriter) getChildren(ctx context.Context, folderPath string, treeEntries []repository.FileTreeEntry) ([]*ParsedResource, []Folder, error) { + var resourcesInFolder []repository.FileTreeEntry + var foldersInFolder []Folder + for _, entry := range treeEntries { + // make sure the path is supported (i.e. not ignored by git sync) and that the path is the folder itself or a child of the folder + if IsPathSupported(entry.Path) != nil || !safepath.InDir(entry.Path, folderPath) { + continue + } + // folders cannot be parsed as resources, so handle them separately + if entry.Blob { + resourcesInFolder = append(resourcesInFolder, entry) + } else { + folder := ParseFolder(entry.Path, r.repo.Config().Name) + foldersInFolder = append(foldersInFolder, folder) + } + } + + parsedResources := make([]*ParsedResource, len(resourcesInFolder)) + for i, entry := range resourcesInFolder { + fileInfo, err := r.repo.Read(ctx, entry.Path, "") + if err != nil && !apierrors.IsNotFound(err) { + return nil, nil, fmt.Errorf("could not find resource in repository: %w", err) + } + + parsed, err := r.parser.Parse(ctx, fileInfo) + if err != nil { + return nil, nil, fmt.Errorf("could not parse resource: %w", err) + } + + parsedResources[i] = parsed + } + + return parsedResources, foldersInFolder, nil +} + +func (r *DualReadWriter) deleteChildren(ctx context.Context, childrenResources []*ParsedResource, folders []Folder) error { + for _, parsed := range childrenResources { + err := parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete nested resource from grafana: %w", err) + } + } + + // we need to delete the folders furthest down in the tree first, as folder deletion will fail if there is anything inside of it + safepath.SortByDepth(folders, func(f Folder) string { return f.Path }, false) + + for _, f := range folders { + err := r.folders.Client().Delete(ctx, f.ID, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed to delete folder from grafana: %w", err) + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/resources/parser_factory_mock.go b/pkg/registry/apis/provisioning/resources/parser_factory_mock.go index 434863be21c..b266809d58e 100644 --- a/pkg/registry/apis/provisioning/resources/parser_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/parser_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/parser_mock.go b/pkg/registry/apis/provisioning/resources/parser_mock.go index c207d0e9095..21e6ab3e78e 100644 --- a/pkg/registry/apis/provisioning/resources/parser_mock.go +++ b/pkg/registry/apis/provisioning/resources/parser_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go b/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go index 3e55762e261..9bf9473d180 100644 --- a/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/repository_resources_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/repository_resources_mock.go b/pkg/registry/apis/provisioning/resources/repository_resources_mock.go index 6245188528e..7e11d9b9eda 100644 --- a/pkg/registry/apis/provisioning/resources/repository_resources_mock.go +++ b/pkg/registry/apis/provisioning/resources/repository_resources_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources @@ -26,64 +26,6 @@ func (_m *MockRepositoryResources) EXPECT() *MockRepositoryResources_Expecter { return &MockRepositoryResources_Expecter{mock: &_m.Mock} } -// WriteResourceFileFromObject provides a mock function with given fields: ctx, obj, options -func (_m *MockRepositoryResources) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) { - ret := _m.Called(ctx, obj, options) - - if len(ret) == 0 { - panic("no return value specified for WriteResourceFileFromObject") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)); ok { - return rf(ctx, obj, options) - } - if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) string); ok { - r0 = rf(ctx, obj, options) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(context.Context, *unstructured.Unstructured, WriteOptions) error); ok { - r1 = rf(ctx, obj, options) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockRepositoryResources_WriteResourceFileFromObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteResourceFileFromObject' -type MockRepositoryResources_WriteResourceFileFromObject_Call struct { - *mock.Call -} - -// WriteResourceFileFromObject is a helper method to define mock.On call -// - ctx context.Context -// - obj *unstructured.Unstructured -// - options WriteOptions -func (_e *MockRepositoryResources_Expecter) WriteResourceFileFromObject(ctx interface{}, obj interface{}, options interface{}) *MockRepositoryResources_WriteResourceFileFromObject_Call { - return &MockRepositoryResources_WriteResourceFileFromObject_Call{Call: _e.mock.On("WriteResourceFileFromObject", ctx, obj, options)} -} - -func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Run(run func(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions)) *MockRepositoryResources_WriteResourceFileFromObject_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*unstructured.Unstructured), args[2].(WriteOptions)) - }) - return _c -} - -func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_WriteResourceFileFromObject_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) RunAndReturn(run func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)) *MockRepositoryResources_WriteResourceFileFromObject_Call { - _c.Call.Return(run) - return _c -} - // EnsureFolderExists provides a mock function with given fields: ctx, folder, parentID func (_m *MockRepositoryResources) EnsureFolderExists(ctx context.Context, folder Folder, parentID string) error { ret := _m.Called(ctx, folder, parentID) @@ -520,6 +462,64 @@ func (_c *MockRepositoryResources_Stats_Call) RunAndReturn(run func(context.Cont return _c } +// WriteResourceFileFromObject provides a mock function with given fields: ctx, obj, options +func (_m *MockRepositoryResources) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) { + ret := _m.Called(ctx, obj, options) + + if len(ret) == 0 { + panic("no return value specified for WriteResourceFileFromObject") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)); ok { + return rf(ctx, obj, options) + } + if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, WriteOptions) string); ok { + r0 = rf(ctx, obj, options) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *unstructured.Unstructured, WriteOptions) error); ok { + r1 = rf(ctx, obj, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositoryResources_WriteResourceFileFromObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteResourceFileFromObject' +type MockRepositoryResources_WriteResourceFileFromObject_Call struct { + *mock.Call +} + +// WriteResourceFileFromObject is a helper method to define mock.On call +// - ctx context.Context +// - obj *unstructured.Unstructured +// - options WriteOptions +func (_e *MockRepositoryResources_Expecter) WriteResourceFileFromObject(ctx interface{}, obj interface{}, options interface{}) *MockRepositoryResources_WriteResourceFileFromObject_Call { + return &MockRepositoryResources_WriteResourceFileFromObject_Call{Call: _e.mock.On("WriteResourceFileFromObject", ctx, obj, options)} +} + +func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Run(run func(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions)) *MockRepositoryResources_WriteResourceFileFromObject_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*unstructured.Unstructured), args[2].(WriteOptions)) + }) + return _c +} + +func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) Return(_a0 string, _a1 error) *MockRepositoryResources_WriteResourceFileFromObject_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepositoryResources_WriteResourceFileFromObject_Call) RunAndReturn(run func(context.Context, *unstructured.Unstructured, WriteOptions) (string, error)) *MockRepositoryResources_WriteResourceFileFromObject_Call { + _c.Call.Return(run) + return _c +} + // WriteResourceFromFile provides a mock function with given fields: ctx, path, ref func (_m *MockRepositoryResources) WriteResourceFromFile(ctx context.Context, path string, ref string) (string, schema.GroupVersionKind, error) { ret := _m.Called(ctx, path, ref) diff --git a/pkg/registry/apis/provisioning/resources/resource_lister_mock.go b/pkg/registry/apis/provisioning/resources/resource_lister_mock.go index fae2e39b221..29fb1712f51 100644 --- a/pkg/registry/apis/provisioning/resources/resource_lister_mock.go +++ b/pkg/registry/apis/provisioning/resources/resource_lister_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/resources/resources.go b/pkg/registry/apis/provisioning/resources/resources.go index 0acbbc4c36a..b1f5cef53c2 100644 --- a/pkg/registry/apis/provisioning/resources/resources.go +++ b/pkg/registry/apis/provisioning/resources/resources.go @@ -7,6 +7,7 @@ import ( "fmt" "slices" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -121,7 +122,7 @@ func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj err = r.repo.Write(ctx, fileName, options.Ref, body, commitMessage) if err != nil { - return "", fmt.Errorf("failed to write file: %w", err) + return "", fmt.Errorf("failed to write file: %s, %w", fileName, err) } return fileName, nil @@ -206,6 +207,10 @@ func (r *ResourcesManager) RemoveResourceFromFile(ctx context.Context, path stri err = client.Delete(ctx, objName, metav1.DeleteOptions{}) if err != nil { + if apierrors.IsNotFound(err) { + return objName, schema.GroupVersionKind{}, nil // Already deleted or simply non-existing, nothing to do + } + return "", schema.GroupVersionKind{}, fmt.Errorf("failed to delete: %w", err) } diff --git a/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go b/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go index 6f67bc8ffda..80647e0129c 100644 --- a/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go +++ b/pkg/registry/apis/provisioning/resources/signature/signature_factory_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package signature diff --git a/pkg/registry/apis/provisioning/resources/signature/signer_mock.go b/pkg/registry/apis/provisioning/resources/signature/signer_mock.go index 93532369d5c..46cd71dab55 100644 --- a/pkg/registry/apis/provisioning/resources/signature/signer_mock.go +++ b/pkg/registry/apis/provisioning/resources/signature/signer_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package signature diff --git a/pkg/registry/apis/provisioning/resources/tree.go b/pkg/registry/apis/provisioning/resources/tree.go index e18ad25f873..427384e8753 100644 --- a/pkg/registry/apis/provisioning/resources/tree.go +++ b/pkg/registry/apis/provisioning/resources/tree.go @@ -3,7 +3,6 @@ package resources import ( "context" "fmt" - "sort" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -95,10 +94,8 @@ func (t *folderTree) Walk(ctx context.Context, fn WalkFunc) error { toWalk = append(toWalk, folder) } - // sort by depth of the paths - sort.Slice(toWalk, func(i, j int) bool { - return safepath.Depth(toWalk[i].Path) < safepath.Depth(toWalk[j].Path) - }) + // sort by depth (shallowest first) + safepath.SortByDepth(toWalk, func(f Folder) string { return f.Path }, true) for _, folder := range toWalk { if err := fn(ctx, folder, t.tree[folder.ID]); err != nil { diff --git a/pkg/registry/apis/provisioning/resources/tree_mock.go b/pkg/registry/apis/provisioning/resources/tree_mock.go index 86b4038a42e..d07ec5459a7 100644 --- a/pkg/registry/apis/provisioning/resources/tree_mock.go +++ b/pkg/registry/apis/provisioning/resources/tree_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package resources diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go index 5f0675e6778..5e222882bd0 100644 --- a/pkg/registry/apis/provisioning/routes.go +++ b/pkg/registry/apis/provisioning/routes.go @@ -156,11 +156,18 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) { return } + availableTypes := []provisioning.RepositoryType{} + for t := range b.availableRepositoryTypes { + availableTypes = append(availableTypes, t) + } + settings := provisioning.RepositoryViewList{ Items: make([]provisioning.RepositoryView, len(all)), // FIXME: this shouldn't be here in provisioning but at the dual writer or something about the storage - LegacyStorage: dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, b.storageStatus), + LegacyStorage: dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, b.storageStatus), + AvailableRepositoryTypes: availableTypes, } + for i, val := range all { branch := "" if val.Spec.GitHub != nil { diff --git a/pkg/registry/apis/provisioning/safepath/walk.go b/pkg/registry/apis/provisioning/safepath/walk.go index 048745da188..a048d239e2d 100644 --- a/pkg/registry/apis/provisioning/safepath/walk.go +++ b/pkg/registry/apis/provisioning/safepath/walk.go @@ -3,6 +3,7 @@ package safepath import ( "context" "path" + "sort" "strings" ) @@ -43,3 +44,22 @@ func Split(p string) []string { } return strings.Split(trimmed, "/") } + +// SortByDepth will sort any resource, by its path depth. You must pass in +// a way to get said path. Ties are alphabetical by default. +func SortByDepth[T any](items []T, pathExtractor func(T) string, asc bool) { + sort.Slice(items, func(i, j int) bool { + pathI, pathJ := pathExtractor(items[i]), pathExtractor(items[j]) + depthI, depthJ := Depth(pathI), Depth(pathJ) + + if depthI == depthJ { + // alphabetical by default if depth is the same + return pathI < pathJ + } + + if asc { + return depthI < depthJ + } + return depthI > depthJ + }) +} diff --git a/pkg/registry/apis/provisioning/safepath/walk_test.go b/pkg/registry/apis/provisioning/safepath/walk_test.go index c1694f2af53..9789e8c0ab0 100644 --- a/pkg/registry/apis/provisioning/safepath/walk_test.go +++ b/pkg/registry/apis/provisioning/safepath/walk_test.go @@ -176,3 +176,56 @@ func TestWalkError(t *testing.T) { require.ErrorIs(t, err, expectedErr) } + +func TestSortByDepth(t *testing.T) { + tests := []struct { + name string + asc bool + paths []string + expected []string + }{ + { + name: "ascending sort (shallowest first)", + paths: []string{"a/b/c", "a", "a/b", "d/e/f/g"}, + asc: true, + expected: []string{"a", "a/b", "a/b/c", "d/e/f/g"}, + }, + { + name: "descending sort with alphabetical tie-break", + paths: []string{"a/b/c", "a", "a/b", "d/e/f/g", "x/y/z"}, + asc: false, + expected: []string{"d/e/f/g", "a/b/c", "x/y/z", "a/b", "a"}, + }, + { + name: "paths with empty string", + paths: []string{"a/b/c", "", "a", "a/b"}, + asc: true, + expected: []string{"", "a", "a/b", "a/b/c"}, + }, + { + name: "paths with trailing slashes", + paths: []string{"a/b/", "a/b/c", "b/", "a/", "a"}, + asc: true, + expected: []string{"a", "a/", "b/", "a/b/", "a/b/c"}, + }, + { + name: "single path", + paths: []string{"a/b/c"}, + expected: []string{"a/b/c"}, + }, + { + name: "empty paths", + paths: []string{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + paths := make([]string, len(tt.paths)) + copy(paths, tt.paths) + SortByDepth(paths, func(s string) string { return s }, tt.asc) + assert.Equal(t, tt.expected, paths) + }) + } +} diff --git a/pkg/registry/apis/provisioning/secrets/legacy_secret.go b/pkg/registry/apis/provisioning/secrets/legacy_secret.go new file mode 100644 index 00000000000..6190e1389ff --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/legacy_secret.go @@ -0,0 +1,37 @@ +package secrets + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/secrets" +) + +// A secrets encryption service. It only operates on values, no names or similar. +// It is likely we will need to change this when the multi-tenant service comes around. +// +// FIXME: this is a temporary service/package until we can make use of +// the new secrets service in app platform. +// +//go:generate mockery --name LegacyService --structname MockLegacyService --inpackage --filename legacy_secret_mock.go --with-expecter +type LegacyService interface { + Encrypt(ctx context.Context, data []byte) ([]byte, error) + Decrypt(ctx context.Context, data []byte) ([]byte, error) +} + +var _ LegacyService = (*singleTenant)(nil) + +type singleTenant struct { + inner secrets.Service +} + +func NewSingleTenant(svc secrets.Service) LegacyService { + return &singleTenant{svc} +} + +func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) { + return s.inner.Encrypt(ctx, data, secrets.WithoutScope()) +} + +func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) { + return s.inner.Decrypt(ctx, data) +} diff --git a/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go b/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go new file mode 100644 index 00000000000..01f3ee1f853 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/legacy_secret_mock.go @@ -0,0 +1,154 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package secrets + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockLegacyService is an autogenerated mock type for the LegacyService type +type MockLegacyService struct { + mock.Mock +} + +type MockLegacyService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockLegacyService) EXPECT() *MockLegacyService_Expecter { + return &MockLegacyService_Expecter{mock: &_m.Mock} +} + +// Decrypt provides a mock function with given fields: ctx, data +func (_m *MockLegacyService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { + ret := _m.Called(ctx, data) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { + r0 = rf(ctx, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockLegacyService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' +type MockLegacyService_Decrypt_Call struct { + *mock.Call +} + +// Decrypt is a helper method to define mock.On call +// - ctx context.Context +// - data []byte +func (_e *MockLegacyService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockLegacyService_Decrypt_Call { + return &MockLegacyService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)} +} + +func (_c *MockLegacyService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Decrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]byte)) + }) + return _c +} + +func (_c *MockLegacyService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Decrypt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockLegacyService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Decrypt_Call { + _c.Call.Return(run) + return _c +} + +// Encrypt provides a mock function with given fields: ctx, data +func (_m *MockLegacyService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { + ret := _m.Called(ctx, data) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { + return rf(ctx, data) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { + r0 = rf(ctx, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { + r1 = rf(ctx, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockLegacyService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' +type MockLegacyService_Encrypt_Call struct { + *mock.Call +} + +// Encrypt is a helper method to define mock.On call +// - ctx context.Context +// - data []byte +func (_e *MockLegacyService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockLegacyService_Encrypt_Call { + return &MockLegacyService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)} +} + +func (_c *MockLegacyService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Encrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]byte)) + }) + return _c +} + +func (_c *MockLegacyService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Encrypt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockLegacyService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Encrypt_Call { + _c.Call.Return(run) + return _c +} + +// NewMockLegacyService creates a new instance of MockLegacyService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockLegacyService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockLegacyService { + mock := &MockLegacyService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go new file mode 100644 index 00000000000..50ad9773e93 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go @@ -0,0 +1,111 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + service "github.com/grafana/grafana/pkg/registry/apis/secret/service" + mock "github.com/stretchr/testify/mock" +) + +// MockDecryptService is an autogenerated mock type for the DecryptService type +type MockDecryptService struct { + mock.Mock +} + +type MockDecryptService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDecryptService) EXPECT() *MockDecryptService_Expecter { + return &MockDecryptService_Expecter{mock: &_m.Mock} +} + +// Decrypt provides a mock function with given fields: ctx, namespace, names +func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]service.DecryptResult, error) { + _va := make([]interface{}, len(names)) + for _i := range names { + _va[_i] = names[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, namespace) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 map[string]service.DecryptResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) (map[string]service.DecryptResult, error)); ok { + return rf(ctx, namespace, names...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) map[string]service.DecryptResult); ok { + r0 = rf(ctx, namespace, names...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]service.DecryptResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...string) error); ok { + r1 = rf(ctx, namespace, names...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDecryptService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' +type MockDecryptService_Decrypt_Call struct { + *mock.Call +} + +// Decrypt is a helper method to define mock.On call +// - ctx context.Context +// - namespace string +// - names ...string +func (_e *MockDecryptService_Expecter) Decrypt(ctx interface{}, namespace interface{}, names ...interface{}) *MockDecryptService_Decrypt_Call { + return &MockDecryptService_Decrypt_Call{Call: _e.mock.On("Decrypt", + append([]interface{}{ctx, namespace}, names...)...)} +} + +func (_c *MockDecryptService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, names ...string)) *MockDecryptService_Decrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *MockDecryptService_Decrypt_Call) Return(_a0 map[string]service.DecryptResult, _a1 error) *MockDecryptService_Decrypt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDecryptService_Decrypt_Call) RunAndReturn(run func(context.Context, string, ...string) (map[string]service.DecryptResult, error)) *MockDecryptService_Decrypt_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDecryptService creates a new instance of MockDecryptService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDecryptService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDecryptService { + mock := &MockDecryptService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/secrets/repository.go b/pkg/registry/apis/provisioning/secrets/repository.go new file mode 100644 index 00000000000..8e020b32b10 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/repository.go @@ -0,0 +1,107 @@ +package secrets + +import ( + "context" + "errors" + "strings" + + "github.com/grafana/grafana-app-sdk/logging" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/services/featuremgmt" + grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" +) + +func ProvideRepositorySecrets( + features featuremgmt.FeatureToggles, + legacySecretsSvc grafanasecrets.Service, + secretsSvc *service.SecureValueService, + decryptSvc service.DecryptService, +) RepositorySecrets { + return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc), NewSingleTenant(legacySecretsSvc)) +} + +//go:generate mockery --name RepositorySecrets --structname MockRepositorySecrets --inpackage --filename repository_secrets_mock.go --with-expecter +type RepositorySecrets interface { + Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) + Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) (data []byte, err error) + Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error +} + +// repositorySecrets provides a unified interface for encrypting and decrypting repository secrets, +// supporting both the legacy and new secrets services. The active backend is determined by the +// FlagProvisioningSecretsService feature flag: +// - If enabled, operations use the new secrets service. +// - If disabled, operations use the legacy secrets service. +// +// This abstraction enables a seamless migration path between secret backends without breaking +// existing functionality. Once migration is complete and the legacy service is deprecated, +// this wrapper should be removed. +type repositorySecrets struct { + features featuremgmt.FeatureToggles + secretsSvc Service + legacySecrets LegacyService +} + +func NewRepositorySecrets(features featuremgmt.FeatureToggles, secretsSvc Service, legacySecrets LegacyService) RepositorySecrets { + return &repositorySecrets{ + features: features, + secretsSvc: secretsSvc, + legacySecrets: legacySecrets, + } +} + +// Encrypt encrypts the data and returns the name or value of the encrypted data +// If the feature flag is disabled, it uses the legacy secrets service +// If the feature flag is enabled, it uses the secrets service +func (s *repositorySecrets) Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) { + if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { + encrypted, err := s.secretsSvc.Encrypt(ctx, r.GetNamespace(), name, data) + if err != nil { + return nil, err + } + return []byte(encrypted), err + } + + encrypted, err := s.legacySecrets.Encrypt(ctx, []byte(data)) + if err != nil { + return nil, err + } + + return encrypted, nil +} + +// Decrypt retrieves and decrypts secret data for a repository, supporting migration between secret backends. +// The backend used for decryption is determined by a heuristic: +// - If the provided nameOrValue starts with the repository name, it is assumed to be a Kubernetes secret name +// and the new secrets service is used for decryption. +// - Otherwise, it is treated as a legacy secret value and the legacy secrets service is used. +// +// HACK: This approach relies on checking the prefix of nameOrValue to distinguish between secret backends. +// This is a temporary workaround to support both backends during migration and should be removed once +// migration is complete. +// +// This method ensures compatibility and minimizes disruption during the transition between secret backends. +func (s *repositorySecrets) Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) ([]byte, error) { + logger := logging.FromContext(ctx) + // HACK: this is a hack to identify if the name is a potential Kubernetes name for a secret. + if strings.HasPrefix(nameOrValue, r.GetName()) { + logger.Info("Decrypting secret with new secrets service", "name", nameOrValue) + return s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue) + } else { + logger.Info("Decrypting secret with legacy secrets service", "name", nameOrValue) + return s.legacySecrets.Decrypt(ctx, []byte(nameOrValue)) + } +} + +func (s *repositorySecrets) Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error { + if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { + err := s.secretsSvc.Delete(ctx, r.GetNamespace(), nameOrValue) + if err != nil && !errors.Is(err, contracts.ErrSecureValueNotFound) { + return err + } + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go b/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go new file mode 100644 index 00000000000..763422ef7c5 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/repository_secrets_mock.go @@ -0,0 +1,206 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package secrets + +import ( + context "context" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + mock "github.com/stretchr/testify/mock" +) + +// MockRepositorySecrets is an autogenerated mock type for the RepositorySecrets type +type MockRepositorySecrets struct { + mock.Mock +} + +type MockRepositorySecrets_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRepositorySecrets) EXPECT() *MockRepositorySecrets_Expecter { + return &MockRepositorySecrets_Expecter{mock: &_m.Mock} +} + +// Decrypt provides a mock function with given fields: ctx, r, nameOrValue +func (_m *MockRepositorySecrets) Decrypt(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) ([]byte, error) { + ret := _m.Called(ctx, r, nameOrValue) + + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) ([]byte, error)); ok { + return rf(ctx, r, nameOrValue) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) []byte); ok { + r0 = rf(ctx, r, nameOrValue) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string) error); ok { + r1 = rf(ctx, r, nameOrValue) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositorySecrets_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt' +type MockRepositorySecrets_Decrypt_Call struct { + *mock.Call +} + +// Decrypt is a helper method to define mock.On call +// - ctx context.Context +// - r *v0alpha1.Repository +// - nameOrValue string +func (_e *MockRepositorySecrets_Expecter) Decrypt(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Decrypt_Call { + return &MockRepositorySecrets_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, r, nameOrValue)} +} + +func (_c *MockRepositorySecrets_Decrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Decrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string)) + }) + return _c +} + +func (_c *MockRepositorySecrets_Decrypt_Call) Return(data []byte, err error) *MockRepositorySecrets_Decrypt_Call { + _c.Call.Return(data, err) + return _c +} + +func (_c *MockRepositorySecrets_Decrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) ([]byte, error)) *MockRepositorySecrets_Decrypt_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, r, nameOrValue +func (_m *MockRepositorySecrets) Delete(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) error { + ret := _m.Called(ctx, r, nameOrValue) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) error); ok { + r0 = rf(ctx, r, nameOrValue) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockRepositorySecrets_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockRepositorySecrets_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - r *v0alpha1.Repository +// - nameOrValue string +func (_e *MockRepositorySecrets_Expecter) Delete(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Delete_Call { + return &MockRepositorySecrets_Delete_Call{Call: _e.mock.On("Delete", ctx, r, nameOrValue)} +} + +func (_c *MockRepositorySecrets_Delete_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string)) + }) + return _c +} + +func (_c *MockRepositorySecrets_Delete_Call) Return(_a0 error) *MockRepositorySecrets_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRepositorySecrets_Delete_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) error) *MockRepositorySecrets_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Encrypt provides a mock function with given fields: ctx, r, name, data +func (_m *MockRepositorySecrets) Encrypt(ctx context.Context, r *v0alpha1.Repository, name string, data string) ([]byte, error) { + ret := _m.Called(ctx, r, name, data) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)); ok { + return rf(ctx, r, name, data) + } + if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) []byte); ok { + r0 = rf(ctx, r, name, data) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string, string) error); ok { + r1 = rf(ctx, r, name, data) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositorySecrets_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' +type MockRepositorySecrets_Encrypt_Call struct { + *mock.Call +} + +// Encrypt is a helper method to define mock.On call +// - ctx context.Context +// - r *v0alpha1.Repository +// - name string +// - data string +func (_e *MockRepositorySecrets_Expecter) Encrypt(ctx interface{}, r interface{}, name interface{}, data interface{}) *MockRepositorySecrets_Encrypt_Call { + return &MockRepositorySecrets_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, r, name, data)} +} + +func (_c *MockRepositorySecrets_Encrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, name string, data string)) *MockRepositorySecrets_Encrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *MockRepositorySecrets_Encrypt_Call) Return(nameOrValue []byte, err error) *MockRepositorySecrets_Encrypt_Call { + _c.Call.Return(nameOrValue, err) + return _c +} + +func (_c *MockRepositorySecrets_Encrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)) *MockRepositorySecrets_Encrypt_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRepositorySecrets creates a new instance of MockRepositorySecrets. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRepositorySecrets(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepositorySecrets { + mock := &MockRepositorySecrets{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/secrets/repository_test.go b/pkg/registry/apis/provisioning/secrets/repository_test.go new file mode 100644 index 00000000000..028efd10853 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/repository_test.go @@ -0,0 +1,322 @@ +package secrets + +import ( + "context" + "errors" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type testSetup struct { + rs RepositorySecrets + mockFeatures *featuremgmt.MockFeatureToggles + mockSecrets *MockService + mockLegacy *MockLegacyService + repo *provisioning.Repository + ctx context.Context +} + +func setupTest(t *testing.T, namespace string) *testSetup { + mockFeatures := featuremgmt.NewMockFeatureToggles(t) + mockSecrets := NewMockService(t) + mockLegacy := NewMockLegacyService(t) + + rs := NewRepositorySecrets(mockFeatures, mockSecrets, mockLegacy) + + repo := &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: namespace, + }, + } + + return &testSetup{ + rs: rs, + mockFeatures: mockFeatures, + mockSecrets: mockSecrets, + mockLegacy: mockLegacy, + repo: repo, + ctx: context.Background(), + } +} + +func (s *testSetup) expectFeatureFlag(enabled bool) { + s.mockFeatures.EXPECT().IsEnabled( + mock.AnythingOfType("context.backgroundCtx"), + featuremgmt.FlagProvisioningSecretsService, + ).Return(enabled) +} + +func TestRepositorySecrets_Encrypt(t *testing.T) { + tests := []struct { + name string + namespace string + featureEnabled bool + setupMocks func(*testSetup) + expectedResult []byte + expectedError string + }{ + { + name: "new service success", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("encrypted-name", nil) + }, + expectedResult: []byte("encrypted-name"), + }, + { + name: "legacy service success", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return([]byte("encrypted-legacy-data"), nil) + }, + expectedResult: []byte("encrypted-legacy-data"), + }, + { + name: "new service error", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("", errors.New("encryption failed")) + }, + expectedError: "encryption failed", + }, + { + name: "legacy service error", + namespace: "test-namespace", + featureEnabled: false, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return(nil, errors.New("legacy encryption failed")) + }, + expectedError: "legacy encryption failed", + }, + { + name: "empty namespace handling", + namespace: "", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Encrypt(s.ctx, "", "test-secret", "secret-data").Return("encrypted", nil) + }, + expectedResult: []byte("encrypted"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupTest(t, tt.namespace) + tt.setupMocks(setup) + + result, err := setup.rs.Encrypt(setup.ctx, setup.repo, "test-secret", "secret-data") + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +func TestRepositorySecrets_Decrypt(t *testing.T) { + tests := []struct { + name string + namespace string + nameOrValue string + setupMocks func(*testSetup) + expectedResult []byte + expectedError string + }{ + { + name: "new service success - name starts with repo name", + namespace: "test-namespace", + nameOrValue: "test-repo-secret-name", + setupMocks: func(s *testSetup) { + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte("decrypted-data"), nil) + }, + expectedResult: []byte("decrypted-data"), + }, + { + name: "new service error - name starts with repo name", + namespace: "test-namespace", + nameOrValue: "test-repo-secret-name", + setupMocks: func(s *testSetup) { + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return(nil, errors.New("new service failed")) + }, + expectedError: "new service failed", + }, + { + name: "legacy service success - name does not start with repo name", + namespace: "test-namespace", + nameOrValue: "legacy-encrypted-value", + setupMocks: func(s *testSetup) { + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte("decrypted-legacy-data"), nil) + }, + expectedResult: []byte("decrypted-legacy-data"), + }, + { + name: "legacy service error - name does not start with repo name", + namespace: "test-namespace", + nameOrValue: "legacy-encrypted-value", + setupMocks: func(s *testSetup) { + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return(nil, errors.New("legacy service failed")) + }, + expectedError: "legacy service failed", + }, + { + name: "new service empty bytes - name starts with repo name", + namespace: "test-namespace", + nameOrValue: "test-repo-secret-name", + setupMocks: func(s *testSetup) { + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte{}, nil) + }, + expectedResult: []byte{}, + }, + { + name: "legacy service empty bytes - name does not start with repo name", + namespace: "test-namespace", + nameOrValue: "legacy-encrypted-value", + setupMocks: func(s *testSetup) { + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte{}, nil) + }, + expectedResult: []byte{}, + }, + { + name: "custom namespace handling - name starts with repo name", + namespace: "custom-namespace", + nameOrValue: "test-repo-secret-name", + setupMocks: func(s *testSetup) { + s.mockSecrets.EXPECT().Decrypt(s.ctx, "custom-namespace", "test-repo-secret-name").Return([]byte("test-data"), nil) + }, + expectedResult: []byte("test-data"), + }, + { + name: "exact repo name match - should use new service", + namespace: "test-namespace", + nameOrValue: "test-repo", + setupMocks: func(s *testSetup) { + s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo").Return([]byte("exact-match-data"), nil) + }, + expectedResult: []byte("exact-match-data"), + }, + { + name: "partial repo name match - should use legacy service", + namespace: "test-namespace", + nameOrValue: "test-rep", + setupMocks: func(s *testSetup) { + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("test-rep")).Return([]byte("partial-match-data"), nil) + }, + expectedResult: []byte("partial-match-data"), + }, + { + name: "empty name - should use legacy service", + namespace: "test-namespace", + nameOrValue: "", + setupMocks: func(s *testSetup) { + s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("")).Return([]byte("empty-name-data"), nil) + }, + expectedResult: []byte("empty-name-data"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupTest(t, tt.namespace) + tt.setupMocks(setup) + + result, err := setup.rs.Decrypt(setup.ctx, setup.repo, tt.nameOrValue) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +func TestRepositorySecrets_Delete(t *testing.T) { + tests := []struct { + name string + namespace string + featureEnabled bool + setupMocks func(*testSetup) + expectedError string + }{ + { + name: "new service delete success", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(nil) + }, + }, + { + name: "new service delete error", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(errors.New("delete failed")) + }, + expectedError: "delete failed", + }, + { + name: "new service secret not found - should succeed", + namespace: "test-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(true) + s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "non-existent-secret").Return(contracts.ErrSecureValueNotFound) + }, + }, + { + name: "nothing for legacy", + namespace: "custom-namespace", + featureEnabled: true, + setupMocks: func(s *testSetup) { + s.expectFeatureFlag(false) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setup := setupTest(t, tt.namespace) + tt.setupMocks(setup) + + secretName := "secret-to-delete" + if tt.name == "new service secret not found - should succeed" { + secretName = "non-existent-secret" + } + err := setup.rs.Delete(setup.ctx, setup.repo, secretName) + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + } + + setup.mockSecrets.AssertExpectations(t) + }) + } +} diff --git a/pkg/registry/apis/provisioning/secrets/secret.go b/pkg/registry/apis/provisioning/secrets/secret.go index 10e09f548d3..412ca79bb49 100644 --- a/pkg/registry/apis/provisioning/secrets/secret.go +++ b/pkg/registry/apis/provisioning/secrets/secret.go @@ -2,36 +2,124 @@ package secrets import ( "context" + "errors" - "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/authlib/types" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + grafanasecrets "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// A secrets encryption service. It only operates on values, no names or similar. -// It is likely we will need to change this when the multi-tenant service comes around. -// -// FIXME: this is a temporary service/package until we can make use of -// the new secrets service in app platform. -// +const svcName = "provisioning" + +//go:generate mockery --name SecureValueService --structname MockSecureValueService --inpackage --filename secure_value_mock.go --with-expecter +type SecureValueService interface { + Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) + Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) + Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) +} + //go:generate mockery --name Service --structname MockService --inpackage --filename secret_mock.go --with-expecter type Service interface { - Encrypt(ctx context.Context, data []byte) ([]byte, error) - Decrypt(ctx context.Context, data []byte) ([]byte, error) + Encrypt(ctx context.Context, namespace, name string, data string) (string, error) + Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) + Delete(ctx context.Context, namespace string, name string) error } -var _ Service = (*singleTenant)(nil) +var _ Service = (*secretsService)(nil) -type singleTenant struct { - inner secrets.Service +//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret/service --filename decrypt_service_mock.go --with-expecter +type secretsService struct { + secretsSvc SecureValueService + decryptSvc grafanasecrets.DecryptService } -func NewSingleTenant(svc secrets.Service) *singleTenant { - return &singleTenant{svc} +func NewSecretsService(secretsSvc SecureValueService, decryptSvc grafanasecrets.DecryptService) Service { + return &secretsService{ + secretsSvc: secretsSvc, + decryptSvc: decryptSvc, + } } -func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - return s.inner.Encrypt(ctx, data, secrets.WithoutScope()) +func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, data string) (string, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return "", err + } + + val := secretv1beta1.NewExposedSecureValue(data) + secret := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "provisioning: " + name, + Value: &val, + Decrypters: []string{svcName}, + }, + } + + existing, err := s.secretsSvc.Read(ctx, xkube.Namespace(namespace), name) + if err != nil && !errors.Is(err, contracts.ErrSecureValueNotFound) { + return "", err + } + + if existing != nil { + existing.Spec.Value = &val + existing, _, err = s.secretsSvc.Update(ctx, existing, user.GetUID()) + if err != nil { + return "", err + } + + return existing.GetName(), nil + } + + finalSecret, err := s.secretsSvc.Create(ctx, secret, user.GetUID()) + if err != nil { + return "", err + } + + return finalSecret.GetName(), nil } -func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - return s.inner.Decrypt(ctx, data) +func (s *secretsService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) { + ns, err := types.ParseNamespace(namespace) + if err != nil { + return nil, err + } + ctx = identity.WithServiceIdentityContext(ctx, ns.OrgID, identity.WithServiceIdentityName(svcName)) + + results, err := s.decryptSvc.Decrypt(ctx, namespace, name) + if err != nil { + return nil, err + } + + if res, ok := results[name]; ok { + if res.Error() == nil { + return []byte(res.Value().DangerouslyExposeAndConsumeValue()), nil + } + + return nil, res.Error() + } + + return nil, contracts.ErrDecryptNotFound +} + +func (s *secretsService) Delete(ctx context.Context, namespace string, name string) error { + ns, err := types.ParseNamespace(namespace) + if err != nil { + return err + } + ctx = identity.WithServiceIdentityContext(ctx, ns.OrgID, identity.WithServiceIdentityName(svcName)) + + if _, err := s.secretsSvc.Delete(ctx, xkube.Namespace(namespace), name); err != nil { + return err + } + + return nil } diff --git a/pkg/registry/apis/provisioning/secrets/secret_mock.go b/pkg/registry/apis/provisioning/secrets/secret_mock.go index d182c819e3c..29c9c04f364 100644 --- a/pkg/registry/apis/provisioning/secrets/secret_mock.go +++ b/pkg/registry/apis/provisioning/secrets/secret_mock.go @@ -21,9 +21,9 @@ func (_m *MockService) EXPECT() *MockService_Expecter { return &MockService_Expecter{mock: &_m.Mock} } -// Decrypt provides a mock function with given fields: ctx, data -func (_m *MockService) Decrypt(ctx context.Context, data []byte) ([]byte, error) { - ret := _m.Called(ctx, data) +// Decrypt provides a mock function with given fields: ctx, namespace, name +func (_m *MockService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) { + ret := _m.Called(ctx, namespace, name) if len(ret) == 0 { panic("no return value specified for Decrypt") @@ -31,19 +31,19 @@ func (_m *MockService) Decrypt(ctx context.Context, data []byte) ([]byte, error) var r0 []byte var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { - return rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]byte, error)); ok { + return rf(ctx, namespace, name) } - if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { - r0 = rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string) []byte); ok { + r0 = rf(ctx, namespace, name) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]byte) } } - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, data) + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, name) } else { r1 = ret.Error(1) } @@ -58,14 +58,15 @@ type MockService_Decrypt_Call struct { // Decrypt is a helper method to define mock.On call // - ctx context.Context -// - data []byte -func (_e *MockService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockService_Decrypt_Call { - return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)} +// - namespace string +// - name string +func (_e *MockService_Expecter) Decrypt(ctx interface{}, namespace interface{}, name interface{}) *MockService_Decrypt_Call { + return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, namespace, name)} } -func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockService_Decrypt_Call { +func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Decrypt_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]byte)) + run(args[0].(context.Context), args[1].(string), args[2].(string)) }) return _c } @@ -75,34 +76,80 @@ func (_c *MockService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockService_D return _c } -func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockService_Decrypt_Call { +func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, string, string) ([]byte, error)) *MockService_Decrypt_Call { _c.Call.Return(run) return _c } -// Encrypt provides a mock function with given fields: ctx, data -func (_m *MockService) Encrypt(ctx context.Context, data []byte) ([]byte, error) { - ret := _m.Called(ctx, data) +// Delete provides a mock function with given fields: ctx, namespace, name +func (_m *MockService) Delete(ctx context.Context, namespace string, name string) error { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, namespace, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockService_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockService_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - namespace string +// - name string +func (_e *MockService_Expecter) Delete(ctx interface{}, namespace interface{}, name interface{}) *MockService_Delete_Call { + return &MockService_Delete_Call{Call: _e.mock.On("Delete", ctx, namespace, name)} +} + +func (_c *MockService_Delete_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockService_Delete_Call) Return(_a0 error) *MockService_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockService_Delete_Call) RunAndReturn(run func(context.Context, string, string) error) *MockService_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Encrypt provides a mock function with given fields: ctx, namespace, name, data +func (_m *MockService) Encrypt(ctx context.Context, namespace string, name string, data string) (string, error) { + ret := _m.Called(ctx, namespace, name, data) if len(ret) == 0 { panic("no return value specified for Encrypt") } - var r0 []byte + var r0 string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { - return rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok { + return rf(ctx, namespace, name, data) } - if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok { - r0 = rf(ctx, data) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok { + r0 = rf(ctx, namespace, name, data) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } + r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, data) + if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { + r1 = rf(ctx, namespace, name, data) } else { r1 = ret.Error(1) } @@ -117,24 +164,26 @@ type MockService_Encrypt_Call struct { // Encrypt is a helper method to define mock.On call // - ctx context.Context -// - data []byte -func (_e *MockService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockService_Encrypt_Call { - return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)} +// - namespace string +// - name string +// - data string +func (_e *MockService_Expecter) Encrypt(ctx interface{}, namespace interface{}, name interface{}, data interface{}) *MockService_Encrypt_Call { + return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, namespace, name, data)} } -func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockService_Encrypt_Call { +func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, namespace string, name string, data string)) *MockService_Encrypt_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]byte)) + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) }) return _c } -func (_c *MockService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockService_Encrypt_Call { +func (_c *MockService_Encrypt_Call) Return(_a0 string, _a1 error) *MockService_Encrypt_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockService_Encrypt_Call { +func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, string, string, string) (string, error)) *MockService_Encrypt_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/secrets/secret_test.go b/pkg/registry/apis/provisioning/secrets/secret_test.go new file mode 100644 index 00000000000..6bc25a1fd33 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/secret_test.go @@ -0,0 +1,465 @@ +package secrets + +import ( + "context" + "errors" + "testing" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets/mocks" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNewSecretsService(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + assert.NotNil(t, svc) + assert.IsType(t, &secretsService{}, svc) +} + +//nolint:gocyclo // This test is complex but it's a good test for the SecretsService. +func TestSecretsService_Encrypt(t *testing.T) { + tests := []struct { + name string + namespace string + secretName string + data string + setupMocks func(*MockSecureValueService, *mocks.MockDecryptService) + expectedName string + expectedError string + }{ + { + name: "successfully create new secret", + namespace: "test-namespace", + secretName: "test-secret", + data: "secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + // Assert Read call with correct parameters + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "test-secret", + ).Return(nil, contracts.ErrSecureValueNotFound) + + // Assert Create call with detailed validation + mockSecretsSvc.EXPECT().Create( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + if sv.Namespace != "test-namespace" || sv.Name != "test-secret" { + return false + } + if sv.Spec.Description != "provisioning: test-secret" { + return false + } + if sv.Spec.Value == nil { + return false + } + if len(sv.Spec.Decrypters) != 1 || sv.Spec.Decrypters[0] != svcName { + return false + } + // Verify the actual secret value + if sv.Spec.Value.DangerouslyExposeAndConsumeValue() != "secret-data" { + return false + } + return true + }), + ":test-uid", + ).Return(&secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-namespace", + }, + }, nil) + }, + expectedName: "test-secret", + }, + { + name: "successfully update existing secret", + namespace: "test-namespace", + secretName: "existing-secret", + data: "new-secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + existingSecret := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-secret", + Namespace: "test-namespace", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "provisioning: existing-secret", + Decrypters: []string{svcName}, + }, + } + + // Assert Read call with context validation + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "existing-secret", + ).Return(existingSecret, nil) + + // Assert Update call with detailed validation + mockSecretsSvc.EXPECT().Update( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + if sv.Namespace != "test-namespace" || sv.Name != "existing-secret" { + return false + } + if sv.Spec.Value == nil { + return false + } + // Verify the updated secret value + if sv.Spec.Value.DangerouslyExposeAndConsumeValue() != "new-secret-data" { + return false + } + return true + }), + ":test-uid", + ).Return(&secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-secret", + Namespace: "test-namespace", + }, + }, true, nil) + }, + expectedName: "existing-secret", + }, + { + name: "error reading existing secret", + namespace: "test-namespace", + secretName: "test-secret", + data: "secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "test-secret", + ).Return(nil, errors.New("database error")) + }, + expectedError: "database error", + }, + { + name: "error creating new secret", + namespace: "test-namespace", + secretName: "test-secret", + data: "secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "test-secret", + ).Return(nil, contracts.ErrSecureValueNotFound) + + mockSecretsSvc.EXPECT().Create( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + return sv.Namespace == "test-namespace" && + sv.Name == "test-secret" && + sv.Spec.Value != nil && + sv.Spec.Value.DangerouslyExposeAndConsumeValue() == "secret-data" + }), + ":test-uid", + ).Return(nil, errors.New("creation failed")) + }, + expectedError: "creation failed", + }, + { + name: "error updating existing secret", + namespace: "test-namespace", + secretName: "existing-secret", + data: "new-secret-data", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + existingSecret := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-secret", + Namespace: "test-namespace", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "provisioning: existing-secret", + Decrypters: []string{svcName}, + }, + } + + mockSecretsSvc.EXPECT().Read( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + xkube.Namespace("test-namespace"), + "existing-secret", + ).Return(existingSecret, nil) + + mockSecretsSvc.EXPECT().Update( + mock.MatchedBy(func(ctx context.Context) bool { + requester, err := identity.GetRequester(ctx) + return err == nil && requester != nil && requester.GetUID() == ":test-uid" + }), + mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { + return sv.Namespace == "test-namespace" && + sv.Name == "existing-secret" && + sv.Spec.Value != nil && + sv.Spec.Value.DangerouslyExposeAndConsumeValue() == "new-secret-data" + }), + ":test-uid", + ).Return(nil, false, errors.New("update failed")) + }, + expectedError: "update failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + tt.setupMocks(mockSecretsSvc, mockDecryptSvc) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + ctx = identity.WithRequester(ctx, &identity.StaticRequester{ + UserUID: "test-uid", + }) + + result, err := svc.Encrypt(ctx, tt.namespace, tt.secretName, tt.data) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedName, result) + } + }) + } +} + +func TestSecretsService_Encrypt_NoIdentity(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + + result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data") + + assert.Error(t, err) + assert.Empty(t, result) +} + +func TestSecretsService_Decrypt(t *testing.T) { + tests := []struct { + name string + namespace string + secretName string + setupMocks func(*MockSecureValueService, *mocks.MockDecryptService) + expectedResult []byte + expectedError string + }{ + { + name: "successfully decrypt secret", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data") + mockResult := service.NewDecryptResultValue(&exposedValue) + + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + // Verify that the context is not nil (the service creates a new StaticRequester) + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{ + "test-secret": mockResult, + }, nil) + }, + expectedResult: []byte("decrypted-data"), + }, + { + name: "decrypt service error", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(nil, errors.New("decrypt service error")) + }, + expectedError: "decrypt service error", + }, + { + name: "secret not found in results", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{}, nil) + }, + expectedError: contracts.ErrDecryptNotFound.Error(), + }, + { + name: "decrypt result has error", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockResult := service.NewDecryptResultErr(errors.New("decryption failed")) + + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{ + "test-secret": mockResult, + }, nil) + }, + expectedError: "decryption failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + tt.setupMocks(mockSecretsSvc, mockDecryptSvc) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + + result, err := svc.Decrypt(ctx, tt.namespace, tt.secretName) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} + +// Test to verify that the Decrypt method creates the correct StaticRequester +func TestSecretsService_Decrypt_StaticRequesterCreation(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + exposedValue := secretv1beta1.NewExposedSecureValue("test-data") + mockResult := service.NewDecryptResultValue(&exposedValue) + + // Create a more detailed context matcher to verify the StaticRequester is created correctly + mockDecryptSvc.EXPECT().Decrypt( + mock.MatchedBy(func(ctx context.Context) bool { + // At minimum, verify the context is not nil and is different from the original + return ctx != nil + }), + "test-namespace", + "test-secret", + ).Return(map[string]service.DecryptResult{ + "test-secret": mockResult, + }, nil) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + + ctx := context.Background() + result, err := svc.Decrypt(ctx, "test-namespace", "test-secret") + + assert.NoError(t, err) + assert.Equal(t, []byte("test-data"), result) +} + +func TestSecretsService_Delete(t *testing.T) { + tests := []struct { + name string + namespace string + secretName string + setupMocks func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) + expectedError string + }{ + { + name: "delete success", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockSecretsSvc.EXPECT(). + Delete(mock.Anything, xkube.Namespace("test-namespace"), "test-secret"). + Return(&secretv1beta1.SecureValue{}, nil) + }, + }, + { + name: "delete returns error", + namespace: "test-namespace", + secretName: "test-secret", + setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { + mockSecretsSvc.EXPECT(). + Delete(mock.Anything, xkube.Namespace("test-namespace"), "test-secret"). + Return(nil, errors.New("delete failed")) + }, + expectedError: "delete failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecretsSvc := NewMockSecureValueService(t) + mockDecryptSvc := &mocks.MockDecryptService{} + + tt.setupMocks(mockSecretsSvc, mockDecryptSvc) + + svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) + ctx := context.Background() + + err := svc.Delete(ctx, tt.namespace, tt.secretName) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/secrets/secure_value_mock.go b/pkg/registry/apis/provisioning/secrets/secure_value_mock.go new file mode 100644 index 00000000000..cf35e47d870 --- /dev/null +++ b/pkg/registry/apis/provisioning/secrets/secure_value_mock.go @@ -0,0 +1,286 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package secrets + +import ( + context "context" + + v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + mock "github.com/stretchr/testify/mock" + + xkube "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +// MockSecureValueService is an autogenerated mock type for the SecureValueService type +type MockSecureValueService struct { + mock.Mock +} + +type MockSecureValueService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockSecureValueService) EXPECT() *MockSecureValueService_Expecter { + return &MockSecureValueService_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, sv, actorUID +func (_m *MockSecureValueService) Create(ctx context.Context, sv *v1beta1.SecureValue, actorUID string) (*v1beta1.SecureValue, error) { + ret := _m.Called(ctx, sv, actorUID) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 *v1beta1.SecureValue + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, error)); ok { + return rf(ctx, sv, actorUID) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, sv, actorUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1beta1.SecureValue, string) error); ok { + r1 = rf(ctx, sv, actorUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockSecureValueService_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockSecureValueService_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - sv *v1beta1.SecureValue +// - actorUID string +func (_e *MockSecureValueService_Expecter) Create(ctx interface{}, sv interface{}, actorUID interface{}) *MockSecureValueService_Create_Call { + return &MockSecureValueService_Create_Call{Call: _e.mock.On("Create", ctx, sv, actorUID)} +} + +func (_c *MockSecureValueService_Create_Call) Run(run func(ctx context.Context, sv *v1beta1.SecureValue, actorUID string)) *MockSecureValueService_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v1beta1.SecureValue), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Create_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockSecureValueService_Create_Call) RunAndReturn(run func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, namespace, name +func (_m *MockSecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*v1beta1.SecureValue, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 *v1beta1.SecureValue + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, xkube.Namespace, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockSecureValueService_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockSecureValueService_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - namespace xkube.Namespace +// - name string +func (_e *MockSecureValueService_Expecter) Delete(ctx interface{}, namespace interface{}, name interface{}) *MockSecureValueService_Delete_Call { + return &MockSecureValueService_Delete_Call{Call: _e.mock.On("Delete", ctx, namespace, name)} +} + +func (_c *MockSecureValueService_Delete_Call) Run(run func(ctx context.Context, namespace xkube.Namespace, name string)) *MockSecureValueService_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(xkube.Namespace), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Delete_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Delete_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockSecureValueService_Delete_Call) RunAndReturn(run func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: ctx, namespace, name +func (_m *MockSecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*v1beta1.SecureValue, error) { + ret := _m.Called(ctx, namespace, name) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 *v1beta1.SecureValue + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)); ok { + return rf(ctx, namespace, name) + } + if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, namespace, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, xkube.Namespace, string) error); ok { + r1 = rf(ctx, namespace, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockSecureValueService_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockSecureValueService_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - ctx context.Context +// - namespace xkube.Namespace +// - name string +func (_e *MockSecureValueService_Expecter) Read(ctx interface{}, namespace interface{}, name interface{}) *MockSecureValueService_Read_Call { + return &MockSecureValueService_Read_Call{Call: _e.mock.On("Read", ctx, namespace, name)} +} + +func (_c *MockSecureValueService_Read_Call) Run(run func(ctx context.Context, namespace xkube.Namespace, name string)) *MockSecureValueService_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(xkube.Namespace), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Read_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Read_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockSecureValueService_Read_Call) RunAndReturn(run func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Read_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, newSecureValue, actorUID +func (_m *MockSecureValueService) Update(ctx context.Context, newSecureValue *v1beta1.SecureValue, actorUID string) (*v1beta1.SecureValue, bool, error) { + ret := _m.Called(ctx, newSecureValue, actorUID) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *v1beta1.SecureValue + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, bool, error)); ok { + return rf(ctx, newSecureValue, actorUID) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) *v1beta1.SecureValue); ok { + r0 = rf(ctx, newSecureValue, actorUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1beta1.SecureValue) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1beta1.SecureValue, string) bool); ok { + r1 = rf(ctx, newSecureValue, actorUID) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, *v1beta1.SecureValue, string) error); ok { + r2 = rf(ctx, newSecureValue, actorUID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockSecureValueService_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockSecureValueService_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - newSecureValue *v1beta1.SecureValue +// - actorUID string +func (_e *MockSecureValueService_Expecter) Update(ctx interface{}, newSecureValue interface{}, actorUID interface{}) *MockSecureValueService_Update_Call { + return &MockSecureValueService_Update_Call{Call: _e.mock.On("Update", ctx, newSecureValue, actorUID)} +} + +func (_c *MockSecureValueService_Update_Call) Run(run func(ctx context.Context, newSecureValue *v1beta1.SecureValue, actorUID string)) *MockSecureValueService_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v1beta1.SecureValue), args[2].(string)) + }) + return _c +} + +func (_c *MockSecureValueService_Update_Call) Return(_a0 *v1beta1.SecureValue, _a1 bool, _a2 error) *MockSecureValueService_Update_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockSecureValueService_Update_Call) RunAndReturn(run func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, bool, error)) *MockSecureValueService_Update_Call { + _c.Call.Return(run) + return _c +} + +// NewMockSecureValueService creates a new instance of MockSecureValueService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSecureValueService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockSecureValueService { + mock := &MockSecureValueService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/usage.go b/pkg/registry/apis/provisioning/usage.go index fa45de200dd..205a3018662 100644 --- a/pkg/registry/apis/provisioning/usage.go +++ b/pkg/registry/apis/provisioning/usage.go @@ -4,34 +4,56 @@ import ( "context" "fmt" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/endpoints/request" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) -func (b *APIBuilder) collectProvisioningStats(ctx context.Context) (map[string]any, error) { +func (b *APIBuilder) collectProvisioningStats(ctx context.Context) (metrics map[string]any, err error) { + ctx, span := b.tracer.Start(ctx, "Provisioning.Usage.collectProvisioningStats") + defer func() { + span.SetStatus(codes.Error, fmt.Sprintf("failed to fetch provisioning usage stats: %v", err)) + span.End() + }() + m := map[string]any{} if b.unified == nil { + span.SetStatus(codes.Ok, "unified storage is not available") return m, nil } + // FIXME: hardcoded to "default" for now -- it works for single tenant deployments + // we could discover the set of valid namespaces, but that would count everything for + // each instance in cloud. + ns := "default" + ctx, _, err = identity.WithProvisioningIdentity(ctx, ns) + if err != nil { + return nil, err + } + ctx = request.WithNamespace(ctx, ns) + // FIXME: hardcoded to "default" for now -- it works for single tenant deployments // we could discover the set of valid namespaces, but that would count everything for // each instance in cloud. // // We could get namespaces from the list of repos below, but that could be zero // while we still have resources managed by terraform, etc - ns := "default" count, err := b.unified.CountManagedObjects(ctx, &resourcepb.CountManagedObjectsRequest{ Namespace: ns, }) if err != nil { - return m, err + return m, fmt.Errorf("count managed objects: %w", err) } counts := make(map[string]int, 10) for _, v := range count.Items { counts[v.Kind] = counts[v.Kind] + int(v.Count) } + + span.SetAttributes(attribute.Int("totalManagedObjectsCount", len(count.Items))) for k, v := range counts { m[fmt.Sprintf("stats.managed_by.%s.count", k)] = v } @@ -39,13 +61,14 @@ func (b *APIBuilder) collectProvisioningStats(ctx context.Context) (map[string]a // Inspect all configs repos, err := b.repositoryLister.List(labels.Everything()) if err != nil { - return m, err + return m, fmt.Errorf("list repositories: %w", err) } clear(counts) for _, repo := range repos { counts[string(repo.Spec.Type)] = counts[string(repo.Spec.Type)] + 1 } + span.SetAttributes(attribute.Int("repositoryCount", len(repos))) // Count how many items of each repository type for k, v := range counts { m[fmt.Sprintf("stats.repository.%s.count", k)] = v diff --git a/pkg/registry/apis/provisioning/webhooks/mutator.go b/pkg/registry/apis/provisioning/webhooks/mutator.go new file mode 100644 index 00000000000..10ad8288503 --- /dev/null +++ b/pkg/registry/apis/provisioning/webhooks/mutator.go @@ -0,0 +1,35 @@ +package webhooks + +import ( + "context" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "k8s.io/apimachinery/pkg/runtime" +) + +func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { + return func(ctx context.Context, obj runtime.Object) error { + repo, ok := obj.(*provisioning.Repository) + if !ok { + return nil + } + + if repo.Status.Webhook == nil { + return nil + } + + if repo.Status.Webhook.Secret != "" { + secretName := repo.Name + webhookSecretSuffix + nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Status.Webhook.Secret) + if err != nil { + return err + } + repo.Status.Webhook.EncryptedSecret = nameOrValue + repo.Status.Webhook.Secret = "" + } + + return nil + } +} diff --git a/pkg/registry/apis/provisioning/webhooks/mutator_test.go b/pkg/registry/apis/provisioning/webhooks/mutator_test.go new file mode 100644 index 00000000000..07581398fdc --- /dev/null +++ b/pkg/registry/apis/provisioning/webhooks/mutator_test.go @@ -0,0 +1,157 @@ +package webhooks + +import ( + "context" + "errors" + "testing" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestMutator(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + secret string + setupMocks func(*secrets.MockRepositorySecrets) + expectedEncryptedSecret string + expectedError string + }{ + { + name: "successful secret encryption", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + Secret: "webhook-secret", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Encrypt( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + Secret: "webhook-secret", + }, + }, + }, + "test-repo-webhook-secret", + "webhook-secret", + ).Return([]byte("encrypted-webhook-secret"), nil) + }, + expectedEncryptedSecret: "encrypted-webhook-secret", + }, + { + name: "encryption error", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + Secret: "webhook-secret", + }, + }, + }, + setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Encrypt( + context.Background(), + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + Secret: "webhook-secret", + }, + }, + }, + "test-repo-webhook-secret", + "webhook-secret", + ).Return(nil, errors.New("encryption failed")) + }, + expectedError: "encryption failed", + }, + { + name: "no webhook status", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Status: provisioning.RepositoryStatus{ + Webhook: nil, + }, + }, + setupMocks: func(_ *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + { + name: "empty secret", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + Secret: "", + }, + }, + }, + setupMocks: func(_ *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + { + name: "non-repository object", + obj: &runtime.Unknown{}, + setupMocks: func(_ *secrets.MockRepositorySecrets) { + // No expectations + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMocks(mockSecrets) + + mutator := Mutator(mockSecrets) + err := mutator(context.Background(), tt.obj) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + + // Check that secret was cleared and encrypted secret was set + if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Status.Webhook != nil { + if tt.expectedEncryptedSecret != "" { + // Secret should be cleared after encryption + assert.Empty(t, repo.Status.Webhook.Secret, "Secret should be cleared after encryption") + // EncryptedSecret should be set to the expected value + assert.Equal(t, tt.expectedEncryptedSecret, string(repo.Status.Webhook.EncryptedSecret), "EncryptedSecret should match expected value") + } + } + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go index cddf0ada660..c1c09944ea9 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/blobstore_client_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest @@ -9,7 +9,7 @@ import ( mock "github.com/stretchr/testify/mock" - "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) // MockBlobStoreClient is an autogenerated mock type for the BlobStoreClient type @@ -69,7 +69,7 @@ type MockBlobStoreClient_PutBlob_Call struct { // PutBlob is a helper method to define mock.On call // - ctx context.Context -// - in *resource.PutBlobRequest +// - in *resourcepb.PutBlobRequest // - opts ...grpc.CallOption func (_e *MockBlobStoreClient_Expecter) PutBlob(ctx interface{}, in interface{}, opts ...interface{}) *MockBlobStoreClient_PutBlob_Call { return &MockBlobStoreClient_PutBlob_Call{Call: _e.mock.On("PutBlob", diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go index 785e552f68d..299a34b3fe5 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_commenter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go index 54673cd92d8..9d328127af4 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_evaluator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go index 08a6f3dff61..f8c4b9ba5a9 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/mock_pullrequest_repo.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go index f7aa5bb5039..e2a4ed6db94 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/render_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/webhooks/register.go b/pkg/registry/apis/provisioning/webhooks/register.go index ff73ad525d4..3f8d17f8964 100644 --- a/pkg/registry/apis/provisioning/webhooks/register.go +++ b/pkg/registry/apis/provisioning/webhooks/register.go @@ -9,14 +9,15 @@ import ( "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" provisioningapis "github.com/grafana/grafana/pkg/registry/apis/provisioning" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" - gogit "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/go-git" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/nanogit" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/rendering" @@ -38,8 +39,9 @@ type WebhookExtraBuilder struct { func ProvideWebhooks( cfg *setting.Cfg, features featuremgmt.FeatureToggles, - // FIXME: use multi-tenant service when one exists. In this state, we can't make this a multi-tenant service! - secretsSvc grafanasecrets.Service, + legacySecretsSvc grafanasecrets.Service, + secretsSvc *service.SecureValueService, + decryptSvc service.DecryptService, ghFactory *github.Factory, renderer rendering.Service, blobstore resource.ResourceClient, @@ -66,13 +68,13 @@ func ProvideWebhooks( evaluator := pullrequest.NewEvaluator(screenshotRenderer, parsers, urlProvider) commenter := pullrequest.NewCommenter() pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter) + repositorySecrets := secrets.NewRepositorySecrets(features, secrets.NewSecretsService(secretsSvc, decryptSvc), secrets.NewSingleTenant(legacySecretsSvc)) return NewWebhookExtra( - features, render, webhook, urlProvider, - secrets.NewSingleTenant(secretsSvc), + repositorySecrets, ghFactory, filepath.Join(cfg.DataPath, "clone"), parsers, @@ -85,11 +87,10 @@ func ProvideWebhooks( // WebhookExtra implements the Extra interface for webhooks // to wrap around type WebhookExtra struct { - features featuremgmt.FeatureToggles render *renderConnector webhook *webhookConnector urlProvider func(namespace string) string - secrets secrets.Service + secrets secrets.RepositorySecrets ghFactory *github.Factory clonedir string parsers resources.ParserFactory @@ -97,18 +98,16 @@ type WebhookExtra struct { } func NewWebhookExtra( - features featuremgmt.FeatureToggles, render *renderConnector, webhook *webhookConnector, urlProvider func(namespace string) string, - secrets secrets.Service, + secrets secrets.RepositorySecrets, ghFactory *github.Factory, clonedir string, parsers resources.ParserFactory, workers []jobs.Worker, ) *WebhookExtra { return &WebhookExtra{ - features: features, render: render, webhook: webhook, urlProvider: urlProvider, @@ -130,19 +129,11 @@ func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) ( return e.render.Authorize(ctx, a) } -// Mutate delegates mutation to the webhook connector -func (e *WebhookExtra) Mutate(ctx context.Context, r *provisioning.Repository) error { - // Encrypt webhook secret if present - if r.Status.Webhook != nil && r.Status.Webhook.Secret != "" { - encryptedSecret, err := e.secrets.Encrypt(ctx, []byte(r.Status.Webhook.Secret)) - if err != nil { - return fmt.Errorf("failed to encrypt webhook secret: %w", err) - } - r.Status.Webhook.EncryptedSecret = encryptedSecret - r.Status.Webhook.Secret = "" +// Mutators returns the mutators for the webhook extra +func (e *WebhookExtra) Mutators() []controller.Mutator { + return []controller.Mutator{ + Mutator(e.secrets), } - - return nil } // UpdateStorage updates the storage with both render and webhook connectors @@ -180,45 +171,50 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit gvr.Resource, r.GetName(), ) - cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) { - return gogit.Clone(ctx, e.clonedir, r, opts, e.secrets) - } - - apiRepo, err := repository.NewGitHub(ctx, r, e.ghFactory, e.secrets, cloneFn) - if err != nil { - return nil, fmt.Errorf("create github API repository: %w", err) - } logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path) - if !e.features.IsEnabledGlobally(featuremgmt.FlagNanoGit) { - logger.Debug("Instantiating Github repository with go-git and Github API") - return NewGithubWebhookRepository(apiRepo, webhookURL, e.secrets), nil - } - - logger.Info("Instantiating Github repository with nanogit") - + logger.Info("Instantiating Github repository with webhooks") ghCfg := r.Spec.GitHub if ghCfg == nil { return nil, fmt.Errorf("github configuration is required for nano git") } - gitCfg := nanogit.RepositoryConfig{ + // Decrypt GitHub token if needed + ghToken := ghCfg.Token + if ghToken == "" && len(ghCfg.EncryptedToken) > 0 { + decrypted, err := e.secrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken)) + if err != nil { + return nil, fmt.Errorf("decrypt github token: %w", err) + } + ghToken = string(decrypted) + } + + gitCfg := git.RepositoryConfig{ URL: ghCfg.URL, Branch: ghCfg.Branch, Path: ghCfg.Path, - Token: ghCfg.Token, + Token: ghToken, EncryptedToken: ghCfg.EncryptedToken, } - nanogitRepo, err := nanogit.NewGitRepository(ctx, e.secrets, r, gitCfg) + gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, e.secrets) if err != nil { - return nil, fmt.Errorf("error creating nanogit repository: %w", err) + return nil, fmt.Errorf("error creating git repository: %w", err) } - basicRepo := nanogit.NewGithubRepository(apiRepo, nanogitRepo) + basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken, e.secrets) + if err != nil { + return nil, fmt.Errorf("error creating github repository: %w", err) + } return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil } return nil, nil } + +func (e *WebhookExtra) RepositoryTypes() []provisioning.RepositoryType { + return []provisioning.RepositoryType{ + provisioning.GitHubRepositoryType, + } +} diff --git a/pkg/registry/apis/provisioning/webhooks/repository.go b/pkg/registry/apis/provisioning/webhooks/repository.go index c9a619e6c7e..4e08111dec5 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository.go +++ b/pkg/registry/apis/provisioning/webhooks/repository.go @@ -20,31 +20,34 @@ import ( var subscribedEvents = []string{"push", "pull_request"} +//nolint:gosec // This is a constant for a secret suffix +const webhookSecretSuffix = "-webhook-secret" + type WebhookRepository interface { Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error) } type GithubWebhookRepository interface { - repository.GithubRepository + pgh.GithubRepository repository.Hooks WebhookRepository } type githubWebhookRepository struct { - repository.GithubRepository + pgh.GithubRepository config *provisioning.Repository owner string repo string - secrets secrets.Service + secrets secrets.RepositorySecrets gh pgh.Client webhookURL string } func NewGithubWebhookRepository( - basic repository.GithubRepository, + basic pgh.GithubRepository, webhookURL string, - secrets secrets.Service, + secrets secrets.RepositorySecrets, ) GithubWebhookRepository { return &githubWebhookRepository{ GithubRepository: basic, @@ -63,7 +66,7 @@ func (r *githubWebhookRepository) Webhook(ctx context.Context, req *http.Request return nil, fmt.Errorf("unexpected webhook request") } - secret, err := r.secrets.Decrypt(ctx, r.config.Status.Webhook.EncryptedSecret) + secret, err := r.secrets.Decrypt(ctx, r.config, string(r.config.Status.Webhook.EncryptedSecret)) if err != nil { return nil, fmt.Errorf("failed to decrypt secret: %w", err) } @@ -269,6 +272,7 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo } func (r *githubWebhookRepository) deleteWebhook(ctx context.Context) error { + logger := logging.FromContext(ctx) if r.config.Status.Webhook == nil { return fmt.Errorf("webhook not found") } @@ -279,7 +283,7 @@ func (r *githubWebhookRepository) deleteWebhook(ctx context.Context) error { return fmt.Errorf("delete webhook: %w", err) } - logging.FromContext(ctx).Info("webhook deleted", "url", r.config.Status.Webhook.URL, "id", id) + logger.Info("webhook deleted", "url", r.config.Status.Webhook.URL, "id", id) return nil } @@ -332,10 +336,22 @@ func (r *githubWebhookRepository) OnUpdate(ctx context.Context) ([]map[string]in } func (r *githubWebhookRepository) OnDelete(ctx context.Context) error { - if len(r.webhookURL) == 0 { + ctx, logger := r.logger(ctx, "") + if err := r.GithubRepository.OnDelete(ctx); err != nil { + return fmt.Errorf("on delete from basic github repository: %w", err) + } + + if r.config.Status.Webhook == nil { return nil } - ctx, _ = r.logger(ctx, "") + + secretName := r.config.Name + webhookSecretSuffix + if err := r.secrets.Delete(ctx, r.config, secretName); err != nil { + return fmt.Errorf("delete webhook secret: %w", err) + } + + logger.Info("Deleted webhook secret", "secretName", secretName) + return r.deleteWebhook(ctx) } diff --git a/pkg/registry/apis/provisioning/webhooks/repository_test.go b/pkg/registry/apis/provisioning/webhooks/repository_test.go index 6c5f55bc35f..76de9f50f02 100644 --- a/pkg/registry/apis/provisioning/webhooks/repository_test.go +++ b/pkg/registry/apis/provisioning/webhooks/repository_test.go @@ -15,7 +15,7 @@ import ( "testing" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" - pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -116,7 +116,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { config *provisioning.Repository webhookSecret string setupRequest func() *http.Request - mockSetup func(t *testing.T, mockSecrets *secrets.MockService) + mockSetup func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) expected *provisioning.WebhookResponse expectedError error }{ @@ -156,8 +156,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { req, _ := http.NewRequest("POST", "/webhook", nil) return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return(nil, errors.New("decryption failed")) }, expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"), @@ -183,8 +183,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { req.Header.Set("Content-Type", "application/json") return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: apierrors.NewUnauthorized("invalid signature"), @@ -218,8 +218,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -264,8 +264,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -312,8 +312,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -358,8 +358,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("missing repository in push event"), @@ -398,8 +398,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("repository mismatch"), @@ -441,8 +441,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -497,8 +497,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -564,8 +564,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -628,8 +628,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -682,8 +682,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -733,8 +733,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("missing repository in pull request event"), @@ -782,8 +782,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("missing GitHub config"), @@ -833,8 +833,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("repository mismatch"), @@ -873,12 +873,136 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expectedError: fmt.Errorf("expected PR in event"), }, + { + name: "secret decryption error with new secrets store", + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + EncryptedSecret: []byte("test-secret"), + }, + }, + }, + setupRequest: func() *http.Request { + req, _ := http.NewRequest("POST", "/webhook", nil) + return req + }, + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). + Return(nil, errors.New("decryption failed")) + }, + expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"), + }, + { + name: "ping event with new secrets store", + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + EncryptedSecret: []byte("test-secret"), + }, + }, + }, + webhookSecret: "webhook-secret", + setupRequest: func() *http.Request { + payload := `{}` + req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) + req.Header.Set("X-GitHub-Event", "ping") + req.Header.Set("Content-Type", "application/json") + + // Create a valid signature + mac := hmac.New(sha256.New, []byte("webhook-secret")) + mac.Write([]byte(payload)) + signature := hex.EncodeToString(mac.Sum(nil)) + req.Header.Set("X-Hub-Signature-256", "sha256="+signature) + + return req + }, + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). + Return([]byte("webhook-secret"), nil) + }, + expected: &provisioning.WebhookResponse{ + Code: http.StatusOK, + Message: "ping received", + }, + }, + { + name: "push event for main branch with new secrets store", + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + Sync: provisioning.SyncOptions{ + Enabled: true, + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + EncryptedSecret: []byte("test-secret"), + }, + }, + }, + webhookSecret: "webhook-secret", + setupRequest: func() *http.Request { + payload := `{ + "ref": "refs/heads/main", + "repository": { + "full_name": "grafana/grafana" + } + }` + req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload)) + req.Header.Set("X-GitHub-Event", "push") + req.Header.Set("Content-Type", "application/json") + + // Create a valid signature + mac := hmac.New(sha256.New, []byte("webhook-secret")) + mac.Write([]byte(payload)) + signature := hex.EncodeToString(mac.Sum(nil)) + req.Header.Set("X-Hub-Signature-256", "sha256="+signature) + + return req + }, + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret"). + Return([]byte("webhook-secret"), nil) + }, + expected: &provisioning.WebhookResponse{ + Code: http.StatusAccepted, + Job: &provisioning.JobSpec{ + Repository: "test-repo", + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{ + Incremental: true, + }, + }, + }, + }, { name: "unsupported event type", config: &provisioning.Repository{ @@ -908,8 +1032,8 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) { - mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")). + mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) { + mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret"). Return([]byte("webhook-secret"), nil) }, expected: &provisioning.WebhookResponse{ @@ -922,7 +1046,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a mock secrets service - mockSecrets := secrets.NewMockService(t) + mockSecrets := secrets.NewMockRepositorySecrets(t) // Set up the mock expectations if tt.mockSetup != nil { @@ -983,14 +1107,14 @@ func TestGitHubRepository_Webhook(t *testing.T) { func TestGitHubRepository_CommentPullRequest(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) prNumber int comment string expectedError error }{ { name: "successfully comment on pull request", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 123, "Test comment"). Return(nil) }, @@ -1000,7 +1124,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) { }, { name: "error commenting on pull request", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 456, "Error comment"). Return(fmt.Errorf("failed to create comment")) }, @@ -1013,7 +1137,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock @@ -1050,7 +1174,7 @@ func TestGitHubRepository_CommentPullRequest(t *testing.T) { func TestGitHubRepository_OnCreate(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) config *provisioning.Repository webhookURL string expectedHook *provisioning.WebhookStatus @@ -1058,12 +1182,12 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }{ { name: "successfully create webhook", - setupMock: func(m *pgh.MockClient) { - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg pgh.WebhookConfig) bool { + setupMock: func(m *github.MockClient) { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg github.WebhookConfig) bool { return cfg.URL == "https://example.com/webhook" && cfg.ContentType == "json" && cfg.Active == true - })).Return(pgh.WebhookConfig{ + })).Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Secret: "test-secret", @@ -1086,7 +1210,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }, { name: "no webhook URL", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // No webhook creation expected }, config: &provisioning.Repository{ @@ -1102,9 +1226,9 @@ func TestGitHubRepository_OnCreate(t *testing.T) { }, { name: "error creating webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook")) + Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1122,7 +1246,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock @@ -1166,7 +1290,7 @@ func TestGitHubRepository_OnCreate(t *testing.T) { func TestGitHubRepository_OnUpdate(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient) config *provisioning.Repository webhookURL string expectedHook *provisioning.WebhookStatus @@ -1174,17 +1298,17 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }{ { name: "successfully update webhook when webhook exists", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock getting the existing webhook m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Events: []string{"push"}, }, nil) // Mock editing the webhook - m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.ID == 123 && hook.URL == "https://example.com/webhook-updated" && slices.Equal(hook.Events, subscribedEvents) })).Return(nil) @@ -1212,18 +1336,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "create webhook when it doesn't exist", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook not found m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound) + Return(github.WebhookConfig{}, github.ErrResourceNotFound) // Mock creating a new webhook - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.URL == "https://example.com/webhook" && hook.ContentType == "json" && slices.Equal(hook.Events, subscribedEvents) && hook.Active == true - })).Return(pgh.WebhookConfig{ + })).Return(github.WebhookConfig{ ID: 456, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1252,7 +1376,7 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "no webhook URL provided", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // No mocks needed }, config: &provisioning.Repository{}, @@ -1262,9 +1386,9 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error getting webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, fmt.Errorf("failed to get webhook")) + Return(github.WebhookConfig{}, fmt.Errorf("failed to get webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1285,10 +1409,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error editing webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock getting the existing webhook m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Events: []string{"push"}, @@ -1317,10 +1441,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "create webhook when webhook status is nil", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock creating a new webhook m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 456, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1348,10 +1472,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "create webhook when webhook ID is zero", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock creating a new webhook m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 789, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1382,10 +1506,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error when creating webhook fails", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook creation failure m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything). - Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook")) + Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1403,18 +1527,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "creates webhook when ErrResourceNotFound", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook not found m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound) + Return(github.WebhookConfig{}, github.ErrResourceNotFound) // Mock creating a new webhook - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.URL == "https://example.com/webhook" && hook.ContentType == "json" && slices.Equal(hook.Events, subscribedEvents) && hook.Active == true - })).Return(pgh.WebhookConfig{ + })).Return(github.WebhookConfig{ ID: 456, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1443,18 +1567,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "error on create when not found", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock webhook not found m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound) + Return(github.WebhookConfig{}, github.ErrResourceNotFound) // Mock error when creating a new webhook - m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool { + m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook github.WebhookConfig) bool { return hook.URL == "https://example.com/webhook" && hook.ContentType == "json" && slices.Equal(hook.Events, subscribedEvents) && hook.Active == true - })).Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook")) + })).Return(github.WebhookConfig{}, fmt.Errorf("failed to create webhook")) }, config: &provisioning.Repository{ Spec: provisioning.RepositorySpec{ @@ -1475,10 +1599,10 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { }, { name: "no update needed when URL and events match", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient) { // Mock getting the existing webhook with matching URL and events m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)). - Return(pgh.WebhookConfig{ + Return(github.WebhookConfig{ ID: 123, URL: "https://example.com/webhook", Events: subscribedEvents, @@ -1514,7 +1638,7 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) + mockGH := github.NewMockClient(t) tt.setupMock(mockGH) // Create repository with mock @@ -1563,19 +1687,24 @@ func TestGitHubRepository_OnUpdate(t *testing.T) { func TestGitHubRepository_OnDelete(t *testing.T) { tests := []struct { name string - setupMock func(m *pgh.MockClient) + setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) config *provisioning.Repository webhookURL string expectedError error }{ { name: "successfully delete webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil) // Mock deleting the webhook m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)). Return(nil) }, config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", @@ -1593,19 +1722,32 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }, { name: "no webhook URL provided", - setupMock: func(m *pgh.MockClient) { - // No mocks needed + setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, }, - config: &provisioning.Repository{}, webhookURL: "", expectedError: nil, }, { name: "webhook not found in status", - setupMock: func(m *pgh.MockClient) { - // No mocks needed + setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + // No secrets deletion or webhook deletion mocks needed - method returns early when webhook is nil }, config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", @@ -1616,16 +1758,45 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }, }, webhookURL: "https://example.com/webhook", - expectedError: fmt.Errorf("webhook not found"), + expectedError: nil, // No error expected - method returns early when webhook is nil + }, + { + name: "error on delete from basic github repository", + setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(fmt.Errorf("failed to delete webhook")) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + ID: 123, + URL: "https://example.com/webhook", + }, + }, + }, + webhookURL: "https://example.com/webhook", + expectedError: fmt.Errorf("on delete from basic github repository: failed to delete webhook"), }, { name: "error deleting webhook", - setupMock: func(m *pgh.MockClient) { + setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil) // Mock webhook deletion failure m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)). Return(fmt.Errorf("failed to delete webhook")) }, config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, Spec: provisioning.RepositorySpec{ GitHub: &provisioning.GitHubRepositoryConfig{ Branch: "main", @@ -1646,16 +1817,20 @@ func TestGitHubRepository_OnDelete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mock GitHub client - mockGH := pgh.NewMockClient(t) - tt.setupMock(mockGH) + mockGH := github.NewMockClient(t) + mockRepo := github.NewMockGithubRepository(t) + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMock(mockGH, mockRepo, mockSecrets) // Create repository with mock repo := &githubWebhookRepository{ - gh: mockGH, - config: tt.config, - owner: "grafana", - repo: "grafana", - webhookURL: tt.webhookURL, + GithubRepository: mockRepo, + gh: mockGH, + config: tt.config, + secrets: mockSecrets, + owner: "grafana", + repo: "grafana", + webhookURL: tt.webhookURL, } // Call the OnDelete method @@ -1674,3 +1849,156 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }) } } + +func TestGitHubRepository_OnDelete_WithSecrets(t *testing.T) { + tests := []struct { + name string + setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) + config *provisioning.Repository + webhookURL string + expectedError string + }{ + { + name: "successful deletion with secrets", + setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + mockSecrets.EXPECT().Delete( + mock.Anything, + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + ID: 123, + URL: "https://example.com/webhook", + }, + }, + }, + "test-repo"+webhookSecretSuffix, + ).Return(nil) + m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).Return(nil) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + ID: 123, + URL: "https://example.com/webhook", + }, + }, + }, + webhookURL: "https://example.com/webhook", + }, + { + name: "secret deletion error", + setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + mockSecrets.EXPECT().Delete( + mock.Anything, + &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + ID: 123, + URL: "https://example.com/webhook", + }, + }, + }, + "test-repo"+webhookSecretSuffix, + ).Return(errors.New("failed to delete webhook secret")) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + Status: provisioning.RepositoryStatus{ + Webhook: &provisioning.WebhookStatus{ + ID: 123, + URL: "https://example.com/webhook", + }, + }, + }, + webhookURL: "https://example.com/webhook", + expectedError: "delete webhook secret: failed to delete webhook secret", + }, + { + name: "no webhook URL - no secrets deletion", + setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) { + mockRepo.On("OnDelete", mock.Anything).Return(nil) + }, + config: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "default", + }, + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Branch: "main", + }, + }, + }, + webhookURL: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := github.NewMockClient(t) + mockRepo := github.NewMockGithubRepository(t) + mockSecrets := secrets.NewMockRepositorySecrets(t) + tt.setupMock(mockClient, mockRepo, mockSecrets) + + repo := &githubWebhookRepository{ + GithubRepository: mockRepo, + gh: mockClient, + config: tt.config, + secrets: mockSecrets, + owner: "grafana", + repo: "grafana", + webhookURL: tt.webhookURL, + } + + err := repo.OnDelete(context.Background()) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + } + + mockClient.AssertExpectations(t) + mockRepo.AssertExpectations(t) + mockSecrets.AssertExpectations(t) + }) + } +} diff --git a/pkg/registry/apis/query/client/plugin.go b/pkg/registry/apis/query/client/plugin.go index 5b4de7e546d..a13976fe6b8 100644 --- a/pkg/registry/apis/query/client/plugin.go +++ b/pkg/registry/apis/query/client/plugin.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/apis/query/clientapi" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/adapters" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -109,8 +110,34 @@ func getGrafanaDataSourceSettings(ctx context.Context) (*backend.DataSourceInsta return adapters.ModelToInstanceSettings(ds, decryptFunc) } +func (d *pluginClient) QueryData(ctx context.Context, req data.QueryDataRequest) (*clientapi.Response, error) { + // middlewares may set response-http-headers through context, so we need to do extra steps + // first we create an isolated context to be used by query-data + isolatedCtx := contexthandler.CopyWithReqContext(ctx) + + qdr, err := d.innerQueryData(isolatedCtx, req) + + if err != nil { + return nil, err + } + + rsp := &clientapi.Response{ + QDR: qdr, + Headers: nil, + } + + // we extract the response-headers from the isolated context, and return them explicitly + // in the clientapi.Response structure + reqCtx := contexthandler.FromContext(isolatedCtx) + if reqCtx != nil { + rsp.Headers = reqCtx.Resp.Header() + } + + return rsp, nil +} + // ExecuteQueryData implements QueryHelper. -func (d *pluginClient) QueryData(ctx context.Context, req data.QueryDataRequest) (*backend.QueryDataResponse, error) { +func (d *pluginClient) innerQueryData(ctx context.Context, req data.QueryDataRequest) (*backend.QueryDataResponse, error) { queries, dsRef, err := data.ToDataSourceQueries(req) if err != nil { return nil, err diff --git a/pkg/registry/apis/query/clientapi/clientapi.go b/pkg/registry/apis/query/clientapi/clientapi.go index 6046b690fa7..73f1ff129ae 100644 --- a/pkg/registry/apis/query/clientapi/clientapi.go +++ b/pkg/registry/apis/query/clientapi/clientapi.go @@ -2,14 +2,20 @@ package clientapi import ( "context" + "net/http" "github.com/grafana/grafana-plugin-sdk-go/backend" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/services/featuremgmt" ) +type Response struct { + QDR *backend.QueryDataResponse + Headers http.Header +} + type QueryDataClient interface { - QueryData(ctx context.Context, req data.QueryDataRequest) (*backend.QueryDataResponse, error) + QueryData(ctx context.Context, req data.QueryDataRequest) (*Response, error) } type InstanceConfigurationSettings struct { diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index edc3be07ac4..47dace1b27f 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -5,12 +5,14 @@ import ( "errors" "fmt" "net/http" + "slices" "strconv" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/query/clientapi" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/models" "go.opentelemetry.io/otel/attribute" @@ -179,6 +181,8 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O return } + logEmptyRefids(raw.Queries, b.log) + for i := range req.Requests { req.Requests[i].Headers = ExtractKnownHeaders(httpreq.Header) } @@ -195,10 +199,16 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O // Actually run the query (includes expressions) rsp, err := b.execute(ctx, req, instanceConfig) if err != nil { - b.log.Error("execute error", "http code", query.GetResponseCode(rsp), "err", err) - if rsp != nil { // if we have a response, we assume the err is set in the response - responder.Object(query.GetResponseCode(rsp), &query.QueryDataResponse{ - QueryDataResponse: *rsp, + // we extract the QDR, but the response may be nil + var qdr *backend.QueryDataResponse + if rsp != nil { + qdr = rsp.QDR + } + + b.log.Error("execute error", "http code", query.GetResponseCode(qdr), "err", err) + if qdr != nil { // if we have a response, we assume the err is set in the response + responder.Object(query.GetResponseCode(qdr), &query.QueryDataResponse{ + QueryDataResponse: *qdr, }) return } else { @@ -209,45 +219,80 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O } } - responder.Object(query.GetResponseCode(rsp), &query.QueryDataResponse{ - QueryDataResponse: *rsp, // wrap the backend response as a QueryDataResponse + // response headers are communicated back using the context for some reason + reqCtx := contexthandler.FromContext(ctx) + if reqCtx != nil { + mergeHeaders(reqCtx.Resp.Header(), rsp.Headers, b.log) + } + + responder.Object(query.GetResponseCode(rsp.QDR), &query.QueryDataResponse{ + QueryDataResponse: *rsp.QDR, // wrap the backend response as a QueryDataResponse }) }), nil } -func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo, instanceConfig clientapi.InstanceConfigurationSettings) (qdr *backend.QueryDataResponse, err error) { +func logEmptyRefids(queries []v0alpha1.DataQuery, logger log.Logger) { + emptyCount := 0 + + for _, q := range queries { + if q.RefID == "" { + emptyCount += 1 + } + } + + if emptyCount > 0 { + logger.Info("empty refid found", "empty_count", emptyCount, "query_count", len(queries)) + } +} + +func mergeHeaders(main http.Header, extra http.Header, l log.Logger) { + for headerName, extraValues := range extra { + mainValues := main.Values(headerName) + for _, extraV := range extraValues { + if !slices.Contains(mainValues, extraV) { + main.Add(headerName, extraV) + } else { + l.Warn("skipped duplicate response header", "header", headerName, "value", extraV) + } + } + } +} + +func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo, instanceConfig clientapi.InstanceConfigurationSettings) (*clientapi.Response, error) { + var rsp *clientapi.Response + var err error switch len(req.Requests) { case 0: b.log.Debug("executing empty query") - qdr = &backend.QueryDataResponse{} + rsp = &clientapi.Response{QDR: &backend.QueryDataResponse{}} case 1: b.log.Debug("executing single query") - qdr, err = b.handleQuerySingleDatasource(ctx, req.Requests[0], instanceConfig) + rsp, err = b.handleQuerySingleDatasource(ctx, req.Requests[0], instanceConfig) if err != nil { b.log.Debug("handleQuerySingleDatasource failed", err) } if err == nil && isSingleAlertQuery(req) { b.log.Debug("handling alert query with single query") - qdr, err = b.convertQueryFromAlerting(ctx, req.Requests[0], qdr) + rsp.QDR, err = b.convertQueryFromAlerting(ctx, req.Requests[0], rsp.QDR) if err != nil { b.log.Debug("convertQueryFromAlerting failed", "err", err) } } default: b.log.Debug("executing concurrent queries") - qdr, err = b.executeConcurrentQueries(ctx, req.Requests, instanceConfig) + rsp, err = b.executeConcurrentQueries(ctx, req.Requests, instanceConfig) if err != nil { b.log.Debug("error in executeConcurrentQueries", "err", err) } } if err != nil { b.log.Debug("error in query phase, skipping expressions", "error", err) - return qdr, err //return early here to prevent expressions from being executed if we got an error during the query phase + return rsp, err //return early here to prevent expressions from being executed if we got an error during the query phase } if len(req.Expressions) > 0 { b.log.Debug("executing expressions") - qdr, err = b.handleExpressions(ctx, req, qdr) + rsp.QDR, err = b.handleExpressions(ctx, req, rsp.QDR) if err != nil { b.log.Debug("handleExpressions failed", "err", err) } @@ -255,18 +300,18 @@ func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo, in // Remove hidden results for _, refId := range req.HideBeforeReturn { - r, ok := qdr.Responses[refId] + r, ok := rsp.QDR.Responses[refId] if ok && r.Error == nil { - delete(qdr.Responses, refId) + delete(rsp.QDR.Responses, refId) } } - return qdr, err + return rsp, err } // Process a single request // See: https://github.com/grafana/grafana/blob/v10.2.3/pkg/services/query/query.go#L242 -func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req datasourceRequest, instanceConfig clientapi.InstanceConfigurationSettings) (*backend.QueryDataResponse, error) { +func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req datasourceRequest, instanceConfig clientapi.InstanceConfigurationSettings) (*clientapi.Response, error) { ctx, span := b.tracer.Start(ctx, "Query.handleQuerySingleDatasource") defer span.End() span.SetAttributes( @@ -282,7 +327,7 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d } } if allHidden { - return &backend.QueryDataResponse{}, nil + return &clientapi.Response{}, nil } client, err := b.clientSupplier.GetDataSourceClient( @@ -305,14 +350,14 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d if err == nil && rsp != nil { for _, q := range req.Request.Queries { if q.ResultAssertions != nil { - result, ok := rsp.Responses[q.RefID] + result, ok := rsp.QDR.Responses[q.RefID] if ok && result.Error == nil { err = q.ResultAssertions.Validate(result.Frames) if err != nil { b.log.Error("Validate failed", "err", err) result.Error = err result.ErrorSource = backend.ErrorSourceDownstream - rsp.Responses[q.RefID] = result + rsp.QDR.Responses[q.RefID] = result } } } @@ -328,24 +373,24 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d } // buildErrorResponses applies the provided error to each query response in the list. These queries should all belong to the same datasource. -func buildErrorResponse(err error, req datasourceRequest) *backend.QueryDataResponse { +func buildErrorResponse(err error, req datasourceRequest) *clientapi.Response { rsp := backend.NewQueryDataResponse() for _, query := range req.Request.Queries { rsp.Responses[query.RefID] = backend.DataResponse{ Error: err, } } - return rsp + return &clientapi.Response{QDR: rsp, Headers: nil} } // executeConcurrentQueries executes queries to multiple datasources concurrently and returns the aggregate result. -func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests []datasourceRequest, instanceConfig clientapi.InstanceConfigurationSettings) (*backend.QueryDataResponse, error) { +func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests []datasourceRequest, instanceConfig clientapi.InstanceConfigurationSettings) (*clientapi.Response, error) { ctx, span := b.tracer.Start(ctx, "Query.executeConcurrentQueries") defer span.End() g, ctx := errgroup.WithContext(ctx) g.SetLimit(b.concurrentQueryLimit) // prevent too many concurrent requests - rchan := make(chan *backend.QueryDataResponse, len(requests)) + rchan := make(chan *clientapi.Response, len(requests)) // Create panic recovery function for loop below recoveryFn := func(req datasourceRequest) { @@ -370,9 +415,9 @@ func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests g.Go(func() error { defer recoveryFn(req) - dqr, err := b.handleQuerySingleDatasource(ctx, req, instanceConfig) + rsp, err := b.handleQuerySingleDatasource(ctx, req, instanceConfig) if err == nil { - rchan <- dqr + rchan <- rsp } else { rchan <- buildErrorResponse(err, req) } @@ -386,14 +431,19 @@ func (b *QueryAPIBuilder) executeConcurrentQueries(ctx context.Context, requests close(rchan) // Merge the results from each response - resp := backend.NewQueryDataResponse() + rsp := &clientapi.Response{ + QDR: backend.NewQueryDataResponse(), + Headers: http.Header{}, + } for result := range rchan { - for refId, dataResponse := range result.Responses { - resp.Responses[refId] = dataResponse + for refId, dataResponse := range result.QDR.Responses { + rsp.QDR.Responses[refId] = dataResponse } + + mergeHeaders(rsp.Headers, result.Headers, b.log) } - return resp, nil + return rsp, nil } // Unlike the implementation in expr/node.go, all datasource queries have been processed first diff --git a/pkg/registry/apis/query/query_test.go b/pkg/registry/apis/query/query_test.go index c429cc80e56..f6c6b631e85 100644 --- a/pkg/registry/apis/query/query_test.go +++ b/pkg/registry/apis/query/query_test.go @@ -173,3 +173,111 @@ func (m mockClient) CheckHealth(ctx context.Context, req *backend.CheckHealthReq func (m mockClient) GetInstanceConfigurationSettings(_ context.Context) (clientapi.InstanceConfigurationSettings, error) { return clientapi.InstanceConfigurationSettings{}, nil } + +func TestMergeHeaders(t *testing.T) { + tests := []struct { + name string + h1 http.Header + h2 http.Header + expected http.Header + }{ + { + name: "into empty", + h1: http.Header{}, + h2: http.Header{ + "A": {"1", "2"}, + "B": {"3"}, + }, + expected: http.Header{ + "A": {"1", "2"}, + "B": {"3"}, + }, + }, + { + name: "from empty", + h1: http.Header{ + "A": {"1", "2"}, + "B": {"3"}, + }, + h2: http.Header{}, + expected: http.Header{ + "A": {"1", "2"}, + "B": {"3"}, + }, + }, + { + name: "from nil", + h1: http.Header{ + "A": {"1", "2"}, + "B": {"3"}, + }, + h2: nil, + expected: http.Header{ + "A": {"1", "2"}, + "B": {"3"}, + }, + }, + { + name: "no merging", + h1: http.Header{ + "A": {"1", "2"}, + }, + h2: http.Header{ + "B": {"3", "4"}, + }, + expected: http.Header{ + "A": {"1", "2"}, + "B": {"3", "4"}, + }, + }, + { + name: "with merging", + h1: http.Header{ + "A": {"1", "2"}, + }, + h2: http.Header{ + "A": {"3", "4"}, + }, + expected: http.Header{ + "A": {"1", "2", "3", "4"}, + }, + }, + { + name: "with duplicates", + h1: http.Header{ + "A": {"1", "2"}, + }, + h2: http.Header{ + "A": {"2", "3"}, + }, + expected: http.Header{ + "A": {"1", "2", "3"}, + }, + }, + { + name: "with all", + h1: http.Header{ + "A": {"1", "2", "3"}, + "B": {"4"}, + }, + h2: http.Header{ + "A": {"3", "4"}, + "B": {"5"}, + "C": {"6"}, + }, + expected: http.Header{ + "A": {"1", "2", "3", "4"}, + "B": {"4", "5"}, + "C": {"6"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h1 := tt.h1.Clone() // don't mutate the test-data + mergeHeaders(h1, tt.h2, log.New("test.logger")) + require.Equal(t, tt.expected, h1) + }) + } +} diff --git a/pkg/registry/apis/secret/contracts/decrypt.go b/pkg/registry/apis/secret/contracts/decrypt.go index 73ff2248e7a..13c28b1025c 100644 --- a/pkg/registry/apis/secret/contracts/decrypt.go +++ b/pkg/registry/apis/secret/contracts/decrypt.go @@ -4,7 +4,7 @@ import ( "context" "errors" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) @@ -16,13 +16,10 @@ var ( // DecryptStorage is the interface for wiring and dependency injection. type DecryptStorage interface { - Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv0alpha1.ExposedSecureValue, error) + Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv1beta1.ExposedSecureValue, error) } // DecryptAuthorizer is the interface for authorizing decryption requests. type DecryptAuthorizer interface { Authorize(ctx context.Context, secureValueName string, secureValueDecrypters []string) (identity string, allowed bool) } - -// TEMPORARY: Needed to pass it with wire. -type DecryptAllowList map[string]struct{} diff --git a/pkg/registry/apis/secret/contracts/encryption.go b/pkg/registry/apis/secret/contracts/encryption.go index 176fb8ace24..eb5292ee794 100644 --- a/pkg/registry/apis/secret/contracts/encryption.go +++ b/pkg/registry/apis/secret/contracts/encryption.go @@ -13,16 +13,17 @@ type EncryptionManager interface { } type EncryptedValue struct { - UID string Namespace string + Name string + Version int64 EncryptedData []byte Created int64 Updated int64 } type EncryptedValueStorage interface { - Create(ctx context.Context, namespace string, encryptedData []byte) (*EncryptedValue, error) - Update(ctx context.Context, namespace string, uid string, encryptedData []byte) error - Get(ctx context.Context, namespace string, uid string) (*EncryptedValue, error) - Delete(ctx context.Context, namespace string, uid string) error + Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (*EncryptedValue, error) + Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error + Get(ctx context.Context, namespace, name string, version int64) (*EncryptedValue, error) + Delete(ctx context.Context, namespace, name string, version int64) error } diff --git a/pkg/registry/apis/secret/contracts/keeper.go b/pkg/registry/apis/secret/contracts/keeper.go index c7f0482e54d..48db9a13dc0 100644 --- a/pkg/registry/apis/secret/contracts/keeper.go +++ b/pkg/registry/apis/secret/contracts/keeper.go @@ -4,7 +4,7 @@ import ( "context" "errors" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -15,12 +15,12 @@ var ( // KeeperMetadataStorage is the interface for wiring and dependency injection. type KeeperMetadataStorage interface { - Create(ctx context.Context, keeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) - Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv0alpha1.Keeper, error) - Update(ctx context.Context, keeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) + Create(ctx context.Context, keeper *secretv1beta1.Keeper, actorUID string) (*secretv1beta1.Keeper, error) + Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv1beta1.Keeper, error) + Update(ctx context.Context, keeper *secretv1beta1.Keeper, actorUID string) (*secretv1beta1.Keeper, error) Delete(ctx context.Context, namespace xkube.Namespace, name string) error - List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.Keeper, error) - GetKeeperConfig(ctx context.Context, namespace string, name *string, opts ReadOpts) (secretv0alpha1.KeeperConfig, error) + List(ctx context.Context, namespace xkube.Namespace) ([]secretv1beta1.Keeper, error) + GetKeeperConfig(ctx context.Context, namespace string, name *string, opts ReadOpts) (secretv1beta1.KeeperConfig, error) } // ErrKeeperInvalidSecureValues is returned when a Keeper references SecureValues that do not exist. @@ -95,14 +95,14 @@ func (s ExternalID) String() string { // Keeper is the interface for secret keepers. type Keeper interface { - Store(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, exposedValueOrRef string) (ExternalID, error) - Update(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID ExternalID, exposedValueOrRef string) error - Expose(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID ExternalID) (secretv0alpha1.ExposedSecureValue, error) - Delete(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID ExternalID) error + Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (ExternalID, error) + Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error + Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) + Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error } // Service is the interface for secret keeper services. // This exists because OSS and Enterprise have different amounts of keepers available. type KeeperService interface { - KeeperForConfig(secretv0alpha1.KeeperConfig) (Keeper, error) + KeeperForConfig(secretv1beta1.KeeperConfig) (Keeper, error) } diff --git a/pkg/registry/apis/secret/contracts/outbox_queue.go b/pkg/registry/apis/secret/contracts/outbox_queue.go deleted file mode 100644 index 767273b1254..00000000000 --- a/pkg/registry/apis/secret/contracts/outbox_queue.go +++ /dev/null @@ -1,64 +0,0 @@ -package contracts - -import ( - "context" -) - -type contextRequestIdKey struct{} - -type OutboxMessageType string - -func GetRequestId(ctx context.Context) string { - v := ctx.Value(contextRequestIdKey{}) - requestId, ok := v.(string) - if !ok { - return "" - } - - return requestId -} - -func ContextWithRequestID(ctx context.Context, requestId string) context.Context { - return context.WithValue(ctx, contextRequestIdKey{}, requestId) -} - -const ( - CreateSecretOutboxMessage OutboxMessageType = "create" - UpdateSecretOutboxMessage OutboxMessageType = "update" - DeleteSecretOutboxMessage OutboxMessageType = "delete" -) - -type AppendOutboxMessage struct { - RequestID string - Type OutboxMessageType - Name string - Namespace string - EncryptedSecret string - KeeperName *string - ExternalID *string -} - -type OutboxMessage struct { - RequestID string - Type OutboxMessageType - MessageID int64 - Name string - Namespace string - EncryptedSecret string - KeeperName *string - ExternalID *string - // How many times this message has been received - ReceiveCount int - Created int64 -} - -type OutboxQueue interface { - // Appends a message to the outbox queue - Append(ctx context.Context, message AppendOutboxMessage) (int64, error) - // Receives at most n messages from the outbox queue - ReceiveN(ctx context.Context, n uint) ([]OutboxMessage, error) - // Deletes a message from the outbox queue - Delete(ctx context.Context, messageID int64) error - // Increments the number of times each message has been received by 1. Must be atomic. - IncrementReceiveCount(ctx context.Context, messageIDs []int64) error -} diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index ca3f66f991d..33637fb2cba 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -4,7 +4,7 @@ import ( "context" "errors" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) @@ -30,12 +30,19 @@ type ReadOpts struct { // SecureValueMetadataStorage is the interface for wiring and dependency injection. type SecureValueMetadataStorage interface { - Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) - Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv0alpha1.SecureValue, error) - Update(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) - Delete(ctx context.Context, namespace xkube.Namespace, name string) error - List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.SecureValue, error) - SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error - SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID ExternalID) error + Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) + Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv1beta1.SecureValue, error) + List(ctx context.Context, namespace xkube.Namespace) ([]secretv1beta1.SecureValue, error) + SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error + SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error + SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID ExternalID) error ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*DecryptSecureValue, error) } + +type SecureValueService interface { + Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) + Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) + List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error) + Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) + Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) +} diff --git a/pkg/registry/apis/secret/contracts/validator.go b/pkg/registry/apis/secret/contracts/validator.go new file mode 100644 index 00000000000..a0f326ab268 --- /dev/null +++ b/pkg/registry/apis/secret/contracts/validator.go @@ -0,0 +1,16 @@ +package contracts + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" +) + +type SecureValueValidator interface { + Validate(sv *secretv1beta1.SecureValue, oldSv *secretv1beta1.SecureValue, operation admission.Operation) field.ErrorList +} + +type KeeperValidator interface { + Validate(keeper *secretv1beta1.Keeper, oldKeeper *secretv1beta1.Keeper, operation admission.Operation) field.ErrorList +} diff --git a/pkg/registry/apis/secret/decrypt/allow_list.go b/pkg/registry/apis/secret/decrypt/allow_list.go deleted file mode 100644 index 9f3e34ec71a..00000000000 --- a/pkg/registry/apis/secret/decrypt/allow_list.go +++ /dev/null @@ -1,8 +0,0 @@ -package decrypt - -import "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - -// TEMPORARY -func ProvideDecryptAllowList() contracts.DecryptAllowList { - return make(map[string]struct{}, 0) -} diff --git a/pkg/registry/apis/secret/decrypt/authorizer.go b/pkg/registry/apis/secret/decrypt/authorizer.go index 6350e5f80af..042ec12e0ab 100644 --- a/pkg/registry/apis/secret/decrypt/authorizer.go +++ b/pkg/registry/apis/secret/decrypt/authorizer.go @@ -9,20 +9,18 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" ) // decryptAuthorizer is the authorizer implementation for decrypt operations. type decryptAuthorizer struct { - tracer trace.Tracer - allowList contracts.DecryptAllowList + tracer trace.Tracer } -func ProvideDecryptAuthorizer(tracer trace.Tracer, allowList contracts.DecryptAllowList) contracts.DecryptAuthorizer { +func ProvideDecryptAuthorizer(tracer trace.Tracer) contracts.DecryptAuthorizer { return &decryptAuthorizer{ - tracer: tracer, - allowList: allowList, + tracer: tracer, } } @@ -56,13 +54,9 @@ func (a *decryptAuthorizer) Authorize(ctx context.Context, secureValueName strin return "", false } - serviceIdentity := serviceIdentityList[0] - - // TEMPORARY: while we can't onboard every app into secrets, we can block them from decrypting - // securevalues preemptively here before even reaching out to the database. - // This check can be removed once we open the gates for any service to use secrets. - if _, exists := a.allowList[serviceIdentity]; !exists || serviceIdentity == "" { - return serviceIdentity, false + serviceIdentity := strings.TrimSpace(serviceIdentityList[0]) + if len(serviceIdentity) == 0 { + return "", false } // Checks whether the token has the permission to decrypt secure values. @@ -86,8 +80,8 @@ func (a *decryptAuthorizer) Authorize(ctx context.Context, secureValueName strin // Changes: 1) we don't support `*` for verbs; 2) we support specific names in the permission. func hasPermissionInToken(tokenPermissions []string, name string) bool { var ( - group = secretv0alpha1.GROUP - resource = secretv0alpha1.SecureValuesResourceInfo.GetName() + group = secretv1beta1.APIGroup + resource = secretv1beta1.SecureValuesResourceInfo.GetName() verb = "decrypt" ) diff --git a/pkg/registry/apis/secret/decrypt/authorizer_test.go b/pkg/registry/apis/secret/decrypt/authorizer_test.go index 3e4cd59ef31..131ec33fb8b 100644 --- a/pkg/registry/apis/secret/decrypt/authorizer_test.go +++ b/pkg/registry/apis/secret/decrypt/authorizer_test.go @@ -17,7 +17,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when no auth info is present, it returns false", func(t *testing.T) { ctx := context.Background() - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", nil) require.Empty(t, identity) @@ -26,7 +26,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when token permissions are empty, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{}) - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", nil) require.NotEmpty(t, identity) @@ -35,7 +35,16 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when service identity is empty, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "", []string{}) - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) + + identity, allowed := authorizer.Authorize(ctx, "", nil) + require.Empty(t, identity) + require.False(t, allowed) + }) + + t.Run("when service identity is empty string, it returns false", func(t *testing.T) { + ctx := createAuthContext(context.Background(), " ", []string{}) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", nil) require.Empty(t, identity) @@ -43,7 +52,7 @@ func TestDecryptAuthorizer(t *testing.T) { }) t.Run("when permission format is malformed (missing verb), it returns false", func(t *testing.T) { - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) // nameless ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues"}) @@ -59,7 +68,7 @@ func TestDecryptAuthorizer(t *testing.T) { }) t.Run("when permission verb is not exactly `decrypt`, it returns false", func(t *testing.T) { - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) // nameless ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:*"}) @@ -76,7 +85,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when permission does not have 2 or 3 parts, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", nil) require.NotEmpty(t, identity) @@ -85,7 +94,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when permission has group that is not `secret.grafana.app`, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{"wrong.group/securevalues/invalid:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", nil) require.NotEmpty(t, identity) @@ -93,7 +102,7 @@ func TestDecryptAuthorizer(t *testing.T) { }) t.Run("when permission has resource that is not `securevalues`, it returns false", func(t *testing.T) { - authorizer := ProvideDecryptAuthorizer(tracer, nil) + authorizer := ProvideDecryptAuthorizer(tracer) // nameless ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/invalid-resource:decrypt"}) @@ -108,17 +117,17 @@ func TestDecryptAuthorizer(t *testing.T) { require.False(t, allowed) }) - t.Run("when the identity is not in the allow list, it returns false", func(t *testing.T) { + t.Run("when the allow list is empty, it allows all identities", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"allowed1": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) - identity, allowed := authorizer.Authorize(ctx, "", nil) + identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"}) require.NotEmpty(t, identity) - require.False(t, allowed) + require.True(t, allowed) }) t.Run("when the identity doesn't match any allowed decrypters, it returns false", func(t *testing.T) { - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) // nameless ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"}) @@ -134,7 +143,7 @@ func TestDecryptAuthorizer(t *testing.T) { }) t.Run("when the identity matches an allowed decrypter, it returns true", func(t *testing.T) { - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) // nameless ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"}) @@ -157,7 +166,7 @@ func TestDecryptAuthorizer(t *testing.T) { "wrong.group/securevalues/group2:decrypt", "secret.grafana.app/securevalues/identity:decrypt", // old style of identity+permission }) - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "name1", []string{"identity"}) require.True(t, allowed) @@ -170,7 +179,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when empty secure value name with specific permission, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/name:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"}) require.Equal(t, "identity", identity) @@ -179,7 +188,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when permission has an extra / but no name, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues/:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"}) require.Equal(t, "identity", identity) @@ -188,7 +197,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when the decrypters list is empty, meaning nothing can decrypt the secure value, it returns false", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity", []string{"secret.grafana.app/securevalues:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "name", []string{}) require.Equal(t, "identity", identity) @@ -197,7 +206,7 @@ func TestDecryptAuthorizer(t *testing.T) { t.Run("when one of decrypters matches the identity, it returns true", func(t *testing.T) { ctx := createAuthContext(context.Background(), "identity1", []string{"secret.grafana.app/securevalues:decrypt"}) - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity1": {}, "identity2": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) identity, allowed := authorizer.Authorize(ctx, "", []string{"identity1", "identity2", "identity3"}) require.Equal(t, "identity1", identity) @@ -205,7 +214,7 @@ func TestDecryptAuthorizer(t *testing.T) { }) t.Run("permissions must be case-sensitive and return false", func(t *testing.T) { - authorizer := ProvideDecryptAuthorizer(tracer, map[string]struct{}{"identity": {}}) + authorizer := ProvideDecryptAuthorizer(tracer) ctx := createAuthContext(context.Background(), "identity", []string{"SECRET.grafana.app/securevalues:decrypt"}) identity, allowed := authorizer.Authorize(ctx, "", []string{"identity"}) diff --git a/pkg/registry/apis/secret/decrypt/service.go b/pkg/registry/apis/secret/decrypt/service.go index 115c073aee2..d35fb6eb2e9 100644 --- a/pkg/registry/apis/secret/decrypt/service.go +++ b/pkg/registry/apis/secret/decrypt/service.go @@ -14,7 +14,7 @@ type OSSDecryptService struct { var _ service.DecryptService = &OSSDecryptService{} -func ProvideDecryptService(decryptStore contracts.DecryptStorage) *OSSDecryptService { +func ProvideDecryptService(decryptStore contracts.DecryptStorage) service.DecryptService { return &OSSDecryptService{ decryptStore: decryptStore, } diff --git a/pkg/registry/apis/secret/decrypt/service_test.go b/pkg/registry/apis/secret/decrypt/service_test.go index 665a6730527..0aaac869a67 100644 --- a/pkg/registry/apis/secret/decrypt/service_test.go +++ b/pkg/registry/apis/secret/decrypt/service_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/stretchr/testify/mock" @@ -22,7 +22,7 @@ func TestDecryptService(t *testing.T) { mockErr := errors.New("mock error") mockStorage := &MockDecryptStorage{} - mockStorage.On("Decrypt", mock.Anything, mock.Anything, mock.Anything).Return(secretv0alpha1.ExposedSecureValue(""), mockErr) + mockStorage.On("Decrypt", mock.Anything, mock.Anything, mock.Anything).Return(secretv1beta1.ExposedSecureValue(""), mockErr) decryptedValuesResp := map[string]service.DecryptResult{ "secure-value-1": service.NewDecryptResultErr(mockErr), } @@ -42,8 +42,8 @@ func TestDecryptService(t *testing.T) { mockStorage := &MockDecryptStorage{} // Set up the mock to return a different value for each name in the test - exposedSecureValue1 := secretv0alpha1.NewExposedSecureValue("value1") - exposedSecureValue2 := secretv0alpha1.NewExposedSecureValue("value2") + exposedSecureValue1 := secretv1beta1.NewExposedSecureValue("value1") + exposedSecureValue2 := secretv1beta1.NewExposedSecureValue("value2") mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). Return(exposedSecureValue1, nil) mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). @@ -69,11 +69,11 @@ func TestDecryptService(t *testing.T) { mockErr := errors.New("mock error") mockStorage := &MockDecryptStorage{} - exposedSecureValue := secretv0alpha1.NewExposedSecureValue("value") + exposedSecureValue := secretv1beta1.NewExposedSecureValue("value") mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-1"). Return(exposedSecureValue, nil) mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). - Return(secretv0alpha1.ExposedSecureValue(""), mockErr) + Return(secretv1beta1.ExposedSecureValue(""), mockErr) decryptedValuesResp := map[string]service.DecryptResult{ "secure-value-1": service.NewDecryptResultValue(&exposedSecureValue), @@ -95,7 +95,7 @@ type MockDecryptStorage struct { mock.Mock } -func (m *MockDecryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv0alpha1.ExposedSecureValue, error) { +func (m *MockDecryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (secretv1beta1.ExposedSecureValue, error) { args := m.Called(ctx, namespace, name) - return args.Get(0).(secretv0alpha1.ExposedSecureValue), args.Error(1) + return args.Get(0).(secretv1beta1.ExposedSecureValue), args.Error(1) } diff --git a/pkg/registry/apis/secret/encryption/cipher/service/service.go b/pkg/registry/apis/secret/encryption/cipher/service/service.go index 83d140da1a9..48ebef7e3a5 100644 --- a/pkg/registry/apis/secret/encryption/cipher/service/service.go +++ b/pkg/registry/apis/secret/encryption/cipher/service/service.go @@ -14,20 +14,16 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider" - "github.com/grafana/grafana/pkg/setting" ) const ( encryptionAlgorithmDelimiter = '*' ) -// Service must not be used for cipher. -// Use secrets.Service implementing envelope encryption instead. -type Service struct { +type cipherService struct { tracer trace.Tracer log log.Logger - cfg *setting.Cfg usageMetrics usagestats.Service cipher cipher.Encrypter @@ -35,16 +31,13 @@ type Service struct { algorithm string } -func NewEncryptionService( +// ProvideAESGCMCipherService provides an AES-GCM cipher for encryption and decryption. +// It should not be used to encrypt payloads directly, as it is intended to encrypt data keys for envelope encryption. +func ProvideAESGCMCipherService( tracer trace.Tracer, usageMetrics usagestats.Service, - cfg *setting.Cfg, -) (*Service, error) { - if cfg.SecretsManagement.SecretKey == "" { - return nil, fmt.Errorf("`[secrets_manager]secret_key` is not set") - } - - s := &Service{ +) (cipher.Cipher, error) { + s := &cipherService{ tracer: tracer, log: log.New("encryption"), @@ -55,7 +48,6 @@ func NewEncryptionService( algorithm: provider.AesGcm, usageMetrics: usageMetrics, - cfg: cfg, } s.registerUsageMetrics() @@ -63,7 +55,7 @@ func NewEncryptionService( return s, nil } -func (s *Service) registerUsageMetrics() { +func (s *cipherService) registerUsageMetrics() { s.usageMetrics.RegisterMetricsFunc(func(context.Context) (map[string]any, error) { return map[string]any{ fmt.Sprintf("stats.%s.encryption.cipher.%s.count", encryption.UsageInsightsPrefix, s.algorithm): 1, @@ -71,7 +63,7 @@ func (s *Service) registerUsageMetrics() { }) } -func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) { +func (s *cipherService) Decrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "CipherService.Decrypt") defer span.End() @@ -99,7 +91,7 @@ func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([ return decrypted, err } -func (s *Service) deriveEncryptionAlgorithm(payload []byte) (string, []byte, error) { +func (s *cipherService) deriveEncryptionAlgorithm(payload []byte) (string, []byte, error) { if len(payload) == 0 { return "", nil, fmt.Errorf("unable to derive encryption algorithm") } @@ -120,7 +112,7 @@ func (s *Service) deriveEncryptionAlgorithm(payload []byte) (string, []byte, err return string(algorithm), payload, nil } -func (s *Service) Encrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) { +func (s *cipherService) Encrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) { ctx, span := s.tracer.Start(ctx, "CipherService.Encrypt") defer span.End() diff --git a/pkg/registry/apis/secret/encryption/cipher/service/service_test.go b/pkg/registry/apis/secret/encryption/cipher/service/service_test.go index 62a65ea0dbc..dd24e7927c6 100644 --- a/pkg/registry/apis/secret/encryption/cipher/service/service_test.go +++ b/pkg/registry/apis/secret/encryption/cipher/service/service_test.go @@ -8,21 +8,14 @@ import ( "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" ) -func newGcmService(t *testing.T) *Service { +func newGcmService(t *testing.T) cipher.Cipher { t.Helper() usageStats := &usagestats.UsageStatsMock{} - settings := &setting.Cfg{ - SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: "SdlklWklckeLS", - EncryptionProvider: "secretKey.v1", - }, - } - - svc, err := NewEncryptionService(noop.NewTracerProvider().Tracer("test"), usageStats, settings) + svc, err := ProvideAESGCMCipherService(noop.NewTracerProvider().Tracer("test"), usageStats) require.NoError(t, err, "failed to set up encryption service") return svc } diff --git a/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider/grafana_provider.go b/pkg/registry/apis/secret/encryption/kmsproviders/grafana_provider.go similarity index 51% rename from pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider/grafana_provider.go rename to pkg/registry/apis/secret/encryption/kmsproviders/grafana_provider.go index 067d455bfb6..e27fed9da07 100644 --- a/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider/grafana_provider.go +++ b/pkg/registry/apis/secret/encryption/kmsproviders/grafana_provider.go @@ -1,4 +1,4 @@ -package defaultprovider +package kmsproviders import ( "context" @@ -7,22 +7,22 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" ) -type grafanaProvider struct { +type secretKeyProvider struct { sk string encryption cipher.Cipher } -func New(sk string, encryption cipher.Cipher) encryption.Provider { - return grafanaProvider{ +func newSecretKeyProvider(sk string, encryption cipher.Cipher) encryption.Provider { + return secretKeyProvider{ sk: sk, encryption: encryption, } } -func (p grafanaProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) { +func (p secretKeyProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) { return p.encryption.Encrypt(ctx, blob, p.sk) } -func (p grafanaProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) { +func (p secretKeyProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) { return p.encryption.Decrypt(ctx, blob, p.sk) } diff --git a/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go b/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go index 0797d784c8e..d88fd353ab6 100644 --- a/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go +++ b/pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go @@ -1,19 +1,40 @@ package kmsproviders import ( + "fmt" + "strings" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider" "github.com/grafana/grafana/pkg/setting" ) const ( - // Default is the identifier of the default kms provider which fallbacks to the configured secret_key - Default = "secretKey.v1" + // OSSProviderType is the identifier of the default kms provider which fallbacks to the configured secret_key + OSSProviderType = "secret_key" + // SecretKeyKey is the key in the section that contains the secret key + SecretKeyKey = "secret_key" ) -func GetOSSKMSProviders(cfg *setting.Cfg, enc cipher.Cipher) encryption.ProviderMap { - return encryption.ProviderMap{ - Default: defaultprovider.New(cfg.SecretsManagement.SecretKey, enc), +// ProvideOSSKMSProviders provides the ProviderConfig expected by the encryption manager in the OSS wire configuration. +// It looks for all configured 'secret_key' sections and creates a separate provider for each, each with its own secret key, allowing users to upgrade their secret key without breaking existing secrets. +func ProvideOSSKMSProviders(cfg *setting.Cfg, cipher cipher.Cipher) (encryption.ProviderConfig, error) { + pCfg := encryption.ProviderConfig{ + CurrentProvider: encryption.ProviderID(cfg.SecretsManagement.CurrentEncryptionProvider), + AvailableProviders: make(encryption.ProviderMap), } + + // Look through the available secret_key providers and add them to the map + for providerName, properties := range cfg.SecretsManagement.ConfiguredKMSProviders { + if strings.HasPrefix(providerName, OSSProviderType) { + secretKey := properties[SecretKeyKey] + if secretKey != "" { + pCfg.AvailableProviders[encryption.ProviderID(providerName)] = newSecretKeyProvider(secretKey, cipher) + } else { + return pCfg, fmt.Errorf("missing secret_key for provider %s", providerName) + } + } + } + + return pCfg, nil } diff --git a/pkg/registry/apis/secret/encryption/manager/manager.go b/pkg/registry/apis/secret/encryption/manager/manager.go index 1c596a3c70f..f48b350c1bd 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager.go +++ b/pkg/registry/apis/secret/encryption/manager/manager.go @@ -20,9 +20,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -33,16 +30,14 @@ const ( type EncryptionManager struct { tracer trace.Tracer store contracts.DataKeyStorage - enc cipher.Cipher - cfg *setting.Cfg usageStats usagestats.Service mtx sync.Mutex - pOnce sync.Once - providers encryption.ProviderMap - - currentProviderID encryption.ProviderID + // The cipher is used to encrypt and decrypt payloads with a data key. + cipher cipher.Cipher + // The providerConfig are used to encrypt and decrypt the data keys. + providerConfig encryption.ProviderConfig log log.Logger } @@ -51,33 +46,22 @@ type EncryptionManager struct { func ProvideEncryptionManager( tracer trace.Tracer, store contracts.DataKeyStorage, - cfg *setting.Cfg, usageStats usagestats.Service, - thirdPartyKMS encryption.ProviderMap, + enc cipher.Cipher, + providerConfig encryption.ProviderConfig, ) (contracts.EncryptionManager, error) { - currentProviderID := encryption.ProviderID(cfg.SecretsManagement.EncryptionProvider) - - enc, err := service.NewEncryptionService(tracer, usageStats, cfg) - if err != nil { - return nil, fmt.Errorf("failed to create encryption service: %w", err) + currentProviderID := providerConfig.CurrentProvider + if _, ok := providerConfig.AvailableProviders[currentProviderID]; !ok { + return nil, fmt.Errorf("missing configuration for current encryption provider %s", currentProviderID) } s := &EncryptionManager{ - tracer: tracer, - store: store, - cfg: cfg, - usageStats: usageStats, - enc: enc, - currentProviderID: currentProviderID, - log: log.New("encryption"), - } - - if err := s.InitProviders(thirdPartyKMS); err != nil { - return nil, err - } - - if _, ok := s.providers[currentProviderID]; !ok { - return nil, fmt.Errorf("missing configuration for current encryption provider %s", currentProviderID) + tracer: tracer, + store: store, + usageStats: usageStats, + cipher: enc, + log: log.New("encryption"), + providerConfig: providerConfig, } s.registerUsageMetrics() @@ -85,44 +69,20 @@ func ProvideEncryptionManager( return s, nil } -func (s *EncryptionManager) InitProviders(extraProviders encryption.ProviderMap) (err error) { - done := false - s.pOnce.Do(func() { - providers := kmsproviders.GetOSSKMSProviders(s.cfg, s.enc) - - for id, p := range extraProviders { - if _, exists := s.providers[id]; exists { - err = fmt.Errorf("provider %s already registered", id) - return - } - providers[id] = p - } - - s.providers = providers - done = true - }) - - if !done && err == nil { - err = fmt.Errorf("providers were already initialized, no action taken") - } - - return -} - func (s *EncryptionManager) registerUsageMetrics() { s.usageStats.RegisterMetricsFunc(func(ctx context.Context) (map[string]any, error) { usageMetrics := make(map[string]any) // Current provider - kind, err := s.currentProviderID.Kind() + kind, err := s.providerConfig.CurrentProvider.Kind() if err != nil { return nil, fmt.Errorf("encryptionManager.registerUsageMetrics: %w", err) } usageMetrics[fmt.Sprintf("stats.%s.encryption.current_provider.%s.count", encryption.UsageInsightsPrefix, kind)] = 1 // Count by kind - countByKind := make(map[string]int, len(s.providers)) - for id := range s.providers { + countByKind := make(map[string]int, len(s.providerConfig.AvailableProviders)) + for id := range s.providerConfig.AvailableProviders { kind, err := id.Kind() if err != nil { return nil, fmt.Errorf("encryptionManager.registerUsageMetrics: %w", err) @@ -161,7 +121,7 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo } }() - label := encryption.KeyLabel(s.currentProviderID) + label := encryption.KeyLabel(s.providerConfig.CurrentProvider) var id string var dataKey []byte @@ -172,7 +132,7 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo } var encrypted []byte - encrypted, err = s.enc.Encrypt(ctx, payload, string(dataKey)) + encrypted, err = s.cipher.Encrypt(ctx, payload, string(dataKey)) if err != nil { s.log.Error("Failed to encrypt secret", "error", err) return nil, err @@ -235,7 +195,7 @@ func (s *EncryptionManager) dataKeyByLabel(ctx context.Context, namespace, label } // 2.1 Find the encryption provider. - provider, exists := s.providers[dataKey.Provider] + provider, exists := s.providerConfig.AvailableProviders[dataKey.Provider] if !exists { return "", nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider) } @@ -264,9 +224,9 @@ func (s *EncryptionManager) newDataKey(ctx context.Context, namespace string, la } // 2.1 Find the encryption provider. - provider, exists := s.providers[s.currentProviderID] + provider, exists := s.providerConfig.AvailableProviders[s.providerConfig.CurrentProvider] if !exists { - return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID) + return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.providerConfig.CurrentProvider) } // 2.2 Encrypt the data key. @@ -282,7 +242,7 @@ func (s *EncryptionManager) newDataKey(ctx context.Context, namespace string, la Active: true, UID: id, Namespace: namespace, - Provider: s.currentProviderID, + Provider: s.providerConfig.CurrentProvider, EncryptedData: encrypted, Label: label, } @@ -351,7 +311,7 @@ func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, paylo } var decrypted []byte - decrypted, err = s.enc.Decrypt(ctx, payload, string(dataKey)) + decrypted, err = s.cipher.Decrypt(ctx, payload, string(dataKey)) return decrypted, err } @@ -384,7 +344,7 @@ func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id strin } // 2.1. Find the encryption provider. - provider, exists := s.providers[dataKey.Provider] + provider, exists := s.providerConfig.AvailableProviders[dataKey.Provider] if !exists { return nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider) } @@ -398,6 +358,6 @@ func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id strin return decrypted, nil } -func (s *EncryptionManager) GetProviders() encryption.ProviderMap { - return s.providers +func (s *EncryptionManager) GetProviders() encryption.ProviderConfig { + return s.providerConfig } diff --git a/pkg/registry/apis/secret/encryption/manager/manager_test.go b/pkg/registry/apis/secret/encryption/manager/manager_test.go index e6b5b05ca9b..97166c52267 100644 --- a/pkg/registry/apis/secret/encryption/manager/manager_test.go +++ b/pkg/registry/apis/secret/encryption/manager/manager_test.go @@ -5,6 +5,7 @@ import ( "errors" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" @@ -14,7 +15,8 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" - "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" + osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -67,17 +69,16 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) { reports, err := svc.usageStats.GetUsageReport(context.Background()) require.NoError(t, err) - assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.current_provider.secretKey.count"]) - assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.providers.secretKey.count"]) + assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.current_provider.secret_key.count"]) + assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.providers.secret_key.count"]) }) } func TestEncryptionService_DataKeys(t *testing.T) { // Initialize data key storage with a fake db testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) tracer := noop.NewTracerProvider().Tracer("test") - store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil) + store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, nil) require.NoError(t, err) ctx := context.Background() @@ -161,7 +162,7 @@ func TestEncryptionService_DataKeys(t *testing.T) { func TestEncryptionService_UseCurrentProvider(t *testing.T) { t.Run("When encryption_provider is not specified explicitly, should use 'secretKey' as a current provider", func(t *testing.T) { svc := setupTestService(t) - assert.Equal(t, encryption.ProviderID("secretKey.v1"), svc.currentProviderID) + assert.Equal(t, encryption.ProviderID("secret_key.v1"), svc.providerConfig.CurrentProvider) }) t.Run("Should use encrypt/decrypt methods of the current encryption provider", func(t *testing.T) { @@ -175,23 +176,29 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { cfg := &setting.Cfg{ Raw: raw, SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: "sdDkslslld", - EncryptionProvider: "secretKey.v1", + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}}, }, } - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") - encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil) + encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, nil) + require.NoError(t, err) + + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc) require.NoError(t, err) encMgr, err := ProvideEncryptionManager( tracer, encryptionStore, - cfg, - &usagestats.UsageStatsMock{T: t}, - encryption.ProvideThirdPartyProviderMap(), + usageStats, + enc, + ossProviders, ) require.NoError(t, err) @@ -199,8 +206,10 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { //override default provider with fake, and register the fake separately fake := &fakeProvider{} - encryptionManager.providers[encryption.ProviderID("fakeProvider.v1")] = fake - encryptionManager.currentProviderID = "fakeProvider.v1" + encryptionManager.providerConfig.AvailableProviders = encryption.ProviderMap{ + encryption.ProviderID("fakeProvider.v1"): fake, + } + encryptionManager.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1") namespace := "test-namespace" encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{}) @@ -212,21 +221,183 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) { svcDecryptMgr, err := ProvideEncryptionManager( tracer, encryptionStore, - cfg, - &usagestats.UsageStatsMock{T: t}, - encryption.ProvideThirdPartyProviderMap(), + usageStats, + enc, + ossProviders, ) require.NoError(t, err) svcDecrypt := svcDecryptMgr.(*EncryptionManager) - svcDecrypt.providers[encryption.ProviderID("fakeProvider.v1")] = fake - svcDecrypt.currentProviderID = "fakeProvider.v1" + svcDecrypt.providerConfig.AvailableProviders = encryption.ProviderMap{ + encryption.ProviderID("fakeProvider.v1"): fake, + } + svcDecrypt.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1") _, _ = svcDecrypt.Decrypt(context.Background(), namespace, encrypted) assert.True(t, fake.decryptCalled, "fake provider's decrypt should be called") }) } +func TestEncryptionService_SecretKeyVersionUpgrade(t *testing.T) { + ctx := context.Background() + namespace := "test-namespace" + + // Generate random keys for testing + oldKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars + newKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars + + t.Run("should encrypt with v1, upgrade to v2, encrypt with v2, and decrypt both", func(t *testing.T) { + // Step 1: Set up v1 configuration + cfgV1 := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": oldKey}}, + }, + } + + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + tracer := noop.NewTracerProvider().Tracer("test") + encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, nil) + require.NoError(t, err) + + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfgV1, enc) + require.NoError(t, err) + + svcV1, err := ProvideEncryptionManager( + tracer, + encryptionStore, + usageStats, + enc, + ossProviders, + ) + require.NoError(t, err) + + // Step 2: Encrypt something with v1 + plaintext := []byte("secret data from v1") + encryptedV1, err := svcV1.Encrypt(ctx, namespace, plaintext) + require.NoError(t, err) + + // Verify v1 can decrypt its own data + decryptedV1, err := svcV1.Decrypt(ctx, namespace, encryptedV1) + require.NoError(t, err) + assert.Equal(t, plaintext, decryptedV1) + + // Verify current provider is v1 + encMgrV1 := svcV1.(*EncryptionManager) + assert.Equal(t, encryption.ProviderID("secret_key.v1"), encMgrV1.providerConfig.CurrentProvider) + + // Step 3: Create new configuration with v2 as current provider + cfgV2 := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + CurrentEncryptionProvider: "secret_key.v2", + ConfiguredKMSProviders: map[string]map[string]string{ + "secret_key.v1": {"secret_key": oldKey}, + "secret_key.v2": {"secret_key": newKey}, + }, + }, + } + + // Reinitialize service with v2 configuration (reuse same store) + ossProvidersV2, err := osskmsproviders.ProvideOSSKMSProviders(cfgV2, enc) + require.NoError(t, err) + + svcV2, err := ProvideEncryptionManager( + tracer, + encryptionStore, + usageStats, + enc, + ossProvidersV2, + ) + require.NoError(t, err) + + // Step 4: Ensure we can encrypt and decrypt with the new key (v2) + newPlaintext := []byte("secret data from v2") + encryptedV2, err := svcV2.Encrypt(ctx, namespace, newPlaintext) + require.NoError(t, err) + + decryptedV2, err := svcV2.Decrypt(ctx, namespace, encryptedV2) + require.NoError(t, err) + assert.Equal(t, newPlaintext, decryptedV2) + + // Verify current provider is v2 + encMgrV2 := svcV2.(*EncryptionManager) + assert.Equal(t, encryption.ProviderID("secret_key.v2"), encMgrV2.providerConfig.CurrentProvider) + + // Step 5: Ensure we can decrypt the old value encrypted with v1 + decryptedOldWithV2, err := svcV2.Decrypt(ctx, namespace, encryptedV1) + require.NoError(t, err) + assert.Equal(t, plaintext, decryptedOldWithV2) + + // Verify both providers are available + assert.Contains(t, encMgrV2.providerConfig.AvailableProviders, encryption.ProviderID("secret_key.v1")) + assert.Contains(t, encMgrV2.providerConfig.AvailableProviders, encryption.ProviderID("secret_key.v2")) + assert.Equal(t, 2, len(encMgrV2.providerConfig.AvailableProviders)) + }) + + t.Run("encrypting with v1 then removing the v1 config should cause decryption to fail", func(t *testing.T) { + tracer := noop.NewTracerProvider().Tracer("test") + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, nil) + require.NoError(t, err) + + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + cfgV1 := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{ + "secret_key.v1": {"secret_key": uuid.New().String()}, + }, + }, + } + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfgV1, enc) + require.NoError(t, err) + + svcV1, err := ProvideEncryptionManager( + tracer, + encryptionStore, + usageStats, + enc, + ossProviders, + ) + require.NoError(t, err) + + rsp, err := svcV1.Encrypt(ctx, namespace, []byte("test")) + require.NoError(t, err) + + cfgV2 := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + CurrentEncryptionProvider: "secret_key.v2", + ConfiguredKMSProviders: map[string]map[string]string{ + "secret_key.v2": {"secret_key": uuid.New().String()}, + }, + }, + } + + ossProvidersV2, err := osskmsproviders.ProvideOSSKMSProviders(cfgV2, enc) + require.NoError(t, err) + + svcV2, err := ProvideEncryptionManager( + tracer, + encryptionStore, + usageStats, + enc, + ossProvidersV2, + ) + require.NoError(t, err) + + _, err = svcV2.Decrypt(ctx, namespace, rsp) + require.Error(t, err) + }) +} + type fakeProvider struct { encryptCalled bool decryptCalled bool @@ -365,26 +536,29 @@ func TestIntegration_SecretsService(t *testing.T) { testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - defaultKey := "SdlklWklckeLS" - cfg := &setting.Cfg{ SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: defaultKey, - EncryptionProvider: "secretKey.v1", + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}}, }, } - store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil) + store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, nil) require.NoError(t, err) usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc) + require.NoError(t, err) + svc, err := ProvideEncryptionManager( tracer, store, - cfg, usageStats, - encryption.ProvideThirdPartyProviderMap(), + enc, + ossProviders, ) require.NoError(t, err) @@ -415,34 +589,28 @@ func TestIntegration_SecretsService(t *testing.T) { } } -func TestEncryptionService_ReInitReturnsError(t *testing.T) { - svc := setupTestService(t) - err := svc.InitProviders(encryption.ProviderMap{ - "fakeProvider.v1": &fakeProvider{}, - }) - require.Error(t, err) -} - func TestEncryptionService_ThirdPartyProviders(t *testing.T) { - cfg := &setting.Cfg{ - SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: "SdlklWklckeLS", - EncryptionProvider: "secretKey.v1", - }, - } + tracer := noop.NewTracerProvider().Tracer("test") + usageStats := &usagestats.UsageStatsMock{T: t} + + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) svc, err := ProvideEncryptionManager( + tracer, nil, - nil, - cfg, - &usagestats.UsageStatsMock{}, - encryption.ProviderMap{ - "fakeProvider.v1": &fakeProvider{}, + usageStats, + enc, + encryption.ProviderConfig{ + CurrentProvider: encryption.ProviderID("fakeProvider.v1"), + AvailableProviders: encryption.ProviderMap{ + encryption.ProviderID("fakeProvider.v1"): &fakeProvider{}, + }, }, ) require.NoError(t, err) encMgr := svc.(*EncryptionManager) - require.Len(t, encMgr.providers, 2) - require.Contains(t, encMgr.providers, encryption.ProviderID("fakeProvider.v1")) + require.Len(t, encMgr.providerConfig.AvailableProviders, 1) + require.Contains(t, encMgr.providerConfig.AvailableProviders, encryption.ProviderID("fakeProvider.v1")) } diff --git a/pkg/registry/apis/secret/encryption/manager/test_helpers.go b/pkg/registry/apis/secret/encryption/manager/test_helpers.go index a3958b5df7c..d8dfe9a0650 100644 --- a/pkg/registry/apis/secret/encryption/manager/test_helpers.go +++ b/pkg/registry/apis/secret/encryption/manager/test_helpers.go @@ -7,8 +7,8 @@ import ( "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" - "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" + osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -23,25 +23,30 @@ func setupTestService(tb testing.TB) *EncryptionManager { tracer := noop.NewTracerProvider().Tracer("test") database := database.ProvideDatabase(testDB, tracer) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) defaultKey := "SdlklWklckeLS" cfg := &setting.Cfg{ SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: defaultKey, - EncryptionProvider: "secretKey.v1", + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}}, }, } - store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil) + store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, nil) require.NoError(tb, err) usageStats := &usagestats.UsageStatsMock{T: tb} + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(tb, err) + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc) + require.NoError(tb, err) + encMgr, err := ProvideEncryptionManager( tracer, store, - cfg, usageStats, - encryption.ProvideThirdPartyProviderMap(), + enc, + ossProviders, ) require.NoError(tb, err) diff --git a/pkg/registry/apis/secret/encryption/secrets.go b/pkg/registry/apis/secret/encryption/secrets.go index 55a6d36c6cd..36beed76c37 100644 --- a/pkg/registry/apis/secret/encryption/secrets.go +++ b/pkg/registry/apis/secret/encryption/secrets.go @@ -9,7 +9,14 @@ import ( const UsageInsightsPrefix = "secrets_manager" -// Provider is a key encryption key provider for envelope encryption +type ProviderConfig struct { + CurrentProvider ProviderID + AvailableProviders ProviderMap +} + +type ProviderMap map[ProviderID]Provider + +// Provider is a fully configured key encryption key provider used for to encrypt and decrypt data keys for envelope encryption type Provider interface { Encrypt(ctx context.Context, blob []byte) ([]byte, error) Decrypt(ctx context.Context, blob []byte) ([]byte, error) @@ -17,6 +24,7 @@ type Provider interface { type ProviderID string +// Kind returns the kind of the provider, e.g. "secret_key", "aws_kms", "azure_keyvault", "google_kms", "hashicorp_vault" func (id ProviderID) Kind() (string, error) { idStr := string(id) @@ -28,17 +36,11 @@ func (id ProviderID) Kind() (string, error) { return parts[0], nil } +// KeyLabel returns a label for the data key that is unique to the current provider and today's date. func KeyLabel(providerID ProviderID) string { return fmt.Sprintf("%s@%s", time.Now().Format("2006-01-02"), providerID) } -type ProviderMap map[ProviderID]Provider - -// ProvideThirdPartyProviderMap fulfills the wire dependency needed by the encryption manager in OSS -func ProvideThirdPartyProviderMap() ProviderMap { - return ProviderMap{} -} - // BackgroundProvider should be implemented for a provider that has a task that needs to be run in the background. type BackgroundProvider interface { Run(ctx context.Context) error diff --git a/pkg/registry/apis/secret/register.go b/pkg/registry/apis/secret/register.go index f43b164f6b4..60aa3896d50 100644 --- a/pkg/registry/apis/secret/register.go +++ b/pkg/registry/apis/secret/register.go @@ -19,15 +19,16 @@ func RegisterDependencies( secretDBMigrator contracts.SecretDBMigrator, accessControlService accesscontrol.Service, ) (*DependencyRegisterer, error) { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { - return nil, nil - } - // Permissions for requests in multi-tenant mode will come from HG. if err := registerAccessControlRoles(accessControlService); err != nil { return nil, fmt.Errorf("registering access control roles: %w", err) } + // TODO: Remove once the DB schema is more stable. + if !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { + return nil, nil + } + // We shouldn't need to create the DB in HG, as that will use the MT api server. if cfg.StackID == "" { // Some DBs that claim to be MySQL/Postgres-compatible might not support table locking. diff --git a/pkg/registry/apis/secret/secretkeeper/fakes/fake_keeper.go b/pkg/registry/apis/secret/secretkeeper/fakes/fake_keeper.go deleted file mode 100644 index 0c8408a04bd..00000000000 --- a/pkg/registry/apis/secret/secretkeeper/fakes/fake_keeper.go +++ /dev/null @@ -1,68 +0,0 @@ -package fakes - -import ( - "context" - "errors" - - "github.com/google/uuid" - - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" -) - -var ErrSecretNotFound = errors.New("secret not found") - -type FakeKeeper struct { - values map[string]map[string]string -} - -var _ contracts.Keeper = (*FakeKeeper)(nil) - -func NewFakeKeeper() *FakeKeeper { - return &FakeKeeper{ - values: make(map[string]map[string]string), - } -} - -func (s *FakeKeeper) Store(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, exposedValueOrRef string) (contracts.ExternalID, error) { - ns, ok := s.values[namespace] - if !ok { - ns = make(map[string]string) - } - uid := uuid.New().String() - ns[uid] = exposedValueOrRef - s.values[namespace] = ns - - return contracts.ExternalID(uid), nil -} - -func (s *FakeKeeper) Expose(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID) (secretv0alpha1.ExposedSecureValue, error) { - ns, ok := s.values[namespace] - if !ok { - return "", ErrSecretNotFound - } - exposedVal, ok := ns[externalID.String()] - if !ok { - return "", ErrSecretNotFound - } - - return secretv0alpha1.NewExposedSecureValue(exposedVal), nil -} - -func (s *FakeKeeper) Delete(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID) error { - return nil -} - -func (s *FakeKeeper) Update(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID, exposedValueOrRef string) error { - ns, ok := s.values[namespace] - if !ok { - return ErrSecretNotFound - } - _, ok = ns[externalID.String()] - if !ok { - return ErrSecretNotFound - } - - ns[externalID.String()] = exposedValueOrRef - return nil -} diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go index 9cb1a4ec495..fc65573ac61 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper.go @@ -3,7 +3,7 @@ package secretkeeper import ( "go.opentelemetry.io/otel/trace" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" "github.com/prometheus/client_golang/prometheus" @@ -30,6 +30,6 @@ func ProvideService( // Ignore the config, but we could use it to get the keeper type and then return the correct keeper. // Instantiation only happens on ProvideService ONCE. -func (k *OSSKeeperService) KeeperForConfig(secretv0alpha1.KeeperConfig) (contracts.Keeper, error) { +func (k *OSSKeeperService) KeeperForConfig(secretv1beta1.KeeperConfig) (contracts.Keeper, error) { return k.systemKeeper, nil } diff --git a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go index f202babab8d..84baaf207b0 100644 --- a/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go @@ -8,9 +8,10 @@ import ( "go.opentelemetry.io/otel/trace/noop" "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" + osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/secret/database" @@ -26,8 +27,8 @@ func TestMain(m *testing.M) { func Test_OSSKeeperService(t *testing.T) { cfg := &setting.Cfg{ SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: "sdDkslslld", - EncryptionProvider: "secretKey.v1", + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}}, }, } keeperService, err := setupTestService(t, cfg) @@ -47,15 +48,20 @@ func setupTestService(t *testing.T, cfg *setting.Cfg) (*OSSKeeperService, error) testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") database := database.ProvideDatabase(testDB, tracer) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil) + dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, nil) require.NoError(t, err) - encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features) + encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer) require.NoError(t, err) - encryptionManager, err := manager.ProvideEncryptionManager(tracer, dataKeyStore, cfg, &usagestats.UsageStatsMock{T: t}, nil) + usageStats := &usagestats.UsageStatsMock{T: t} + enc, err := service.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc) + require.NoError(t, err) + encryptionManager, err := manager.ProvideEncryptionManager(tracer, dataKeyStore, usageStats, enc, ossProviders) require.NoError(t, err) // Initialize the keeper service diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go index 30656bb8e65..bfe3972f72b 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/metrics" "github.com/prometheus/client_golang/prometheus" @@ -36,8 +36,13 @@ func NewSQLKeeper( } } -func (s *SQLKeeper) Store(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, exposedValueOrRef string) (contracts.ExternalID, error) { - ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store", trace.WithAttributes(attribute.String("namespace", namespace))) +func (s *SQLKeeper) Store(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) (contracts.ExternalID, error) { + ctx, span := s.tracer.Start(ctx, "SQLKeeper.Store", + trace.WithAttributes( + attribute.String("namespace", namespace), + attribute.String("name", name), + attribute.Int64("version", version)), + ) defer span.End() start := time.Now() @@ -46,27 +51,28 @@ func (s *SQLKeeper) Store(ctx context.Context, cfg secretv0alpha1.KeeperConfig, return "", fmt.Errorf("unable to encrypt value: %w", err) } - encryptedVal, err := s.store.Create(ctx, namespace, encryptedData) + _, err = s.store.Create(ctx, namespace, name, version, encryptedData) if err != nil { return "", fmt.Errorf("unable to store encrypted value: %w", err) } s.metrics.StoreDuration.WithLabelValues(string(cfg.Type())).Observe(time.Since(start).Seconds()) - externalID := contracts.ExternalID(encryptedVal.UID) - span.SetAttributes(attribute.String("externalID", externalID.String())) - return externalID, nil + // An external id is not required to interact with the sql keeper. + // An empty string is returned just to comply with the Keeper interface. + return contracts.ExternalID(""), nil } -func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID) (secretv0alpha1.ExposedSecureValue, error) { +func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) (secretv1beta1.ExposedSecureValue, error) { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Expose", trace.WithAttributes( attribute.String("namespace", namespace), - attribute.String("externalID", externalID.String()), + attribute.String("name", name), + attribute.Int64("version", version), )) defer span.End() start := time.Now() - encryptedValue, err := s.store.Get(ctx, namespace, externalID.String()) + encryptedValue, err := s.store.Get(ctx, namespace, name, version) if err != nil { return "", fmt.Errorf("unable to get encrypted value: %w", err) } @@ -76,21 +82,22 @@ func (s *SQLKeeper) Expose(ctx context.Context, cfg secretv0alpha1.KeeperConfig, return "", fmt.Errorf("unable to decrypt value: %w", err) } - exposedValue := secretv0alpha1.NewExposedSecureValue(string(exposedBytes)) + exposedValue := secretv1beta1.NewExposedSecureValue(string(exposedBytes)) s.metrics.ExposeDuration.WithLabelValues(string(cfg.Type())).Observe(time.Since(start).Seconds()) return exposedValue, nil } -func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID) error { +func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Delete", trace.WithAttributes( attribute.String("namespace", namespace), - attribute.String("externalID", externalID.String()), + attribute.String("name", name), + attribute.Int64("version", version), )) defer span.End() start := time.Now() - err := s.store.Delete(ctx, namespace, externalID.String()) + err := s.store.Delete(ctx, namespace, name, version) if err != nil { return fmt.Errorf("failed to delete encrypted value: %w", err) } @@ -100,10 +107,11 @@ func (s *SQLKeeper) Delete(ctx context.Context, cfg secretv0alpha1.KeeperConfig, return nil } -func (s *SQLKeeper) Update(ctx context.Context, cfg secretv0alpha1.KeeperConfig, namespace string, externalID contracts.ExternalID, exposedValueOrRef string) error { +func (s *SQLKeeper) Update(ctx context.Context, cfg secretv1beta1.KeeperConfig, namespace, name string, version int64, exposedValueOrRef string) error { ctx, span := s.tracer.Start(ctx, "SQLKeeper.Update", trace.WithAttributes( attribute.String("namespace", namespace), - attribute.String("externalID", externalID.String()), + attribute.String("name", name), + attribute.Int64("version", version), )) defer span.End() @@ -113,7 +121,7 @@ func (s *SQLKeeper) Update(ctx context.Context, cfg secretv0alpha1.KeeperConfig, return fmt.Errorf("unable to encrypt value: %w", err) } - err = s.store.Update(ctx, namespace, externalID.String(), encryptedData) + err = s.store.Update(ctx, namespace, name, version, encryptedData) if err != nil { return fmt.Errorf("failed to update encrypted value: %w", err) } diff --git a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go index fa9aa7a6ba5..1333cf894c7 100644 --- a/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go +++ b/pkg/registry/apis/secret/secretkeeper/sqlkeeper/keeper_test.go @@ -1,23 +1,13 @@ -package sqlkeeper +package sqlkeeper_test import ( - "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - encryptionmanager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/storage/secret/database" - encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" - "github.com/grafana/grafana/pkg/storage/secret/migrator" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -26,159 +16,133 @@ func TestMain(m *testing.M) { } func Test_SQLKeeperSetup(t *testing.T) { - ctx := context.Background() namespace1 := "namespace1" + name1 := "name1" + version1 := int64(1) namespace2 := "namespace2" + name2 := "name2" plaintext1 := "very secret string in namespace 1" plaintext2 := "very secret string in namespace 2" - nonExistentID := contracts.ExternalID("non existent") - cfg := &setting.Cfg{ - SecretsManagement: setting.SecretsManagerSettings{ - SecretKey: "sdDkslslld", - EncryptionProvider: "secretKey.v1", - }, - } - - sqlKeeper, err := setupTestService(t, cfg) - require.NoError(t, err) - require.NotNil(t, sqlKeeper) - - keeperCfg := &secretv0alpha1.SystemKeeperConfig{} + keeperCfg := &secretv1beta1.SystemKeeperConfig{} t.Run("storing an encrypted value returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) - require.NoError(t, err) - require.NotEmpty(t, externalId1) + sut := testutils.Setup(t) - externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext2) + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) + require.NoError(t, err) + + _, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace2, name2, version1, plaintext2) require.NoError(t, err) - require.NotEmpty(t, externalId2) t.Run("expose the encrypted value from existing namespace", func(t *testing.T) { - exposedVal1, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1) + sut := testutils.Setup(t) + + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) + require.NoError(t, err) + + exposedVal1, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1) require.NoError(t, err) require.NotNil(t, exposedVal1) assert.Equal(t, plaintext1, exposedVal1.DangerouslyExposeAndConsumeValue()) - exposedVal2, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId2) + _, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace2, name2, version1, plaintext2) + require.NoError(t, err) + + exposedVal2, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace2, name2, version1) require.NoError(t, err) require.NotNil(t, exposedVal2) assert.Equal(t, plaintext2, exposedVal2.DangerouslyExposeAndConsumeValue()) }) t.Run("expose encrypted value from different namespace returns error", func(t *testing.T) { - exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace2, externalId1) + sut := testutils.Setup(t) + + _, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) + require.NoError(t, err) + + exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace2, name1, version1) require.Error(t, err) assert.Empty(t, exposedVal) - exposedVal, err = sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId2) + exposedVal, err = sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name2, version1) require.Error(t, err) assert.Empty(t, exposedVal) }) }) - t.Run("storing same value in same namespace returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) - require.NoError(t, err) - require.NotEmpty(t, externalId1) + t.Run("storing same value in same namespace returns error", func(t *testing.T) { + sut := testutils.Setup(t) - externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) require.NoError(t, err) - require.NotEmpty(t, externalId2) - assert.NotEqual(t, externalId1, externalId2) + _, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) + require.NotNil(t, err) }) t.Run("storing same value in different namespace returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) - require.NoError(t, err) - require.NotEmpty(t, externalId1) + sut := testutils.Setup(t) - externalId2, err := sqlKeeper.Store(ctx, keeperCfg, namespace2, plaintext1) + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) require.NoError(t, err) - require.NotEmpty(t, externalId2) - assert.NotEqual(t, externalId1, externalId2) + _, err = sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace2, name1, version1, plaintext1) + require.NoError(t, err) }) t.Run("exposing non existing values returns error", func(t *testing.T) { - exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, nonExistentID) + sut := testutils.Setup(t) + + exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, "non_existing_name", version1) require.Error(t, err) assert.Empty(t, exposedVal) }) t.Run("deleting an existing encrypted value does not return error", func(t *testing.T) { - externalID, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) - require.NoError(t, err) - require.NotEmpty(t, externalID) + sut := testutils.Setup(t) - exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalID) + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) + require.NoError(t, err) + + exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1) require.NoError(t, err) assert.NotNil(t, exposedVal) assert.Equal(t, plaintext1, exposedVal.DangerouslyExposeAndConsumeValue()) - err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, externalID) + err = sut.SQLKeeper.Delete(t.Context(), keeperCfg, namespace1, name1, version1) require.NoError(t, err) }) t.Run("deleting an non existing encrypted value does not return error", func(t *testing.T) { - err = sqlKeeper.Delete(ctx, keeperCfg, namespace1, nonExistentID) + sut := testutils.Setup(t) + + err := sut.SQLKeeper.Delete(t.Context(), keeperCfg, namespace1, "non_existing_name", version1) require.NoError(t, err) }) t.Run("updating an existent encrypted value returns no error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) - require.NoError(t, err) - require.NotEmpty(t, externalId1) + sut := testutils.Setup(t) - err = sqlKeeper.Update(ctx, keeperCfg, namespace1, externalId1, plaintext2) + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) require.NoError(t, err) - exposedVal, err := sqlKeeper.Expose(ctx, keeperCfg, namespace1, externalId1) + err = sut.SQLKeeper.Update(t.Context(), keeperCfg, namespace1, name1, version1, plaintext2) + require.NoError(t, err) + + exposedVal, err := sut.SQLKeeper.Expose(t.Context(), keeperCfg, namespace1, name1, version1) require.NoError(t, err) assert.NotNil(t, exposedVal) assert.Equal(t, plaintext2, exposedVal.DangerouslyExposeAndConsumeValue()) }) t.Run("updating a non existent encrypted value returns error", func(t *testing.T) { - externalId1, err := sqlKeeper.Store(ctx, keeperCfg, namespace1, plaintext1) - require.NoError(t, err) - require.NotEmpty(t, externalId1) + sut := testutils.Setup(t) - err = sqlKeeper.Update(ctx, nil, namespace1, nonExistentID, plaintext2) + _, err := sut.SQLKeeper.Store(t.Context(), keeperCfg, namespace1, name1, version1, plaintext1) + require.NoError(t, err) + + err = sut.SQLKeeper.Update(t.Context(), nil, namespace1, "non_existing_name", version1, plaintext2) require.Error(t, err) }) } - -func setupTestService(t *testing.T, cfg *setting.Cfg) (*SQLKeeper, error) { - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - database := database.ProvideDatabase(testDB, tracer) - - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - - // Initialize the encryption manager - dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil) - require.NoError(t, err) - - usageStats := &usagestats.UsageStatsMock{T: t} - - encMgr, err := encryptionmanager.ProvideEncryptionManager( - tracer, - dataKeyStore, - cfg, - usageStats, - nil, - ) - require.NoError(t, err) - - // Initialize encrypted value storage with a fake db - encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features) - require.NoError(t, err) - - // Initialize the SQLKeeper - sqlKeeper := NewSQLKeeper(tracer, encMgr, encValueStore, nil) - - return sqlKeeper, nil -} diff --git a/pkg/registry/apis/secret/service/decrypt.go b/pkg/registry/apis/secret/service/decrypt.go index d9b19ed2e76..f8ff83d95c9 100644 --- a/pkg/registry/apis/secret/service/decrypt.go +++ b/pkg/registry/apis/secret/service/decrypt.go @@ -3,14 +3,14 @@ package service import ( "context" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" ) // DecryptResult is the (union) result of a decryption operation. // It contains the decrypted `value` when the decryption succeeds, and the `err` when it fails. // It is not possible to construct a `DecryptResult` where both `value` and `err` are set from another package. type DecryptResult struct { - value *secretv0alpha1.ExposedSecureValue + value *secretv1beta1.ExposedSecureValue err error } @@ -18,7 +18,7 @@ func (d DecryptResult) Error() error { return d.err } -func (d DecryptResult) Value() *secretv0alpha1.ExposedSecureValue { +func (d DecryptResult) Value() *secretv1beta1.ExposedSecureValue { return d.value } @@ -26,7 +26,7 @@ func NewDecryptResultErr(err error) DecryptResult { return DecryptResult{err: err} } -func NewDecryptResultValue(value *secretv0alpha1.ExposedSecureValue) DecryptResult { +func NewDecryptResultValue(value *secretv1beta1.ExposedSecureValue) DecryptResult { return DecryptResult{value: value} } diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go new file mode 100644 index 00000000000..295ab09a7bd --- /dev/null +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -0,0 +1,211 @@ +package service + +import ( + "context" + "fmt" + + claims "github.com/grafana/authlib/types" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana-app-sdk/logging" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" +) + +type SecureValueService struct { + tracer trace.Tracer + accessClient claims.AccessClient + database contracts.Database + secureValueMetadataStorage contracts.SecureValueMetadataStorage + keeperMetadataStorage contracts.KeeperMetadataStorage + keeperService contracts.KeeperService +} + +var _ contracts.SecureValueService = &SecureValueService{} + +func ProvideSecureValueService( + tracer trace.Tracer, + accessClient claims.AccessClient, + database contracts.Database, + secureValueMetadataStorage contracts.SecureValueMetadataStorage, + keeperMetadataStorage contracts.KeeperMetadataStorage, + keeperService contracts.KeeperService, +) *SecureValueService { + return &SecureValueService{ + tracer: tracer, + accessClient: accessClient, + database: database, + secureValueMetadataStorage: secureValueMetadataStorage, + keeperMetadataStorage: keeperMetadataStorage, + keeperService: keeperService, + } +} + +func (s *SecureValueService) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes( + attribute.String("name", sv.GetName()), + attribute.String("namespace", sv.GetNamespace()), + attribute.String("actor", actorUID), + )) + defer span.End() + return s.createNewVersion(ctx, sv, actorUID) +} + +func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueService.Update", trace.WithAttributes( + attribute.String("name", newSecureValue.GetName()), + attribute.String("namespace", newSecureValue.GetNamespace()), + attribute.String("actor", actorUID), + )) + defer span.End() + + if newSecureValue.Spec.Value == nil { + currentVersion, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{}) + if err != nil { + return nil, false, fmt.Errorf("reading secure value secret: %+w", err) + } + + // TODO: does this need to be for update? + keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, newSecureValue.Namespace, newSecureValue.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err) + } + + keeper, err := s.keeperService.KeeperForConfig(keeperCfg) + if err != nil { + return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err) + } + logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type()) + + secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, newSecureValue.Name, currentVersion.Status.Version) + if err != nil { + return nil, false, fmt.Errorf("reading secret value from keeper: %w", err) + } + + newSecureValue.Spec.Value = &secret + } + + const updateIsSync = true + createdSv, err := s.createNewVersion(ctx, newSecureValue, actorUID) + return createdSv, updateIsSync, err +} + +func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { + createdSv, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID) + if err != nil { + return nil, fmt.Errorf("creating secure value: %w", err) + } + createdSv.Status = secretv1beta1.SecureValueStatus{ + Version: createdSv.Status.Version, + } + + // TODO: does this need to be for update? + keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, createdSv.Namespace, createdSv.Spec.Keeper, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return nil, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, createdSv.Spec.Keeper, err) + } + + keeper, err := s.keeperService.KeeperForConfig(keeperCfg) + if err != nil { + return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", createdSv.Namespace, createdSv.Spec.Keeper, err) + } + logging.FromContext(ctx).Debug("retrieved keeper", "namespace", createdSv.Namespace, "keeperName", createdSv.Spec.Keeper, "type", keeperCfg.Type()) + + // TODO: can we stop using external id? + // TODO: store uses only the namespace and returns and id. It could be a kv instead. + // TODO: check that the encrypted store works with multiple versions + externalID, err := keeper.Store(ctx, keeperCfg, createdSv.Namespace, createdSv.Name, createdSv.Status.Version, sv.Spec.Value.DangerouslyExposeAndConsumeValue()) + if err != nil { + return nil, fmt.Errorf("storing secure value in keeper: %w", err) + } + createdSv.Status.ExternalID = string(externalID) + + if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version, externalID); err != nil { + return nil, fmt.Errorf("setting secure value external id: %w", err) + } + + if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(createdSv.Namespace), createdSv.Name, createdSv.Status.Version); err != nil { + return nil, fmt.Errorf("marking secure value version as active: %w", err) + } + + // In a single query: + // TODO: set external id + // TODO: set to active + + return createdSv, nil +} + +func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueService.Read", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + )) + defer span.End() + + return s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: false}) +} + +func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueService.List", trace.WithAttributes( + attribute.String("namespace", namespace.String()), + )) + defer span.End() + + user, ok := claims.AuthInfoFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing auth info in context") + } + + hasPermissionFor, err := s.accessClient.Compile(ctx, user, claims.ListRequest{ + Group: secretv1beta1.APIGroup, + Resource: secretv1beta1.SecureValuesResourceInfo.GetName(), + Namespace: namespace.String(), + Verb: utils.VerbGet, // Why not VerbList? + }) + if err != nil { + return nil, fmt.Errorf("failed to compile checker: %w", err) + } + + secureValuesMetadata, err := s.secureValueMetadataStorage.List(ctx, namespace) + if err != nil { + return nil, fmt.Errorf("fetching secure values from storage: %+w", err) + } + + out := make([]secretv1beta1.SecureValue, 0) + + for _, metadata := range secureValuesMetadata { + // Check whether the user has permission to access this specific SecureValue in the namespace. + if !hasPermissionFor(metadata.Name, "") { + continue + } + + out = append(out, metadata) + } + + return &secretv1beta1.SecureValueList{ + Items: out, + }, nil +} + +func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + )) + defer span.End() + + // TODO: does this need to be for update? + sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true}) + if err != nil { + return nil, fmt.Errorf("fetching secure value: %+w", err) + } + + if err := s.secureValueMetadataStorage.SetVersionToInactive(ctx, namespace, name, sv.Status.Version); err != nil { + return nil, fmt.Errorf("setting secure value version to inactive: %+w", err) + } + + return sv, nil +} diff --git a/pkg/registry/apis/secret/service/secure_value_test.go b/pkg/registry/apis/secret/service/secure_value_test.go new file mode 100644 index 00000000000..1304e5b0ce1 --- /dev/null +++ b/pkg/registry/apis/secret/service/secure_value_test.go @@ -0,0 +1,96 @@ +package service_test + +import ( + "testing" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" +) + +func TestCrud(t *testing.T) { + t.Parallel() + + t.Run("creating a secure value creates new versions", func(t *testing.T) { + t.Parallel() + sut := testutils.Setup(t) + + sv1, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + // Create the same secure value twice + input := sv1.DeepCopy() + input.Spec.Description = "d2" + input.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("v2")) + + sv2, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(input)) + require.NoError(t, err) + require.True(t, sv2.Status.Version > sv1.Status.Version) + + // Read the secure value + sv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv2.Namespace), sv2.Name) + require.NoError(t, err) + + // It should be the latest version + require.Equal(t, sv2.Namespace, sv.Namespace) + require.Equal(t, sv2.Name, sv.Name) + require.Equal(t, "d2", sv.Spec.Description) + require.Equal(t, sv2.Status.Version, sv.Status.Version) + }) + + t.Run("updating a secure value creates new versions", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + // Create a secure value + sv1, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + ns := sv1.Namespace + name := sv1.Name + + // Update the secure value + input := sv1.DeepCopy() + input.Spec.Description = "d2" + input.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("v3")) + sv2, err := sut.UpdateSv(t.Context(), input) + require.NoError(t, err) + + // Read the secure value + sv3, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(ns), name) + require.NoError(t, err) + + // Nothing has changed except for the updated field. + require.Equal(t, ns, sv2.Namespace) + require.Equal(t, name, sv2.Name) + require.True(t, sv2.Status.Version > sv1.Status.Version) + require.Equal(t, "d2", sv2.Spec.Description) + + require.Equal(t, ns, sv3.Namespace) + require.Equal(t, name, sv3.Name) + require.Equal(t, sv2.Status.Version, sv3.Status.Version) + require.Equal(t, "d2", sv3.Spec.Description) + }) + + t.Run("deleting secure values", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + sv1, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + sv2, err := sut.DeleteSv(t.Context(), sv1.Namespace, sv1.Name) + require.NoError(t, err) + + require.Equal(t, sv1.Namespace, sv2.Namespace) + require.Equal(t, sv1.Name, sv2.Name) + + _, err = sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name, contracts.ReadOpts{}) + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + }) +} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go new file mode 100644 index 00000000000..e0afd0ab855 --- /dev/null +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -0,0 +1,232 @@ +package testutils + +import ( + "context" + "testing" + + "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/types" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" + cipher "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" + osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" + "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/secret/database" + encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/secret/metadata" + "github.com/grafana/grafana/pkg/storage/secret/migrator" +) + +type SetupConfig struct { + KeeperService contracts.KeeperService +} + +func defaultSetupCfg() SetupConfig { + return SetupConfig{} +} + +func WithKeeperService(keeperService contracts.KeeperService) func(*SetupConfig) { + return func(setupCfg *SetupConfig) { + setupCfg.KeeperService = keeperService + } +} + +func WithMutateCfg(f func(*SetupConfig)) func(*SetupConfig) { + return func(cfg *SetupConfig) { + f(cfg) + } +} + +func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { + setupCfg := defaultSetupCfg() + for _, opt := range opts { + opt(&setupCfg) + } + + tracer := noop.NewTracerProvider().Tracer("test") + testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) + + database := database.ProvideDatabase(testDB, tracer) + + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(database, tracer, nil) + require.NoError(t, err) + + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(database, tracer, nil) + require.NoError(t, err) + + // Initialize access client + access control + accessControl := &actest.FakeAccessControl{ExpectedEvaluate: true} + accessClient := accesscontrol.NewLegacyAccessClient(accessControl, accesscontrol.ResourceAuthorizerOptions{ + Resource: "securevalues", + Attr: "uid", + }) + + defaultKey := "SdlklWklckeLS" + cfg := &setting.Cfg{ + SecretsManagement: setting.SecretsManagerSettings{ + CurrentEncryptionProvider: "secret_key.v1", + ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}}, + }, + } + store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, nil) + require.NoError(t, err) + + usageStats := &usagestats.UsageStatsMock{T: t} + + enc, err := cipher.ProvideAESGCMCipherService(tracer, usageStats) + require.NoError(t, err) + + ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc) + require.NoError(t, err) + + encryptionManager, err := manager.ProvideEncryptionManager( + tracer, + store, + usageStats, + enc, + ossProviders, + ) + require.NoError(t, err) + + // Initialize encrypted value storage with a fake db + encryptedValueStorage, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer) + require.NoError(t, err) + + sqlKeeper := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, nil) + + var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) + + if setupCfg.KeeperService != nil { + keeperService = setupCfg.KeeperService + } + + secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, keeperMetadataStorage, keeperService) + + decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) + + decryptStorage, err := metadata.ProvideDecryptStorage(tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil) + require.NoError(t, err) + + decryptService := decrypt.ProvideDecryptService(decryptStorage) + + return Sut{ + SecureValueService: secureValueService, + SecureValueMetadataStorage: secureValueMetadataStorage, + DecryptStorage: decryptStorage, + DecryptService: decryptService, + EncryptedValueStorage: encryptedValueStorage, + SQLKeeper: sqlKeeper, + Database: database, + } +} + +type Sut struct { + SecureValueService *service.SecureValueService + SecureValueMetadataStorage contracts.SecureValueMetadataStorage + DecryptStorage contracts.DecryptStorage + DecryptService service.DecryptService + EncryptedValueStorage contracts.EncryptedValueStorage + SQLKeeper *sqlkeeper.SQLKeeper + Database *database.Database +} + +type CreateSvConfig struct { + Sv *secretv1beta1.SecureValue +} + +func CreateSvWithSv(sv *secretv1beta1.SecureValue) func(*CreateSvConfig) { + return func(cfg *CreateSvConfig) { + cfg.Sv = sv + } +} + +func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*secretv1beta1.SecureValue, error) { + cfg := CreateSvConfig{ + Sv: &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "desc1", + Value: ptr.To(secretv1beta1.NewExposedSecureValue("v1")), + Decrypters: []string{"decrypter1"}, + }, + Status: secretv1beta1.SecureValueStatus{}, + }, + } + for _, opt := range opts { + opt(&cfg) + } + + createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor-uid") + if err != nil { + return nil, err + } + return createdSv, nil +} + +func (s *Sut) UpdateSv(ctx context.Context, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) { + newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor-uid") + return newSv, err +} + +func (s *Sut) DeleteSv(ctx context.Context, namespace, name string) (*secretv1beta1.SecureValue, error) { + sv, err := s.SecureValueService.Delete(ctx, xkube.Namespace(namespace), name) + return sv, err +} + +type keeperServiceWrapper struct { + keeper contracts.Keeper +} + +func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper { + return &keeperServiceWrapper{keeper: keeper} +} + +func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv1beta1.KeeperConfig) (contracts.Keeper, error) { + return wrapper.keeper, nil +} + +func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context { + orgID := int64(1) + requester := &identity.StaticRequester{ + Namespace: namespace, + Type: types.TypeUser, + UserID: 1, + OrgID: orgID, + Permissions: map[int64]map[string][]string{ + orgID: permissions, + }, + } + + return types.WithAuthInfo(ctx, requester) +} + +func CreateServiceAuthContext(ctx context.Context, serviceIdentity string, permissions []string) context.Context { + requester := &identity.StaticRequester{ + AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: permissions, + ServiceIdentity: serviceIdentity, + }, + }, + } + + return types.WithAuthInfo(ctx, requester) +} diff --git a/pkg/registry/apis/secret/tracectx/carrier.go b/pkg/registry/apis/secret/tracectx/carrier.go new file mode 100644 index 00000000000..a8153c42d1e --- /dev/null +++ b/pkg/registry/apis/secret/tracectx/carrier.go @@ -0,0 +1,57 @@ +package tracectx + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + + "go.opentelemetry.io/otel/propagation" +) + +const ( + kvSeparator = "=" + pairSeparator = "#" +) + +func HexEncodeTraceFromContext(ctx context.Context) string { + carrier := propagation.MapCarrier(make(map[string]string)) + + propagation.TraceContext{}.Inject(ctx, carrier) + + // no trace in context + if len(carrier) == 0 { + return "" + } + + pairs := make([]string, 0, len(carrier)) + for k, v := range carrier { + pairs = append(pairs, k+kvSeparator+v) + } + + return hex.EncodeToString([]byte(strings.Join(pairs, pairSeparator))) +} + +func HexDecodeTraceIntoContext(ctx context.Context, encoded string) (context.Context, error) { + if encoded == "" { + return ctx, nil + } + + decoded, err := hex.DecodeString(encoded) + if err != nil { + return nil, err + } + + pairs := strings.Split(string(decoded), pairSeparator) + + carrier := make(propagation.MapCarrier, len(pairs)) + for _, pair := range pairs { + kv := strings.SplitN(pair, kvSeparator, 2) + if len(kv) != 2 || kv[0] == "" || kv[1] == "" { + return nil, fmt.Errorf("invalid key-value pair: %s", pair) + } + carrier[kv[0]] = kv[1] + } + + return propagation.TraceContext{}.Extract(ctx, carrier), nil +} diff --git a/pkg/registry/apis/secret/tracectx/carrier_test.go b/pkg/registry/apis/secret/tracectx/carrier_test.go new file mode 100644 index 00000000000..043428faeb2 --- /dev/null +++ b/pkg/registry/apis/secret/tracectx/carrier_test.go @@ -0,0 +1,88 @@ +package tracectx + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func TestHexEncodeTraceFromContext(t *testing.T) { + t.Run("when no trace is present in context, it returns empty string", func(t *testing.T) { + ctx := context.Background() + + encoded := HexEncodeTraceFromContext(ctx) + require.Empty(t, encoded) + }) + + t.Run("when trace is present in context, it returns hex-encoded string", func(t *testing.T) { + carrier := propagation.MapCarrier{ + "traceparent": "00-446e31681d64f9dcefd947c95ef321d0-009e2f3d8ded1892-01", + "tracestate": "first=abc1234,second=xyz7890", + } + ctx := propagation.TraceContext{}.Extract(context.Background(), carrier) + + encoded := HexEncodeTraceFromContext(ctx) + require.NotEmpty(t, encoded) + + traceCtx, err := HexDecodeTraceIntoContext(context.Background(), encoded) + require.NoError(t, err) + + span := trace.SpanFromContext(traceCtx) + require.True(t, span.SpanContext().IsValid()) + + carrier = propagation.MapCarrier(make(map[string]string)) + propagation.TraceContext{}.Inject(traceCtx, carrier) + require.Contains(t, carrier, "traceparent") + require.Contains(t, carrier, "tracestate") + }) +} + +func TestHexDecodeTraceIntoContext(t *testing.T) { + t.Run("when encoded string is empty, it returns original context", func(t *testing.T) { + ctx := context.Background() + + result, err := HexDecodeTraceIntoContext(ctx, "") + require.NoError(t, err) + require.Equal(t, ctx, result) + }) + + t.Run("when encoded string is valid hex, it returns context with trace", func(t *testing.T) { + encoded := hex.EncodeToString([]byte("traceparent=00-446e31681d64f9dcefd947c95ef321d0-009e2f3d8ded1892-01#tracestate=first=abc1234,second=xyz7890")) + + ctx, err := HexDecodeTraceIntoContext(context.Background(), encoded) + require.NoError(t, err) + + span := trace.SpanFromContext(ctx) + require.True(t, span.SpanContext().IsValid()) + }) + + t.Run("when encoded string has invalid hex encoding, it returns an error", func(t *testing.T) { + invalidHex := "invalid-hex-zzz" + + result, err := HexDecodeTraceIntoContext(context.Background(), invalidHex) + require.Error(t, err) + require.Nil(t, result) + }) + + t.Run("when decoded string has invalid key-value pair format, it returns an error", func(t *testing.T) { + // missing key + encoded := hex.EncodeToString([]byte("00-446e31681d64f9dcefd947c95ef321d0-009e2f3d8ded1892-01")) + + result, err := HexDecodeTraceIntoContext(context.Background(), encoded) + require.Error(t, err) + require.Nil(t, result) + }) + + t.Run("when decoded string has key without value, it returns error", func(t *testing.T) { + // missing value + encoded := hex.EncodeToString([]byte("traceparent=")) + + result, err := HexDecodeTraceIntoContext(context.Background(), encoded) + require.Error(t, err) + require.Nil(t, result) + }) +} diff --git a/pkg/registry/apis/secret/validator/keeper.go b/pkg/registry/apis/secret/validator/keeper.go new file mode 100644 index 00000000000..64afa94821a --- /dev/null +++ b/pkg/registry/apis/secret/validator/keeper.go @@ -0,0 +1,150 @@ +package validator + +import ( + "strings" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +type keeperValidator struct{} + +var _ contracts.KeeperValidator = &keeperValidator{} + +func ProvideKeeperValidator() contracts.KeeperValidator { + return &keeperValidator{} +} + +func (v *keeperValidator) Validate(keeper *secretv1beta1.Keeper, oldKeeper *secretv1beta1.Keeper, operation admission.Operation) field.ErrorList { + // Only validate Create and Update for now. + if operation != admission.Create && operation != admission.Update { + return nil + } + + errs := make(field.ErrorList, 0) + + if keeper.Spec.Description == "" { + errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required")) + } + + // Only one keeper type can be configured. Return early and don't validate the specific keeper fields. + if err := validateKeepers(keeper); err != nil { + errs = append(errs, err) + + return errs + } + + if keeper.Spec.Aws != nil { + if err := validateCredentialValue(field.NewPath("spec", "aws", "accessKeyID"), keeper.Spec.Aws.AccessKeyID); err != nil { + errs = append(errs, err) + } + + if err := validateCredentialValue(field.NewPath("spec", "aws", "secretAccessKey"), keeper.Spec.Aws.SecretAccessKey); err != nil { + errs = append(errs, err) + } + } + + if keeper.Spec.Azure != nil { + if keeper.Spec.Azure.KeyVaultName == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "keyVaultName"), "a `keyVaultName` is required")) + } + + if keeper.Spec.Azure.TenantID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "tenantID"), "a `tenantID` is required")) + } + + if keeper.Spec.Azure.ClientID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "azure", "clientID"), "a `clientID` is required")) + } + + if err := validateCredentialValue(field.NewPath("spec", "azure", "clientSecret"), keeper.Spec.Azure.ClientSecret); err != nil { + errs = append(errs, err) + } + } + + if keeper.Spec.Gcp != nil { + if keeper.Spec.Gcp.ProjectID == "" { + errs = append(errs, field.Required(field.NewPath("spec", "gcp", "projectID"), "a `projectID` is required")) + } + + if keeper.Spec.Gcp.CredentialsFile == "" { + errs = append(errs, field.Required(field.NewPath("spec", "gcp", "credentialsFile"), "a `credentialsFile` is required")) + } + } + + if keeper.Spec.HashiCorpVault != nil { + if keeper.Spec.HashiCorpVault.Address == "" { + errs = append(errs, field.Required(field.NewPath("spec", "hashiCorpVault", "address"), "an `address` is required")) + } + + if err := validateCredentialValue(field.NewPath("spec", "hashiCorpVault", "token"), keeper.Spec.HashiCorpVault.Token); err != nil { + errs = append(errs, err) + } + } + + return errs +} + +func validateKeepers(keeper *secretv1beta1.Keeper) *field.Error { + availableKeepers := map[string]bool{ + "aws": keeper.Spec.Aws != nil, + "azure": keeper.Spec.Azure != nil, + "gcp": keeper.Spec.Gcp != nil, + "hashiCorpVault": keeper.Spec.HashiCorpVault != nil, + } + + configuredKeepers := make([]string, 0) + + for keeperKind, notNil := range availableKeepers { + if notNil { + configuredKeepers = append(configuredKeepers, keeperKind) + } + } + + if len(configuredKeepers) == 0 { + return field.Required(field.NewPath("spec"), "at least one `keeper` must be present") + } + + if len(configuredKeepers) > 1 { + return field.Invalid( + field.NewPath("spec"), + strings.Join(configuredKeepers, " & "), + "only one `keeper` can be present at a time but found more", + ) + } + + return nil +} + +func validateCredentialValue(path *field.Path, credentials secretv1beta1.KeeperCredentialValue) *field.Error { + availableOptions := map[string]bool{ + "secureValueName": credentials.SecureValueName != "", + "valueFromEnv": credentials.ValueFromEnv != "", + "valueFromConfig": credentials.ValueFromConfig != "", + } + + configuredCredentials := make([]string, 0) + + for credentialKind, notEmpty := range availableOptions { + if notEmpty { + configuredCredentials = append(configuredCredentials, credentialKind) + } + } + + if len(configuredCredentials) == 0 { + return field.Required(path, "one of `secureValueName`, `valueFromEnv` or `valueFromConfig` must be present") + } + + if len(configuredCredentials) > 1 { + return field.Invalid( + path, + strings.Join(configuredCredentials, " & "), + "only one of `secureValueName`, `valueFromEnv` or `valueFromConfig` must be present at a time but found more", + ) + } + + return nil +} diff --git a/pkg/registry/apis/secret/validator/keeper_test.go b/pkg/registry/apis/secret/validator/keeper_test.go new file mode 100644 index 00000000000..3c4b4015da6 --- /dev/null +++ b/pkg/registry/apis/secret/validator/keeper_test.go @@ -0,0 +1,285 @@ +package validator + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/admission" + "k8s.io/utils/ptr" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" +) + +func TestValidateKeeper(t *testing.T) { + t.Run("when creating a new keeper", func(t *testing.T) { + t.Run("the `description` must be present", func(t *testing.T) { + keeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Aws: &secretv1beta1.KeeperAWSConfig{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"}, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ValueFromEnv: "some-value"}, + KmsKeyID: ptr.To("kms-key-id"), + }, + }, + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.description", errs[0].Field) + }) + }) + + t.Run("only one `keeper` must be present", func(t *testing.T) { + keeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Description: "short description", + Aws: &secretv1beta1.KeeperAWSConfig{}, + Azure: &secretv1beta1.KeeperAzureConfig{}, + Gcp: &secretv1beta1.KeeperGCPConfig{}, + HashiCorpVault: &secretv1beta1.KeeperHashiCorpConfig{}, + }, + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("at least one `keeper` must be present", func(t *testing.T) { + keeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + }, + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("aws keeper validation", func(t *testing.T) { + validKeeperAWS := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Aws: &secretv1beta1.KeeperAWSConfig{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "some-value", + }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + SecureValueName: "some-value", + }, + KmsKeyID: ptr.To("optional"), + }, + }, + } + + t.Run("`accessKeyID` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{} + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.accessKeyID", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.Aws.AccessKeyID = secretv1beta1.KeeperCredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.accessKeyID", errs[0].Field) + }) + }) + + t.Run("`secretAccessKey` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{} + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAWS.DeepCopy() + keeper.Spec.Aws.SecretAccessKey = secretv1beta1.KeeperCredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.aws.secretAccessKey", errs[0].Field) + }) + }) + }) + + t.Run("azure keeper validation", func(t *testing.T) { + validKeeperAzure := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Azure: &secretv1beta1.KeeperAzureConfig{ + KeyVaultName: "kv-name", + TenantID: "tenant-id", + ClientID: "client-id", + ClientSecret: secretv1beta1.KeeperCredentialValue{ + ValueFromConfig: "config.path.value", + }, + }, + }, + } + + t.Run("`keyVaultName` must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.KeyVaultName = "" + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.keyVaultName", errs[0].Field) + }) + + t.Run("`tenantID` must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.TenantID = "" + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.tenantID", errs[0].Field) + }) + + t.Run("`clientID` must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.ClientID = "" + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.clientID", errs[0].Field) + }) + + t.Run("`clientSecret` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.ClientSecret = secretv1beta1.KeeperCredentialValue{} + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.clientSecret", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperAzure.DeepCopy() + keeper.Spec.Azure.ClientSecret = secretv1beta1.KeeperCredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.azure.clientSecret", errs[0].Field) + }) + }) + }) + + t.Run("gcp keeper validation", func(t *testing.T) { + validKeeperGCP := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + Gcp: &secretv1beta1.KeeperGCPConfig{ + ProjectID: "project-id", + CredentialsFile: "/path/to/credentials/file.json", + }, + }, + } + + t.Run("`projectID` must be present", func(t *testing.T) { + keeper := validKeeperGCP.DeepCopy() + keeper.Spec.Gcp.ProjectID = "" + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.gcp.projectID", errs[0].Field) + }) + + t.Run("`credentialsFile` must be present", func(t *testing.T) { + keeper := validKeeperGCP.DeepCopy() + keeper.Spec.Gcp.CredentialsFile = "" + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.gcp.credentialsFile", errs[0].Field) + }) + }) + + t.Run("hashicorp keeper validation", func(t *testing.T) { + validKeeperHashiCorp := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ + Description: "description", + HashiCorpVault: &secretv1beta1.KeeperHashiCorpConfig{ + Address: "http://address", + Token: secretv1beta1.KeeperCredentialValue{ + ValueFromConfig: "config.path.value", + }, + }, + }, + } + + t.Run("`address` must be present", func(t *testing.T) { + keeper := validKeeperHashiCorp.DeepCopy() + keeper.Spec.HashiCorpVault.Address = "" + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.hashiCorpVault.address", errs[0].Field) + }) + + t.Run("`token` must be present", func(t *testing.T) { + t.Run("at least one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperHashiCorp.DeepCopy() + keeper.Spec.HashiCorpVault.Token = secretv1beta1.KeeperCredentialValue{} + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.hashiCorpVault.token", errs[0].Field) + }) + + t.Run("at most one of the credential value must be present", func(t *testing.T) { + keeper := validKeeperHashiCorp.DeepCopy() + keeper.Spec.HashiCorpVault.Token = secretv1beta1.KeeperCredentialValue{ + SecureValueName: "a", + ValueFromEnv: "b", + ValueFromConfig: "c", + } + + validator := ProvideKeeperValidator() + errs := validator.Validate(keeper, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.hashiCorpVault.token", errs[0].Field) + }) + }) + }) +} diff --git a/pkg/registry/apis/secret/validator/secure_value.go b/pkg/registry/apis/secret/validator/secure_value.go new file mode 100644 index 00000000000..87d33f636a4 --- /dev/null +++ b/pkg/registry/apis/secret/validator/secure_value.go @@ -0,0 +1,157 @@ +package validator + +import ( + "errors" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +type secureValueValidator struct{} + +var _ contracts.SecureValueValidator = &secureValueValidator{} + +func ProvideSecureValueValidator() contracts.SecureValueValidator { + return &secureValueValidator{} +} + +func (v *secureValueValidator) Validate(sv, oldSv *secretv1beta1.SecureValue, operation admission.Operation) field.ErrorList { + errs := make(field.ErrorList, 0) + + // Operation-specific field validation. + switch operation { + case admission.Create: + errs = validateSecureValueCreate(sv) + + // If we plan to support PATCH-style updates, we shouldn't be requiring fields to be set. + case admission.Update: + errs = validateSecureValueUpdate(sv, oldSv) + + case admission.Delete: + case admission.Connect: + } + + // General validations. + if sv.Spec.Value != nil && len(*sv.Spec.Value) > contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES { + errs = append( + errs, + field.TooLong(field.NewPath("spec", "value"), len(*sv.Spec.Value), contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES), + ) + } + + if errs := validateDecrypters(sv.Spec.Decrypters); len(errs) > 0 { + return errs + } + + return errs +} + +// validateSecureValueCreate does basic spec validation of a securevalue for the Create operation. +func validateSecureValueCreate(sv *secretv1beta1.SecureValue) field.ErrorList { + errs := make(field.ErrorList, 0) + + if sv.Spec.Description == "" { + errs = append(errs, field.Required(field.NewPath("spec", "description"), "a `description` is required")) + } + + if (sv.Spec.Value == nil || (sv.Spec.Value != nil && *sv.Spec.Value == "")) && (sv.Spec.Ref == nil || (sv.Spec.Ref != nil && *sv.Spec.Ref == "")) { + errs = append(errs, field.Required(field.NewPath("spec"), "either a `value` or `ref` is required")) + } + + if (sv.Spec.Value != nil && *sv.Spec.Value != "") && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "only one of `value` or `ref` can be set")) + } + + return errs +} + +// validateSecureValueUpdate does basic spec validation of a securevalue for the Update operation. +func validateSecureValueUpdate(sv, oldSv *secretv1beta1.SecureValue) field.ErrorList { + errs := make(field.ErrorList, 0) + + // For updates, an `old` object is required. + if oldSv == nil { + errs = append(errs, field.InternalError(field.NewPath("spec"), errors.New("old object is nil"))) + + return errs + } + + // Only validate if one of the fields is being changed/set. + if (sv.Spec.Value != nil && *sv.Spec.Value != "") || (sv.Spec.Ref != nil && *sv.Spec.Ref != "") { + if (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref != "") && (sv.Spec.Value != nil && *sv.Spec.Value != "") { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `value` when `ref` was already previously set")) + } + + if (oldSv.Spec.Ref == nil || (oldSv.Spec.Ref != nil && *oldSv.Spec.Ref == "")) && (sv.Spec.Ref != nil && *sv.Spec.Ref != "") { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "cannot set `ref` when `value` was already previously set")) + } + } + + // Keeper cannot be changed. + if sv.Spec.Keeper != oldSv.Spec.Keeper { + errs = append(errs, field.Forbidden(field.NewPath("spec"), "the `keeper` cannot be changed")) + } + + return errs +} + +// validateDecrypters validates that (if populated) the `decrypters` must be unique. +func validateDecrypters(decrypters []string) field.ErrorList { + errs := make(field.ErrorList, 0) + + // Limit the number of decrypters to 64 to not have it unbounded. + // The number was chosen arbitrarily and should be enough. + if len(decrypters) > 64 { + errs = append( + errs, + field.TooMany(field.NewPath("spec", "decrypters"), len(decrypters), 64), + ) + + return errs + } + + decrypterNames := make(map[string]struct{}, 0) + + for i, decrypter := range decrypters { + decrypter = strings.TrimSpace(decrypter) + if decrypter == "" { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters cannot be empty if specified"), + ) + + continue + } + + // Use the same validation as labels for the decrypters. + if verrs := validation.IsValidLabelValue(decrypter); len(verrs) > 0 { + for _, verr := range verrs { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, verr), + ) + } + + continue + } + + if _, exists := decrypterNames[decrypter]; exists { + errs = append( + errs, + field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"), + ) + + continue + } + + decrypterNames[decrypter] = struct{}{} + } + + return errs +} diff --git a/pkg/registry/apis/secret/validator/secure_value_test.go b/pkg/registry/apis/secret/validator/secure_value_test.go new file mode 100644 index 00000000000..4dcf133ae4c --- /dev/null +++ b/pkg/registry/apis/secret/validator/secure_value_test.go @@ -0,0 +1,286 @@ +package validator + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/admission" + "k8s.io/utils/ptr" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +func TestValidateSecureValue(t *testing.T) { + t.Run("when creating a new securevalue", func(t *testing.T) { + keeper := "keeper" + validSecureValue := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "description", + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + Keeper: &keeper, + Decrypters: []string{"app1", "app2"}, + }, + } + + t.Run("the `description` must be present", func(t *testing.T) { + sv := validSecureValue.DeepCopy() + sv.Spec.Description = "" + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.description", errs[0].Field) + }) + + t.Run("either a `value` or `ref` must be present but not both", func(t *testing.T) { + // nil + sv := validSecureValue.DeepCopy() + sv.Spec.Value = nil + sv.Spec.Ref = nil + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + + // empty value + sv.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("")) + sv.Spec.Ref = nil + + validator = ProvideSecureValueValidator() + errs = validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + + // present value and ref + ref := "value" + sv.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("value")) + sv.Spec.Ref = &ref + + validator = ProvideSecureValueValidator() + errs = validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("`value` cannot exceed 24576 bytes", func(t *testing.T) { + sv := validSecureValue.DeepCopy() + sv.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue(strings.Repeat("a", contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES+1))) + sv.Spec.Ref = nil + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.value", errs[0].Field) + }) + }) + + t.Run("when updating a securevalue", func(t *testing.T) { + t.Run("when trying to switch from a `value` (old) to a `ref` (new), it returns an error", func(t *testing.T) { + oldSv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Ref: nil, // empty `ref` means a `value` was present. + }, + } + + ref := "ref" + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Ref: &ref, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, oldSv, admission.Update) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when trying to switch from a `ref` (old) to a `value` (new), it returns an error", func(t *testing.T) { + ref := "non-empty" + oldSv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Ref: &ref, + }, + } + + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, oldSv, admission.Update) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when both `value` and `ref` are set, it returns an error", func(t *testing.T) { + refNonEmpty := "non-empty" + oldSv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Ref: &refNonEmpty, + }, + } + + ref := "ref" + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + Ref: &ref, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, oldSv, admission.Update) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + + oldSv = &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Value: ptr.To(secretv1beta1.NewExposedSecureValue("non-empty")), + }, + } + + validator = ProvideSecureValueValidator() + errs = validator.Validate(sv, oldSv, admission.Update) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when no changes are made, it returns no errors", func(t *testing.T) { + oldSv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "old-description", + }, + } + + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "new-description", + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, oldSv, admission.Update) + require.Empty(t, errs) + }) + + t.Run("when the old object is `nil` it returns an error", func(t *testing.T) { + sv := &secretv1beta1.SecureValue{} + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Update) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + + t.Run("when trying to change the `keeper`, it returns an error", func(t *testing.T) { + keeperA := "a-keeper" + keeperAnother := "another-keeper" + oldSv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Keeper: &keeperA, + }, + } + + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Keeper: &keeperAnother, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, oldSv, admission.Update) + require.Len(t, errs, 1) + require.Equal(t, "spec", errs[0].Field) + }) + }) + + t.Run("`decrypters` must have unique items", func(t *testing.T) { + ref := "ref" + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "description", Ref: &ref, + + Decrypters: []string{ + "app1", + "app1", + }, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.decrypters.[1]", errs[0].Field) + }) + + t.Run("`decrypters` list can be empty", func(t *testing.T) { + ref := "ref" + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "description", Ref: &ref, + + Decrypters: []string{}, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Empty(t, errs) + }) + + t.Run("`decrypters` must be a valid label value", func(t *testing.T) { + decrypters := []string{ + "", // invalid + "is/this/valid", // invalid + "is this valid", // invalid + "is.this.valid", + "is-this-valid", + "is_this_valid", + "0isthisvalid9", + "isthisvalid9", + "0isthisvalid", + "isthisvalid", + } + + ref := "ref" + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "description", Ref: &ref, + + Decrypters: decrypters, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 3) + }) + + t.Run("`decrypters` cannot have more than 64 items", func(t *testing.T) { + decrypters := make([]string, 0, 64+1) + for i := 0; i < 64+1; i++ { + decrypters = append(decrypters, fmt.Sprintf("app%d", i)) + } + + ref := "ref" + sv := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ + Description: "description", Ref: &ref, + + Decrypters: decrypters, + }, + } + + validator := ProvideSecureValueValidator() + errs := validator.Validate(sv, nil, admission.Create) + require.Len(t, errs, 1) + require.Equal(t, "spec.decrypters", errs[0].Field) + }) +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index ed7d0aa1c5e..ebea70d289b 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/ofrep" "github.com/grafana/grafana/pkg/registry/apis/provisioning" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/service" @@ -20,18 +20,6 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" ) -// HACK: This is a hack so that wire can uniquely identify dependencies -func MergeProvisioningExtras(webhook webhooks.WebhookExtraBuilder) []provisioning.ExtraBuilder { - return []provisioning.ExtraBuilder{ - webhook.ExtraBuilder, - } -} - -var ProvisioningExtras = wire.NewSet( - webhooks.ProvideWebhooks, - MergeProvisioningExtras, -) - // WireSetExts is a set of providers that can be overridden by enterprise implementations. var WireSetExts = wire.NewSet( noopstorage.ProvideStorageBackend, @@ -55,9 +43,8 @@ var WireSet = wire.NewSet( featuretoggle.RegisterAPIService, datasource.RegisterAPIService, folders.RegisterAPIService, - WireSetExts, // this will be moved to wireexts_oss.go in a following PR iam.RegisterAPIService, - ProvisioningExtras, + secrets.ProvideRepositorySecrets, provisioning.RegisterAPIService, service.RegisterAPIService, query.RegisterAPIService, diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 979e993eb7a..bca9bf21c6b 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/modules" "github.com/grafana/grafana/pkg/services/authz" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -39,6 +40,7 @@ func NewModule(opts Options, indexMetrics *resource.BleveIndexMetrics, reg prometheus.Registerer, promGatherer prometheus.Gatherer, + tracer tracing.Tracer, // Ensures tracing is initialized license licensing.Licensing, ) (*ModuleServer, error) { s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license) diff --git a/pkg/server/search_server_distributor_test.go b/pkg/server/search_server_distributor_test.go index 3062b1efe8d..32dddffcec8 100644 --- a/pkg/server/search_server_distributor_test.go +++ b/pkg/server/search_server_distributor_test.go @@ -15,6 +15,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/modules" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" @@ -321,7 +322,9 @@ func initModuleServerForTest( opts Options, apiOpts api.ServerOptions, ) testModuleServer { - ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, nil) + tracer := tracing.InitializeTracerForTest() + + ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil) require.NoError(t, err) conn, err := grpc.NewClient(cfg.GRPCServer.Address, diff --git a/pkg/server/server.go b/pkg/server/server.go index ca1263d9e4d..794cf656763 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -11,6 +11,7 @@ import ( "strconv" "sync" + "github.com/grafana/grafana/pkg/services/featuremgmt" "golang.org/x/sync/errgroup" "github.com/prometheus/client_golang/prometheus" @@ -131,6 +132,11 @@ func (s *Server) Init() error { return err } + // Initialize the OpenFeature feature flag system + if err := featuremgmt.InitOpenFeatureWithCfg(s.cfg); err != nil { + return err + } + return s.provisioningService.RunInitProvisioners(s.context) } diff --git a/pkg/server/test_env.go b/pkg/server/test_env.go index fdf52796a8a..ff151cf7b60 100644 --- a/pkg/server/test_env.go +++ b/pkg/server/test_env.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" @@ -33,6 +34,7 @@ func ProvideTestEnv( resourceClient resource.ResourceClient, idService auth.IDService, githubFactory *github.Factory, + repositorySecrets secrets.RepositorySecrets, ) (*TestEnv, error) { return &TestEnv{ TestingT: testingT, @@ -48,6 +50,7 @@ func ProvideTestEnv( ResourceClient: resourceClient, IDService: idService, GitHubFactory: githubFactory, + RepositorySecrets: repositorySecrets, }, nil } @@ -69,4 +72,5 @@ type TestEnv struct { ResourceClient resource.ResourceClient IDService auth.IDService GitHubFactory *github.Factory + RepositorySecrets secrets.RepositorySecrets } diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 406497bcf9f..d32e3ca6586 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -13,7 +13,6 @@ import ( "go.opentelemetry.io/otel/trace" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" @@ -43,8 +42,10 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" secretcontracts "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" secretdecrypt "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" - gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + cipher "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" + secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service" + secretvalidator "github.com/grafana/grafana/pkg/registry/apis/secret/validator" appregistry "github.com/grafana/grafana/pkg/registry/apps" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" @@ -325,8 +326,6 @@ var wireBasicSet = wire.NewSet( expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, - featuremgmt.ProvideOpenFeatureService, - featuremgmt.ProvideStaticEvaluator, dashboardservice.ProvideDashboardServiceImpl, wire.Bind(new(dashboards.PermissionsRegistrationService), new(*dashboardservice.DashboardServiceImpl)), dashboardservice.ProvideDashboardService, @@ -424,16 +423,19 @@ var wireBasicSet = wire.NewSet( // Secrets Manager secretmetadata.ProvideSecureValueMetadataStorage, secretmetadata.ProvideKeeperMetadataStorage, - secretmetadata.ProvideOutboxQueue, + secretmetadata.ProvideDecryptStorage, + secretdecrypt.ProvideDecryptAuthorizer, + secretdecrypt.ProvideDecryptService, secretencryption.ProvideDataKeyStorage, secretencryption.ProvideEncryptedValueStorage, + secretsecurevalueservice.ProvideSecureValueService, + secretvalidator.ProvideKeeperValidator, + secretvalidator.ProvideSecureValueValidator, secretmigrator.NewWithEngine, secretdatabase.ProvideDatabase, wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)), encryptionManager.ProvideEncryptionManager, - gsmEncryption.ProvideThirdPartyProviderMap, - secretdecrypt.ProvideDecryptAuthorizer, - secretdecrypt.ProvideDecryptAllowList, + cipher.ProvideAESGCMCipherService, // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, @@ -499,7 +501,8 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser func InitializeForTest(t sqlutil.ITestDB, testingT interface { mock.TestingT Cleanup(func()) -}, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { +}, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions, +) (*TestEnv, error) { wire.Build(wireExtsTestSet) return &TestEnv{Server: &Server{}, TestingT: testingT, SQLStore: &sqlstore.SQLStore{}, Cfg: &setting.Cfg{}}, nil } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 9fba20aa8c6..3bac211a982 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -55,14 +55,20 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/ofrep" provisioning2 "github.com/grafana/grafana/pkg/registry/apis/provisioning" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/extras" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" query2 "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" - encryption3 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" + service11 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" manager4 "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" + "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" + service12 "github.com/grafana/grafana/pkg/registry/apis/secret/service" + validator3 "github.com/grafana/grafana/pkg/registry/apis/secret/validator" "github.com/grafana/grafana/pkg/registry/apis/userstorage" "github.com/grafana/grafana/pkg/registry/apps" advisor2 "github.com/grafana/grafana/pkg/registry/apps/advisor" @@ -111,7 +117,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/guardian" service7 "github.com/grafana/grafana/pkg/services/datasources/service" - "github.com/grafana/grafana/pkg/services/encryption" + encryption2 "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/encryption/provider" service2 "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/extsvcauth" @@ -191,7 +197,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" - "github.com/grafana/grafana/pkg/services/secrets" + secrets2 "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" kvstore2 "github.com/grafana/grafana/pkg/services/secrets/kvstore" migrations2 "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" @@ -233,7 +239,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" database5 "github.com/grafana/grafana/pkg/storage/secret/database" - encryption2 "github.com/grafana/grafana/pkg/storage/secret/encryption" + "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/grafana/grafana/pkg/storage/secret/metadata" migrator2 "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" @@ -469,8 +475,8 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser } extSvcAccountsService := extsvcaccounts.ProvideExtSvcAccountsService(acimplService, cfg, inProcBus, sqlStore, featureToggles, registerer, serviceAccountsService, secretsService, tracingService) registryRegistry := registry2.ProvideExtSvcRegistry(cfg, extSvcAccountsService, serverLockService, featureToggles) - service11 := service4.ProvideService(sqlStore, secretsService) - serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service11) + service13 := service4.ProvideService(sqlStore, secretsService) + serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service13) initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService) terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService) if err != nil { @@ -493,7 +499,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } pluginService := service5.ProvideDashboardPluginService(featureToggles, dashboardServiceImpl) - service12 := service6.ProvideService(fileStoreManager, pluginService) + service14 := service6.ProvideService(fileStoreManager, pluginService) orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) ssosettingsimplService := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routeRegisterImpl, featureToggles, secretsService, usageStats, registerer, ossImpl, ossLicensingService) socialService := socialimpl.ProvideService(cfg, featureToggles, usageStats, bundleregistryService, remoteCache, orgRoleMapper, ssosettingsimplService) @@ -538,11 +544,11 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser datasourcePermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService(cfg, featureToggles, sqlStore) requestConfigProvider := pluginconfig.NewRequestConfigProvider(pluginInstanceCfg) baseProvider := plugincontext.ProvideBaseService(cfg, requestConfigProvider) - service13, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) + service15, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) if err != nil { return nil, err } - correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service13, accessControl, inProcBus, quotaService, cfg) + correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service15, accessControl, inProcBus, quotaService, cfg) if err != nil { return nil, err } @@ -559,14 +565,14 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service13, correlationsService, dashboardService, folderimplService, service11, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) if err != nil { return nil, err } - dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service13, tracingService, secretsService, featureToggles) + dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service15, tracingService, secretsService, featureToggles) starService := starimpl.ProvideService(sqlStore) searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) - plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service13, service11, requestConfigProvider) + plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service15, service13, requestConfigProvider) exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService) queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider) repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) @@ -580,7 +586,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) logger := loggermw.Provide(cfg, featureToggles) ngAlert := metrics2.ProvideService() - alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service13, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) + alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service15, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) if err != nil { return nil, err } @@ -617,7 +623,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service13, kvStore, featureToggles) + dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() @@ -628,11 +634,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } authnService := authnimpl.ProvideAuthnService(authnimplService) - openFeatureService, err := featuremgmt.ProvideOpenFeatureService(cfg) - if err != nil { - return nil, err - } - navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service11, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService, openFeatureService) + navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service13, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService) searchHTTPService := searchV2.ProvideSearchHTTPService(searchService) statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, featureToggles) gatherer := metrics.ProvideGatherer() @@ -647,9 +649,9 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer) + idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer) verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationService, idimplService) - httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) + httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service14, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service15, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service13, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) if err != nil { return nil, err } @@ -662,12 +664,12 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service13, httpclientProvider, sandboxService, advisorService) + statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service15, httpclientProvider, sandboxService, advisorService) internalMetricsService, err := metrics.ProvideService(cfg, registerer, gatherer) if err != nil { return nil, err } - supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service11, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) + supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service13, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) if err != nil { return nil, err } @@ -675,7 +677,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service13, cacheServiceImpl, plugincontextProvider) + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider) v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner) @@ -693,15 +695,15 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, client, sqlStore, serverLockService, folderimplService) playlistAppProvider := playlist.RegisterApp(playlistService, cfg, featureToggles) investigationsAppProvider := investigations.RegisterApp(cfg) - checkregistryService := checkregistry.ProvideService(service13, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) + checkregistryService := checkregistry.ProvideService(service15, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) appregistryService, err := appregistry.ProvideRegistryServiceSink(apiserverService, eventualRestConfigProvider, featureToggles, playlistAppProvider, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) if err != nil { return nil, err } - importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) - dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) + importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service14, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) + dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) sanitizerProvider := sanitizer.ProvideService(renderingService) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { @@ -715,7 +717,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service10.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service13, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) @@ -728,25 +730,65 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service13) - queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service13, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) + legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service15) + queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service15, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) if err != nil { return nil, err } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) - webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) - v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, v2) + databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer) if err != nil { return nil, err } - staticFlagEvaluator, err := featuremgmt.ProvideStaticEvaluator(cfg) + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, tracer, registerer) + if err != nil { + return nil, err + } + encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, registerer) + if err != nil { + return nil, err + } + cipher, err := service11.ProvideAESGCMCipherService(tracer, usageStats) + if err != nil { + return nil, err + } + providerConfig, err := kmsproviders.ProvideOSSKMSProviders(cfg, cipher) + if err != nil { + return nil, err + } + encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, usageStats, cipher, providerConfig) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + if err != nil { + return nil, err + } + secureValueService := service12.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) + decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) + decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer) + if err != nil { + return nil, err + } + decryptService := decrypt.ProvideDecryptService(decryptStorage) + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueService, decryptService) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, secureValueService, decryptService, factory, renderingService, resourceClient, eventualRestConfigProvider) + v2 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v2) + if err != nil { + return nil, err + } + ofrepAPIBuilder, err := ofrep.RegisterAPIService(apiserverService, cfg) if err != nil { return nil, err } - ofrepAPIBuilder := ofrep.RegisterAPIService(apiserverService, cfg, staticFlagEvaluator) secretDBMigrator := migrator2.NewWithEngine(sqlStore) dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) if err != nil { @@ -758,7 +800,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return nil, err } teamAPI := teamapi.ProvideTeamAPI(routeRegisterImpl, teamService, acimplService, accessControl, teamPermissionsService, userService, ossLicensingService, cfg, prefService, dashboardService, featureToggles) - cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service13, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service11, accessControl, acimplService, kvStore, libraryElementService, alertNG) + cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service15, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service13, accessControl, acimplService, kvStore, libraryElementService, alertNG) if err != nil { return nil, err } @@ -980,8 +1022,8 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { } extSvcAccountsService := extsvcaccounts.ProvideExtSvcAccountsService(acimplService, cfg, inProcBus, sqlStore, featureToggles, registerer, serviceAccountsService, secretsService, tracingService) registryRegistry := registry2.ProvideExtSvcRegistry(cfg, extSvcAccountsService, serverLockService, featureToggles) - service11 := service4.ProvideService(sqlStore, secretsService) - serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service11) + service13 := service4.ProvideService(sqlStore, secretsService) + serviceregistrationService := serviceregistration.ProvideService(cfg, featureToggles, registryRegistry, service13) initialize := pipeline.ProvideInitializationStage(pluginManagementCfg, inMemory, providerService, processService, serviceregistrationService, acimplService, actionSetService, envVarsProvider, tracingService) terminate, err := pipeline.ProvideTerminationStage(pluginManagementCfg, inMemory, processService) if err != nil { @@ -1004,7 +1046,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } pluginService := service5.ProvideDashboardPluginService(featureToggles, dashboardServiceImpl) - service12 := service6.ProvideService(fileStoreManager, pluginService) + service14 := service6.ProvideService(fileStoreManager, pluginService) oauthtokentestService := oauthtokentest.ProvideService() ossCachingService := caching.ProvideCachingService() middlewareHandler, err := pluginsintegration.ProvideClientWithMiddlewares(cfg, inMemory, oauthtokentestService, tracingService, ossCachingService, featureToggles, registerer) @@ -1044,11 +1086,11 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { datasourcePermissionsService := ossaccesscontrol.ProvideDatasourcePermissionsService(cfg, featureToggles, sqlStore) requestConfigProvider := pluginconfig.NewRequestConfigProvider(pluginInstanceCfg) baseProvider := plugincontext.ProvideBaseService(cfg, requestConfigProvider) - service13, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) + service15, err := service7.ProvideService(sqlStore, secretsService, secretsKVStore, cfg, featureToggles, accessControl, datasourcePermissionsService, quotaService, pluginstoreService, middlewareHandler, baseProvider) if err != nil { return nil, err } - correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service13, accessControl, inProcBus, quotaService, cfg) + correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service15, accessControl, inProcBus, quotaService, cfg) if err != nil { return nil, err } @@ -1065,7 +1107,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service13, correlationsService, dashboardService, folderimplService, service11, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService) if err != nil { return nil, err } @@ -1075,10 +1117,10 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { loginStore := authinfoimpl.ProvideStore(sqlStore, secretsService) authinfoimplService := authinfoimpl.ProvideService(loginStore, remoteCache, secretsService) oauthtokenService := oauthtoken.ProvideService(socialService, authinfoimplService, cfg, registerer, serverLockService, tracingService, userAuthTokenService, featureToggles) - dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service13, tracingService, secretsService, featureToggles) + dataSourceProxyService := datasourceproxy.ProvideService(cacheServiceImpl, ossDataSourceRequestValidator, pluginstoreService, cfg, httpclientProvider, oauthtokenService, service15, tracingService, secretsService, featureToggles) starService := starimpl.ProvideService(sqlStore) searchSearchService := search2.ProvideService(cfg, sqlStore, starService, dashboardService, folderimplService, featureToggles, sortService) - plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service13, service11, requestConfigProvider) + plugincontextProvider := plugincontext.ProvideService(cfg, cacheService, pluginstoreService, cacheServiceImpl, service15, service13, requestConfigProvider) exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService) queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider) repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) @@ -1093,7 +1135,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { logger := loggermw.Provide(cfg, featureToggles) notificationServiceMock := notifications.MockNotificationService() ngAlert := metrics2.ProvideServiceForTest() - alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service13, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationServiceMock, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) + alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service15, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationServiceMock, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) if err != nil { return nil, err } @@ -1130,7 +1172,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { csrfCSRF := csrf.ProvideCSRFFilter(cfg) playlistService := playlistimpl.ProvideService(sqlStore, tracingService) secretsMigrator := migrator.ProvideSecretsMigrator(serviceService, secretsService, sqlStore, ossImpl, featureToggles) - dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service13, kvStore, featureToggles) + dataSourceSecretMigrationService := migrations2.ProvideDataSourceMigrationService(service15, kvStore, featureToggles) secretMigrationProviderImpl := migrations2.ProvideSecretMigrationProvider(serverLockService, dataSourceSecretMigrationService) publicDashboardServiceImpl := service3.ProvideService(cfg, featureToggles, publicDashboardStoreImpl, queryServiceImpl, repositoryImpl, accessControl, publicDashboardServiceWrapperImpl, dashboardService, ossLicensingService) middleware := api2.ProvideMiddleware() @@ -1141,11 +1183,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } authnService := authnimpl.ProvideAuthnService(authnimplService) - openFeatureService, err := featuremgmt.ProvideOpenFeatureService(cfg) - if err != nil { - return nil, err - } - navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service11, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService, openFeatureService) + navtreeService := navtreeimpl.ProvideService(cfg, accessControl, pluginstoreService, service13, starService, featureToggles, dashboardService, acimplService, kvStore, apikeyService, ossLicensingService, authnService) searchHTTPService := searchV2.ProvideSearchHTTPService(searchService) statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, featureToggles) gatherer := metrics.ProvideGathererForTest(registerer) @@ -1160,9 +1198,9 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer) + idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer) verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationServiceMock, idimplService) - httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service12, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service13, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service11, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) + httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service14, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service15, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service13, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, noop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl) if err != nil { return nil, err } @@ -1175,12 +1213,12 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service13, httpclientProvider, sandboxService, advisorService) + statscollectorService := statscollector.ProvideService(usageStats, validatorService, statsService, cfg, sqlStore, socialService, pluginstoreService, featureManager, service15, httpclientProvider, sandboxService, advisorService) internalMetricsService, err := metrics.ProvideService(cfg, registerer, gatherer) if err != nil { return nil, err } - supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service11, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) + supportbundlesimplService, err := supportbundlesimpl.ProvideService(accessControl, acimplService, bundleregistryService, cfg, featureToggles, httpServer, kvStore, service13, pluginstoreService, routeRegisterImpl, ossImpl, sqlStore, usageStats, tracingService) if err != nil { return nil, err } @@ -1188,7 +1226,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service13, cacheServiceImpl, plugincontextProvider) + scopedPluginDatasourceProvider := datasource.ProvideDefaultPluginConfigs(service15, cacheServiceImpl, plugincontextProvider) v := builder.ProvideDefaultBuildHandlerChainFuncFromBuilders() aggregatorRunner := aggregatorrunner.ProvideNoopAggregatorConfigurator() apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner) @@ -1206,15 +1244,15 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { zanzanaReconciler := dualwrite2.ProvideZanzanaReconciler(cfg, featureToggles, client, sqlStore, serverLockService, folderimplService) playlistAppProvider := playlist.RegisterApp(playlistService, cfg, featureToggles) investigationsAppProvider := investigations.RegisterApp(cfg) - checkregistryService := checkregistry.ProvideService(service13, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) + checkregistryService := checkregistry.ProvideService(service15, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, noop, provisionedpluginsNoop, ssosettingsimplService, cfg, pluginerrsStore) advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) appregistryService, err := appregistry.ProvideRegistryServiceSink(apiserverService, eventualRestConfigProvider, featureToggles, playlistAppProvider, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) if err != nil { return nil, err } - importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service12, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) - dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service12, importDashboardService, service11, pluginService, dashboardService) + importDashboardService := service9.ProvideService(routeRegisterImpl, quotaService, service14, pluginstoreService, libraryPanelService, dashboardService, accessControl, folderimplService, featureToggles) + dashboardUpdater := service6.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) sanitizerProvider := sanitizer.ProvideService(renderingService) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { @@ -1228,7 +1266,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service10.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service13, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) @@ -1241,25 +1279,65 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service13) - queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service13, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) + legacyDataSourceLookup := service7.ProvideLegacyDataSourceLookup(service15) + queryAPIBuilder, err := query2.RegisterAPIService(featureToggles, apiserverService, service15, pluginstoreService, accessControl, middlewareHandler, plugincontextProvider, registerer, tracingService, legacyDataSourceLookup) if err != nil { return nil, err } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl) - webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, factory, renderingService, resourceClient, eventualRestConfigProvider) - v2 := apiregistry.MergeProvisioningExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, secretsService, v2) + databaseDatabase := database5.ProvideDatabase(sqlStore, tracer) + secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer) if err != nil { return nil, err } - staticFlagEvaluator, err := featuremgmt.ProvideStaticEvaluator(cfg) + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(databaseDatabase, tracer, registerer) + if err != nil { + return nil, err + } + encryptedValueStorage, err := encryption.ProvideEncryptedValueStorage(databaseDatabase, tracer) + if err != nil { + return nil, err + } + dataKeyStorage, err := encryption.ProvideDataKeyStorage(databaseDatabase, tracer, registerer) + if err != nil { + return nil, err + } + cipher, err := service11.ProvideAESGCMCipherService(tracer, usageStats) + if err != nil { + return nil, err + } + providerConfig, err := kmsproviders.ProvideOSSKMSProviders(cfg, cipher) + if err != nil { + return nil, err + } + encryptionManager, err := manager4.ProvideEncryptionManager(tracer, dataKeyStorage, usageStats, cipher, providerConfig) + if err != nil { + return nil, err + } + ossKeeperService, err := secretkeeper.ProvideService(tracer, encryptedValueStorage, encryptionManager, registerer) + if err != nil { + return nil, err + } + secureValueService := service12.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) + decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) + decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer) + if err != nil { + return nil, err + } + decryptService := decrypt.ProvideDecryptService(decryptStorage) + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueService, decryptService) + webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, secretsService, secureValueService, decryptService, factory, renderingService, resourceClient, eventualRestConfigProvider) + v2 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v2) + if err != nil { + return nil, err + } + ofrepAPIBuilder, err := ofrep.RegisterAPIService(apiserverService, cfg) if err != nil { return nil, err } - ofrepAPIBuilder := ofrep.RegisterAPIService(apiserverService, cfg, staticFlagEvaluator) secretDBMigrator := migrator2.NewWithEngine(sqlStore) dependencyRegisterer, err := secret.RegisterDependencies(featureToggles, cfg, secretDBMigrator, acimplService) if err != nil { @@ -1271,7 +1349,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { return nil, err } teamAPI := teamapi.ProvideTeamAPI(routeRegisterImpl, teamService, acimplService, accessControl, teamPermissionsService, userService, ossLicensingService, cfg, prefService, dashboardService, featureToggles) - cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service13, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service11, accessControl, acimplService, kvStore, libraryElementService, alertNG) + cloudmigrationService, err := cloudmigrationimpl.ProvideService(cfg, httpclientProvider, featureToggles, sqlStore, service15, secretsKVStore, secretsService, routeRegisterImpl, registerer, tracingService, dashboardService, folderimplService, pluginstoreService, service13, accessControl, acimplService, kvStore, libraryElementService, alertNG) if err != nil { return nil, err } @@ -1287,7 +1365,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory) + testEnv, err := ProvideTestEnv(testingT, server, sqlStore, cfg, notificationServiceMock, grpcserverProvider, inMemory, httpclientProvider, oauthtokentestService, featureToggles, resourceClient, idimplService, factory, repositorySecrets) if err != nil { return nil, err } @@ -1378,9 +1456,17 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp storageMetrics := resource.ProvideStorageMetrics(registerer) bleveIndexMetrics := resource.ProvideIndexMetrics(registerer) gatherer := metrics.ProvideGatherer() + tracingConfig, err := tracing.ProvideTracingConfig(cfg) + if err != nil { + return nil, err + } + tracingService, err := tracing.ProvideService(tracingConfig) + if err != nil { + return nil, err + } hooksService := hooks.ProvideService() ossLicensingService := licensing.ProvideService(cfg, hooksService) - moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, ossLicensingService) + moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService) if err != nil { return nil, err } @@ -1428,7 +1514,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, featuremgmt.ProvideOpenFeatureService, featuremgmt.ProvideStaticEvaluator, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideOutboxQueue, encryption2.ProvideDataKeyStorage, encryption2.ProvideEncryptedValueStorage, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, encryption3.ProvideThirdPartyProviderMap, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptAllowList, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator2.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service10.ProvideService, wire.Bind(new(service10.LDAP), new(*service10.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service7.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service7.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets2.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets2.Store), new(*database.SecretsStoreImpl)), grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database4.DashboardSnapshotStore)), database4.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service8.ServiceImpl)), service8.ProvideService, service7.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service7.Service)), service7.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager2.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service5.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service5.DashboardServiceImpl)), service5.ProvideDashboardService, service5.ProvideDashboardProvisioningService, service5.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), service9.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service9.ImportDashboardService)), service6.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service6.Service)), service6.ProvideDashboardUpdater, sanitizer.ProvideService, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, decrypt.ProvideDecryptService, encryption.ProvideDataKeyStorage, encryption.ProvideEncryptedValueStorage, service12.ProvideSecureValueService, validator3.ProvideKeeperValidator, validator3.ProvideSecureValueValidator, migrator2.NewWithEngine, database5.ProvideDatabase, wire.Bind(new(contracts.Database), new(*database5.Database)), manager4.ProvideEncryptionManager, service11.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 4c2e5f8b14d..096148c665e 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -8,10 +8,15 @@ import ( "github.com/google/wire" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" + apisregistry "github.com/grafana/grafana/pkg/registry/apis" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/extras" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + gsmKMSProviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" @@ -59,6 +64,11 @@ import ( search2 "github.com/grafana/grafana/pkg/storage/unified/search" ) +var provisioningExtras = wire.NewSet( + webhooks.ProvideWebhooks, + extras.ProvideProvisioningOSSExtras, +) + var wireExtsBasicSet = wire.NewSet( authimpl.ProvideUserAuthTokenService, wire.Bind(new(auth.UserTokenService), new(*authimpl.UserAuthTokenService)), @@ -127,6 +137,9 @@ var wireExtsBasicSet = wire.NewSet( unified.ProvideUnifiedStorageClient, builder.ProvideDefaultBuildHandlerChainFuncFromBuilders, aggregatorrunner.ProvideNoopAggregatorConfigurator, + apisregistry.WireSetExts, + gsmKMSProviders.ProvideOSSKMSProviders, + provisioningExtras, ) var wireExtsSet = wire.NewSet( @@ -153,7 +166,6 @@ var wireExtsBaseCLISet = wire.NewSet( metrics.WireSet, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, - featuremgmt.ProvideOpenFeatureService, hooks.ProvideService, setting.ProvideProvider, wire.Bind(new(setting.Provider), new(*setting.OSSImpl)), licensing.ProvideService, wire.Bind(new(licensing.Licensing), new(*licensing.OSSLicensingService)), @@ -163,6 +175,10 @@ var wireExtsBaseCLISet = wire.NewSet( var wireExtsModuleServerSet = wire.NewSet( NewModule, wireExtsBaseCLISet, + // Tracing + tracing.ProvideTracingConfig, + tracing.ProvideService, + wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, diff --git a/pkg/services/accesscontrol/actest/store_mock.go b/pkg/services/accesscontrol/actest/store_mock.go index ea2a48fe560..84e5bef1eb7 100644 --- a/pkg/services/accesscontrol/actest/store_mock.go +++ b/pkg/services/accesscontrol/actest/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package actest diff --git a/pkg/services/annotations/annotations_repository_mock.go b/pkg/services/annotations/annotations_repository_mock.go index 242834ced2b..f9cab3f896e 100644 --- a/pkg/services/annotations/annotations_repository_mock.go +++ b/pkg/services/annotations/annotations_repository_mock.go @@ -1,10 +1,9 @@ -// Code generated by mockery v2.12.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package annotations import ( context "context" - testing "testing" mock "github.com/stretchr/testify/mock" ) @@ -18,6 +17,10 @@ type FakeAnnotationsRepo struct { func (_m *FakeAnnotationsRepo) Delete(ctx context.Context, params *DeleteParams) error { ret := _m.Called(ctx, params) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteParams) error); ok { r0 = rf(ctx, params) @@ -32,7 +35,15 @@ func (_m *FakeAnnotationsRepo) Delete(ctx context.Context, params *DeleteParams) func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*ItemDTO, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for Find") + } + var r0 []*ItemDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *ItemQuery) ([]*ItemDTO, error)); ok { + return rf(ctx, query) + } if rf, ok := ret.Get(0).(func(context.Context, *ItemQuery) []*ItemDTO); ok { r0 = rf(ctx, query) } else { @@ -41,7 +52,6 @@ func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*I } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *ItemQuery) error); ok { r1 = rf(ctx, query) } else { @@ -55,14 +65,21 @@ func (_m *FakeAnnotationsRepo) Find(ctx context.Context, query *ItemQuery) ([]*I func (_m *FakeAnnotationsRepo) FindTags(ctx context.Context, query *TagsQuery) (FindTagsResult, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for FindTags") + } + var r0 FindTagsResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *TagsQuery) (FindTagsResult, error)); ok { + return rf(ctx, query) + } if rf, ok := ret.Get(0).(func(context.Context, *TagsQuery) FindTagsResult); ok { r0 = rf(ctx, query) } else { r0 = ret.Get(0).(FindTagsResult) } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *TagsQuery) error); ok { r1 = rf(ctx, query) } else { @@ -76,6 +93,10 @@ func (_m *FakeAnnotationsRepo) FindTags(ctx context.Context, query *TagsQuery) ( func (_m *FakeAnnotationsRepo) Save(ctx context.Context, item *Item) error { ret := _m.Called(ctx, item) + if len(ret) == 0 { + panic("no return value specified for Save") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok { r0 = rf(ctx, item) @@ -90,6 +111,10 @@ func (_m *FakeAnnotationsRepo) Save(ctx context.Context, item *Item) error { func (_m *FakeAnnotationsRepo) SaveMany(ctx context.Context, items []Item) error { ret := _m.Called(ctx, items) + if len(ret) == 0 { + panic("no return value specified for SaveMany") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, []Item) error); ok { r0 = rf(ctx, items) @@ -104,6 +129,10 @@ func (_m *FakeAnnotationsRepo) SaveMany(ctx context.Context, items []Item) error func (_m *FakeAnnotationsRepo) Update(ctx context.Context, item *Item) error { ret := _m.Called(ctx, item) + if len(ret) == 0 { + panic("no return value specified for Update") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *Item) error); ok { r0 = rf(ctx, item) @@ -114,8 +143,12 @@ func (_m *FakeAnnotationsRepo) Update(ctx context.Context, item *Item) error { return r0 } -// NewFakeAnnotationsRepo creates a new instance of FakeAnnotationsRepo. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakeAnnotationsRepo(t testing.TB) *FakeAnnotationsRepo { +// NewFakeAnnotationsRepo creates a new instance of FakeAnnotationsRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFakeAnnotationsRepo(t interface { + mock.TestingT + Cleanup(func()) +}) *FakeAnnotationsRepo { mock := &FakeAnnotationsRepo{} mock.Mock.Test(t) diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index b708e372d16..cddbb1e4b15 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -9,8 +9,8 @@ import ( spec "k8s.io/kube-openapi/pkg/validation/spec" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + secret "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - secret "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" ) // This should eventually live in grafana-app-sdk diff --git a/pkg/services/apiserver/builder/runner/builder.go b/pkg/services/apiserver/builder/runner/builder.go index a732c97b5a4..6e150273878 100644 --- a/pkg/services/apiserver/builder/runner/builder.go +++ b/pkg/services/apiserver/builder/runner/builder.go @@ -6,6 +6,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" @@ -132,6 +133,11 @@ func (b *appBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI return err } apiGroupInfo.VersionedResourcesStorageMap[version][resourceInfo.StoragePath()] = store + if registryStore, ok := store.(*genericregistry.Store); ok { + for subPath := range kind.ZeroValue().GetSubresources() { + apiGroupInfo.VersionedResourcesStorageMap[version][resourceInfo.StoragePath(subPath)] = grafanaregistry.NewRegistryStatusStore(opts.Scheme, registryStore) + } + } } } return nil diff --git a/pkg/services/apiserver/options/storage.go b/pkg/services/apiserver/options/storage.go index d6c700303b6..2ff900037fd 100644 --- a/pkg/services/apiserver/options/storage.go +++ b/pkg/services/apiserver/options/storage.go @@ -22,10 +22,11 @@ import ( type StorageType string const ( - StorageTypeFile StorageType = "file" - StorageTypeEtcd StorageType = "etcd" - StorageTypeUnified StorageType = "unified" - StorageTypeUnifiedGrpc StorageType = "unified-grpc" + StorageTypeFile StorageType = "file" + StorageTypeEtcd StorageType = "etcd" + StorageTypeUnified StorageType = "unified" + StorageTypeUnifiedGrpc StorageType = "unified-grpc" + StorageTypeUnifiedKVGrpc StorageType = "unified-kv-grpc" // Deprecated: legacy is a shim that is no longer necessary StorageTypeLegacy StorageType = "legacy" @@ -95,6 +96,8 @@ func (o *StorageOptions) Validate() []error { // nolint:staticcheck case StorageTypeLegacy: // no-op + case StorageTypeUnifiedKVGrpc: + // no-op (enterprise only) case StorageTypeFile, StorageTypeEtcd, StorageTypeUnified, StorageTypeUnifiedGrpc: // no-op default: diff --git a/pkg/services/auth/authimpl/auth_token.go b/pkg/services/auth/authimpl/auth_token.go index bf95502b11e..e8a00ff8c85 100644 --- a/pkg/services/auth/authimpl/auth_token.go +++ b/pkg/services/auth/authimpl/auth_token.go @@ -80,6 +80,9 @@ type UserAuthTokenService struct { } func (s *UserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.CreateToken") + defer span.End() + token, hashedToken, err := generateAndHashToken(s.cfg.SecretKey) if err != nil { return nil, err @@ -136,6 +139,9 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.Create } func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.LookupToken") + defer span.End() + hashedToken := hashToken(s.cfg.SecretKey, unhashedToken) var model userAuthToken var exists bool @@ -234,6 +240,9 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st } func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetTokenByExternalSessionID") + defer span.End() + var token userAuthToken err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { exists, err := dbSession.Where("external_session_id = ?", externalSessionID).Get(&token) @@ -258,14 +267,23 @@ func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, } func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetExternalSession") + defer span.End() + return s.externalSessionStore.Get(ctx, externalSessionID) } func (s *UserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.FindExternalSessions") + defer span.End() + return s.externalSessionStore.List(ctx, query) } func (s *UserAuthTokenService) UpdateExternalSession(ctx context.Context, externalSessionID int64, cmd *auth.UpdateExternalSessionCommand) error { + ctx, span := s.tracer.Start(ctx, "authtoken.UpdateExternalSession") + defer span.End() + return s.externalSessionStore.Update(ctx, externalSessionID, cmd) } @@ -329,6 +347,9 @@ func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateC } func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.rotateToken") + defer span.End() + var clientIPStr string if clientIP != nil { clientIPStr = clientIP.String() @@ -385,6 +406,9 @@ func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.User } func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.UserToken, soft bool) error { + ctx, span := s.tracer.Start(ctx, "authtoken.RevokeToken") + defer span.End() + if token == nil { return auth.ErrUserTokenNotFound } @@ -434,6 +458,9 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User } func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error { + ctx, span := s.tracer.Start(ctx, "authtoken.RevokeAllUserTokens") + defer span.End() + return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error { ctxLogger := s.log.FromContext(ctx) err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { @@ -466,6 +493,9 @@ func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId i } func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error { + ctx, span := s.tracer.Start(ctx, "authtoken.BatchRevokeAllUserTokens") + defer span.End() + return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error { ctxLogger := s.log.FromContext(ctx) if len(userIds) == 0 { @@ -507,6 +537,9 @@ func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, use } func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetUserToken") + defer span.End() + var result auth.UserToken err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { var token userAuthToken @@ -526,6 +559,9 @@ func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTok } func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetUserTokens") + defer span.End() + result := []*auth.UserToken{} err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { var tokens []*userAuthToken @@ -554,6 +590,9 @@ func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) // ActiveTokenCount returns the number of active tokens. If userID is nil, the count is for all users. func (s *UserAuthTokenService) ActiveTokenCount(ctx context.Context, userID *int64) (int64, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.ActiveTokenCount") + defer span.End() + if userID != nil && *userID < 1 { return 0, errUserIDInvalid } @@ -574,6 +613,9 @@ func (s *UserAuthTokenService) ActiveTokenCount(ctx context.Context, userID *int } func (s *UserAuthTokenService) DeleteUserRevokedTokens(ctx context.Context, userID int64, window time.Duration) error { + ctx, span := s.tracer.Start(ctx, "authtoken.DeleteUserRevokedTokens") + defer span.End() + return s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { query := "DELETE FROM user_auth_token WHERE user_id = ? AND revoked_at > 0 AND revoked_at <= ?" res, err := sess.Exec(query, userID, time.Now().Add(-window).Unix()) @@ -592,6 +634,9 @@ func (s *UserAuthTokenService) DeleteUserRevokedTokens(ctx context.Context, user } func (s *UserAuthTokenService) GetUserRevokedTokens(ctx context.Context, userId int64) ([]*auth.UserToken, error) { + ctx, span := s.tracer.Start(ctx, "authtoken.GetUserRevokedTokens") + defer span.End() + result := []*auth.UserToken{} err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { var tokens []*userAuthToken diff --git a/pkg/services/auth/authtest/auth_token_service_mock.go b/pkg/services/auth/authtest/auth_token_service_mock.go index 744ef1fe0b0..11ea10a4d7b 100644 --- a/pkg/services/auth/authtest/auth_token_service_mock.go +++ b/pkg/services/auth/authtest/auth_token_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package authtest diff --git a/pkg/services/auth/authtest/external_session_store_mock.go b/pkg/services/auth/authtest/external_session_store_mock.go index 10894e67444..e4f727e14ec 100644 --- a/pkg/services/auth/authtest/external_session_store_mock.go +++ b/pkg/services/auth/authtest/external_session_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package authtest diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index 5dec411fdee..cb8e6adc9ce 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -8,6 +8,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" "golang.org/x/sync/singleflight" authnlib "github.com/grafana/authlib/authn" @@ -32,18 +33,18 @@ var _ auth.IDService = (*Service)(nil) func ProvideService( cfg *setting.Cfg, signer auth.IDSigner, - cache remotecache.CacheStorage, - authnService authn.Service, - reg prometheus.Registerer, + cache remotecache.CacheStorage, authnService authn.Service, + reg prometheus.Registerer, tracer trace.Tracer, ) *Service { s := &Service{ cfg: cfg, logger: log.New("id-service"), signer: signer, cache: cache, metrics: newMetrics(reg), nsMapper: request.GetNamespaceMapper(cfg), + tracer: tracer, } - authnService.RegisterPostAuthHook(s.hook, 140) + authnService.RegisterPostAuthHook(s.SyncIDToken, 140) return s } @@ -55,10 +56,14 @@ type Service struct { cache remotecache.CacheStorage si singleflight.Group metrics *metrics + tracer trace.Tracer nsMapper request.NamespaceMapper } func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) { + ctx, span := s.tracer.Start(ctx, "user.sync.SignIdentity") + defer span.End() + defer func(t time.Time) { s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds()) }(time.Now()) @@ -140,10 +145,15 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri } func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error { + ctx, span := s.tracer.Start(ctx, "user.sync.RemoveIDToken") + defer span.End() + return s.cache.Delete(ctx, getCacheKey(id)) } -func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { +func (s *Service) SyncIDToken(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + ctx, span := s.tracer.Start(ctx, "user.sync.SyncIDToken") + defer span.End() // FIXME(kalleep): we should probably lazy load this token, idClaims, err := s.SignIdentity(ctx, identity) if err != nil { diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go index bb7ee510e55..8690c5c24bc 100644 --- a/pkg/services/auth/idimpl/service_test.go +++ b/pkg/services/auth/idimpl/service_test.go @@ -11,6 +11,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/idtest" "github.com/grafana/grafana/pkg/services/authn" @@ -29,7 +30,7 @@ func Test_ProvideService(t *testing.T) { }, } - _ = ProvideService(setting.NewCfg(), nil, nil, authnService, nil) + _ = ProvideService(setting.NewCfg(), nil, nil, authnService, nil, tracing.InitializeTracerForTest()) assert.True(t, hookRegistered) }) } @@ -51,7 +52,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign identity", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) token, _, err := s.SignIdentity(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}) require.NoError(t, err) @@ -61,7 +62,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign identity with authenticated by if user is externally authenticated", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) token, _, err := s.SignIdentity(context.Background(), &authn.Identity{ ID: "1", @@ -86,7 +87,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign identity with authenticated by if user is externally authenticated", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) _, gotClaims, err := s.SignIdentity(context.Background(), &authn.Identity{ ID: "1", @@ -106,7 +107,7 @@ func TestService_SignIdentity(t *testing.T) { t.Run("should sign new token if org role has changed", func(t *testing.T) { s := ProvideService( setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(), - &authntest.FakeService{}, nil, + &authntest.FakeService{}, nil, tracing.InitializeTracerForTest(), ) ident := &authn.Identity{ diff --git a/pkg/services/auth/idtest/mock.go b/pkg/services/auth/idtest/mock.go index cb9aeadf316..9837c421cd8 100644 --- a/pkg/services/auth/idtest/mock.go +++ b/pkg/services/auth/idtest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package idtest diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 7eadac99083..ce0ed9a9e89 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -48,10 +48,10 @@ func ProvideRegistration( logger := log.New("authn.registration") authnSvc.RegisterClient(clients.ProvideRender(renderService)) - authnSvc.RegisterClient(clients.ProvideAPIKey(apikeyService)) + authnSvc.RegisterClient(clients.ProvideAPIKey(apikeyService, tracer)) if cfg.LoginCookieName != "" { - authnSvc.RegisterClient(clients.ProvideSession(cfg, sessionService, authInfoService)) + authnSvc.RegisterClient(clients.ProvideSession(cfg, sessionService, authInfoService, tracer)) } var proxyClients []authn.ProxyClient @@ -59,20 +59,20 @@ func ProvideRegistration( // always register LDAP if LDAP is enabled in SSO settings if cfg.LDAPAuthEnabled || features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) { - ldap := clients.ProvideLDAP(cfg, ldapService, userService, authInfoService) + ldap := clients.ProvideLDAP(cfg, ldapService, userService, authInfoService, tracer) proxyClients = append(proxyClients, ldap) passwordClients = append(passwordClients, ldap) } if !cfg.DisableLogin { - grafana := clients.ProvideGrafana(cfg, userService) + grafana := clients.ProvideGrafana(cfg, userService, tracer) proxyClients = append(proxyClients, grafana) passwordClients = append(passwordClients, grafana) } // if we have password clients configure check if basic auth or form auth is enabled if len(passwordClients) > 0 { - passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...) + passwordClient := clients.ProvidePassword(loginAttempts, tracer, passwordClients...) if cfg.BasicAuthEnabled { authnSvc.RegisterClient(clients.ProvideBasic(passwordClient)) } @@ -103,7 +103,7 @@ func ProvideRegistration( } if cfg.AuthProxy.Enabled && len(proxyClients) > 0 { - proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...) + proxy, err := clients.ProvideProxy(cfg, cache, tracer, proxyClients...) if err != nil { logger.Error("Failed to configure auth proxy", "err", err) } else { @@ -113,16 +113,16 @@ func ProvideRegistration( if cfg.JWTAuth.Enabled { orgRoleMapper := connectors.ProvideOrgRoleMapper(cfg, orgService) - authnSvc.RegisterClient(clients.ProvideJWT(jwtService, orgRoleMapper, cfg)) + authnSvc.RegisterClient(clients.ProvideJWT(jwtService, orgRoleMapper, cfg, tracer)) } if cfg.ExtJWTAuth.Enabled { - authnSvc.RegisterClient(clients.ProvideExtendedJWT(cfg)) + authnSvc.RegisterClient(clients.ProvideExtendedJWT(cfg, tracer)) } for name := range socialService.GetOAuthProviders() { clientName := authn.ClientWithPrefix(name) - authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features)) + authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features, tracer)) } if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index 66879861146..69cbc6b8e08 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -133,7 +133,6 @@ func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, currentIden log := s.log.FromContext(ctx).New("auth_module", currentIdentity.AuthenticatedBy, "auth_id", currentIdentity.AuthID) if !currentIdentity.ClientParams.SyncUser { - log.Debug("Skipping user provisioning validation, syncUser is disabled") return nil } diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index 5bc4aa8fb46..2023cb4e418 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "go.opentelemetry.io/otel/trace" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/components/apikeygen" @@ -35,16 +37,18 @@ const ( metaKeySkipLastUsed = "keySkipLastUsed" ) -func ProvideAPIKey(apiKeyService apikey.Service) *APIKey { +func ProvideAPIKey(apiKeyService apikey.Service, tracer trace.Tracer) *APIKey { return &APIKey{ log: log.New(authn.ClientAPIKey), apiKeyService: apiKeyService, + tracer: tracer, } } type APIKey struct { log log.Logger apiKeyService apikey.Service + tracer trace.Tracer } func (s *APIKey) Name() string { @@ -52,6 +56,8 @@ func (s *APIKey) Name() string { } func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.Authenticate") + defer span.End() key, err := s.getAPIKey(ctx, getTokenFromRequest(r)) if err != nil { if errors.Is(err, apikeygen.ErrInvalidApiKey) { @@ -84,6 +90,8 @@ func (s *APIKey) IsEnabled() bool { } func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.getAPIKey") + defer span.End() fn := s.getFromToken if !strings.HasPrefix(token, satokengen.GrafanaPrefix) { fn = s.getFromTokenLegacy @@ -98,6 +106,8 @@ func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, e } func (s *APIKey) getFromToken(ctx context.Context, token string) (*apikey.APIKey, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.getFromToken") + defer span.End() decoded, err := satokengen.Decode(token) if err != nil { return nil, err @@ -112,6 +122,8 @@ func (s *APIKey) getFromToken(ctx context.Context, token string) (*apikey.APIKey } func (s *APIKey) getFromTokenLegacy(ctx context.Context, token string) (*apikey.APIKey, error) { + ctx, span := s.tracer.Start(ctx, "authn.apikey.getFromTokenLegacy") + defer span.End() decoded, err := apikeygen.Decode(token) if err != nil { return nil, err @@ -144,6 +156,9 @@ func (s *APIKey) Priority() uint { } func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error { + ctx, span := s.tracer.Start(ctx, "authn.apikey.Hook") //nolint:ineffassign,staticcheck + defer span.End() + if r.GetMeta(metaKeySkipLastUsed) != "" { return nil } diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go index 889f38cd4d0..62fc83bc7f0 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -12,6 +12,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/components/satokengen" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey/apikeytest" "github.com/grafana/grafana/pkg/services/authn" @@ -106,7 +107,7 @@ func TestAPIKey_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideAPIKey(&apikeytest.Service{ExpectedAPIKey: tt.expectedKey}) + c := ProvideAPIKey(&apikeytest.Service{ExpectedAPIKey: tt.expectedKey}, tracing.InitializeTracerForTest()) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { @@ -173,7 +174,7 @@ func TestAPIKey_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideAPIKey(&apikeytest.Service{}) + c := ProvideAPIKey(&apikeytest.Service{}, tracing.InitializeTracerForTest()) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 74754d85681..5baafdf0c6b 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/go-jose/go-jose/v3/jwt" + "go.opentelemetry.io/otel/trace" authlib "github.com/grafana/authlib/authn" claims "github.com/grafana/authlib/types" @@ -41,7 +42,7 @@ var ( ) ) -func ProvideExtendedJWT(cfg *setting.Cfg) *ExtendedJWT { +func ProvideExtendedJWT(cfg *setting.Cfg, tracer trace.Tracer) *ExtendedJWT { keys := authlib.NewKeyRetriever(authlib.KeyRetrieverConfig{ SigningKeysURL: cfg.ExtJWTAuth.JWKSUrl, }) @@ -60,6 +61,7 @@ func ProvideExtendedJWT(cfg *setting.Cfg) *ExtendedJWT { namespaceMapper: request.GetNamespaceMapper(cfg), accessTokenVerifier: accessTokenVerifier, idTokenVerifier: idTokenVerifier, + tracer: tracer, } } @@ -69,9 +71,13 @@ type ExtendedJWT struct { accessTokenVerifier authlib.Verifier[authlib.AccessTokenClaims] idTokenVerifier authlib.Verifier[authlib.IDTokenClaims] namespaceMapper request.NamespaceMapper + tracer trace.Tracer } func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.extjwt.Authenticate") + defer span.End() + jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest) accessTokenClaims, err := s.accessTokenVerifier.Verify(ctx, jwtToken) diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 13970e681be..b72b6303154 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -17,6 +17,7 @@ import ( authnlib "github.com/grafana/authlib/authn" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/setting" ) @@ -699,7 +700,7 @@ func setupTestCtx(cfg *setting.Cfg) *testEnv { } } - extJwtClient := ProvideExtendedJWT(cfg) + extJwtClient := ProvideExtendedJWT(cfg, tracing.InitializeTracerForTest()) return &testEnv{ s: extJwtClient, diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index ddabb405443..379c19ffca3 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -7,6 +7,8 @@ import ( "net/mail" "strconv" + "go.opentelemetry.io/otel/trace" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" @@ -19,13 +21,14 @@ import ( var _ authn.ProxyClient = new(Grafana) var _ authn.PasswordClient = new(Grafana) -func ProvideGrafana(cfg *setting.Cfg, userService user.Service) *Grafana { - return &Grafana{cfg, userService} +func ProvideGrafana(cfg *setting.Cfg, userService user.Service, tracer trace.Tracer) *Grafana { + return &Grafana{cfg, userService, tracer} } type Grafana struct { cfg *setting.Cfg userService user.Service + tracer trace.Tracer } func (c *Grafana) String() string { @@ -33,6 +36,9 @@ func (c *Grafana) String() string { } func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.grafana.AuthenticateProxy") //nolint:ineffassign,staticcheck + defer span.End() + identity := &authn.Identity{ AuthenticatedBy: login.AuthProxyAuthModule, AuthID: username, @@ -91,6 +97,9 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern } func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.grafana.AuthenticatePassword") + defer span.End() + usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username}) if err != nil { if errors.Is(err, user.ErrUserNotFound) { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index fd93cf2faa7..fd40757e319 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -97,7 +98,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { cfg := setting.NewCfg() cfg.AuthProxy.AutoSignUp = true cfg.AuthProxy.HeaderProperty = tt.proxyProperty - c := ProvideGrafana(cfg, usertest.NewUserServiceFake()) + c := ProvideGrafana(cfg, usertest.NewUserServiceFake(), tracing.InitializeTracerForTest()) identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional) assert.ErrorIs(t, err, tt.expectedErr) @@ -175,7 +176,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { userService.ExpectedError = user.ErrUserNotFound } - c := ProvideGrafana(setting.NewCfg(), userService) + c := ProvideGrafana(setting.NewCfg(), userService, tracing.InitializeTracerForTest()) identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password) assert.ErrorIs(t, err, tt.expectedErr) assert.EqualValues(t, tt.expectedIdentity, identity) diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 82323e21bfe..60d7ed21406 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social/connectors" @@ -30,13 +32,14 @@ var ( "jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim")) ) -func ProvideJWT(jwtService auth.JWTVerifierService, orgRoleMapper *connectors.OrgRoleMapper, cfg *setting.Cfg) *JWT { +func ProvideJWT(jwtService auth.JWTVerifierService, orgRoleMapper *connectors.OrgRoleMapper, cfg *setting.Cfg, tracer trace.Tracer) *JWT { return &JWT{ cfg: cfg, log: log.New(authn.ClientJWT), jwtService: jwtService, orgRoleMapper: orgRoleMapper, orgMappingCfg: orgRoleMapper.ParseOrgMappingSettings(context.Background(), cfg.JWTAuth.OrgMapping, cfg.JWTAuth.RoleAttributeStrict), + tracer: tracer, } } @@ -46,6 +49,7 @@ type JWT struct { orgMappingCfg connectors.MappingConfiguration log log.Logger jwtService auth.JWTVerifierService + tracer trace.Tracer } func (s *JWT) Name() string { @@ -53,6 +57,9 @@ func (s *JWT) Name() string { } func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := s.tracer.Start(ctx, "authn.jwt.Authenticate") + defer span.End() + jwtToken := s.retrieveToken(r.HTTPRequest) s.stripSensitiveParam(r.HTTPRequest) @@ -114,9 +121,6 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi if !s.cfg.JWTAuth.SkipOrgRoleSync { role, grafanaAdmin := s.extractRoleAndAdmin(claims) - if err != nil { - s.log.Warn("Failed to extract role", "err", err) - } if s.cfg.JWTAuth.AllowAssignGrafanaAdmin { id.IsGrafanaAdmin = &grafanaAdmin diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 7fccffc6b17..2de491935b7 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social/connectors" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" @@ -262,7 +263,7 @@ func TestAuthenticateJWT(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(tc.cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - tc.cfg) + tc.cfg, tracing.InitializeTracerForTest()) validHTTPReq := &http.Request{ Header: map[string][]string{ jwtHeaderName: {"sample-token"}}, @@ -380,7 +381,7 @@ func TestJWTClaimConfig(t *testing.T) { } jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, @@ -493,7 +494,7 @@ func TestJWTTest(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) httpReq := &http.Request{ URL: &url.URL{RawQuery: "auth_token=" + tc.token}, Header: map[string][]string{ @@ -549,7 +550,7 @@ func TestJWTStripParam(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, @@ -608,7 +609,7 @@ func TestJWTSubClaimsConfig(t *testing.T) { jwtClient := ProvideJWT(jwtService, connectors.ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}), - cfg) + cfg, tracing.InitializeTracerForTest()) identity, err := jwtClient.Authenticate(context.Background(), &authn.Request{ OrgID: 1, HTTPRequest: httpReq, diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index e78f0ff119c..4f9cbccf066 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -4,6 +4,8 @@ import ( "context" "errors" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap/multildap" @@ -20,8 +22,8 @@ type ldapService interface { User(username string) (*login.ExternalUserInfo, error) } -func ProvideLDAP(cfg *setting.Cfg, ldapService ldapService, userService user.Service, authInfoService login.AuthInfoService) *LDAP { - return &LDAP{cfg, log.New("authn.ldap"), ldapService, userService, authInfoService} +func ProvideLDAP(cfg *setting.Cfg, ldapService ldapService, userService user.Service, authInfoService login.AuthInfoService, tracer trace.Tracer) *LDAP { + return &LDAP{cfg, log.New("authn.ldap"), ldapService, userService, authInfoService, tracer} } type LDAP struct { @@ -30,6 +32,7 @@ type LDAP struct { service ldapService userService user.Service authInfoService login.AuthInfoService + tracer trace.Tracer } func (c *LDAP) String() string { @@ -37,6 +40,8 @@ func (c *LDAP) String() string { } func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, _ map[string]string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.ldap.AuthenticateProxy") + defer span.End() info, err := c.service.User(username) if errors.Is(err, multildap.ErrDidNotFindUser) { return c.disableUser(ctx, username) @@ -50,6 +55,8 @@ func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username } func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.ldap.AuthenticatePassword") + defer span.End() info, err := c.service.Login(&login.LoginUserQuery{ Username: username, Password: password, @@ -75,6 +82,8 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern // disableUser will disable users if they logged in via LDAP previously func (c *LDAP) disableUser(ctx context.Context, username string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.ldap.disableUser") + defer span.End() c.logger.Debug("User was not found in the LDAP directory tree", "username", username) retErr := errIdentityNotFound.Errorf("no user found: %w", multildap.ErrDidNotFindUser) diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index cd08fa2ac01..2d09ef5c933 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ldap/multildap" @@ -197,13 +197,13 @@ func setupLDAPTestCase(tt *ldapTestCase) *LDAP { ExpectedError: tt.expectedAuthInfoErr, } - c := &LDAP{ - cfg: setting.NewCfg(), - logger: log.New("authn.ldap.test"), - service: &service.LDAPFakeService{ExpectedUser: tt.expectedLDAPInfo, ExpectedError: tt.expectedLDAPErr}, - userService: userService, - authInfoService: authInfoService, - } + c := ProvideLDAP( + setting.NewCfg(), + &service.LDAPFakeService{ExpectedUser: tt.expectedLDAPInfo, ExpectedError: tt.expectedLDAPErr}, + userService, + authInfoService, + tracing.InitializeTracerForTest(), + ) return c } diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 1c6429c3cc4..0e8053a39b7 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -12,6 +12,7 @@ import ( "os" "strings" + "go.opentelemetry.io/otel/trace" "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/apimachinery/errutil" @@ -70,12 +71,14 @@ var ( func ProvideOAuth( name string, cfg *setting.Cfg, oauthService oauthtoken.OAuthTokenService, - socialService social.Service, settingsProviderService setting.Provider, features featuremgmt.FeatureToggles, + socialService social.Service, settingsProviderService setting.Provider, + features featuremgmt.FeatureToggles, tracer trace.Tracer, ) *OAuth { providerName := strings.TrimPrefix(name, "auth.client.") return &OAuth{ name, fmt.Sprintf("oauth_%s", providerName), providerName, - log.New(name), cfg, settingsProviderService, oauthService, socialService, features, + log.New(name), cfg, tracer, settingsProviderService, oauthService, + socialService, features, } } @@ -85,6 +88,7 @@ type OAuth struct { providerName string log log.Logger cfg *setting.Cfg + tracer trace.Tracer settingsProviderSvc setting.Provider oauthService oauthtoken.OAuthTokenService @@ -97,6 +101,9 @@ func (c *OAuth) Name() string { } func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.oauth.Authenticate") + defer span.End() + r.SetMeta(authn.MetaKeyAuthModule, c.moduleName) oauthCfg := c.socialService.GetOAuthInfoProvider(c.providerName) @@ -232,6 +239,9 @@ func (c *OAuth) GetConfig() authn.SSOClientConfig { } func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redirect, error) { + ctx, span := c.tracer.Start(ctx, "authn.oauth.RedirectURL") //nolint:ineffassign,staticcheck + defer span.End() + var opts []oauth2.AuthCodeOption oauthCfg := c.socialService.GetOAuthInfoProvider(c.providerName) @@ -274,6 +284,9 @@ func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redir } func (c *OAuth) Logout(ctx context.Context, user identity.Requester, sessionToken *auth.UserToken) (*authn.Redirect, bool) { + ctx, span := c.tracer.Start(ctx, "authn.oauth.Logout") + defer span.End() + token := c.oauthService.GetCurrentOAuthToken(ctx, user, sessionToken) userID, err := identity.UserIdentifier(user.GetID()) diff --git a/pkg/services/authn/clients/oauth_test.go b/pkg/services/authn/clients/oauth_test.go index 7a9222154f7..2bd536d6544 100644 --- a/pkg/services/authn/clients/oauth_test.go +++ b/pkg/services/authn/clients/oauth_test.go @@ -16,6 +16,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" + "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/auth" @@ -296,7 +297,7 @@ func TestOAuth_Authenticate(t *testing.T) { }, } - c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, settingsProvider, featuremgmt.WithFeatures(tt.features...)) + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, settingsProvider, featuremgmt.WithFeatures(tt.features...), tracing.InitializeTracerForTest()) identity, err := c.Authenticate(context.Background(), tt.req) assert.ErrorIs(t, err, tt.expectedErr) @@ -376,7 +377,7 @@ func TestOAuth_RedirectURL(t *testing.T) { cfg := setting.NewCfg() - c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, featuremgmt.WithFeatures()) + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), cfg, nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, featuremgmt.WithFeatures(), tracing.InitializeTracerForTest()) redirect, err := c.RedirectURL(context.Background(), nil) assert.ErrorIs(t, err, tt.expectedErr) @@ -489,7 +490,7 @@ func TestOAuth_Logout(t *testing.T) { fakeSocialSvc := &socialtest.FakeSocialService{ ExpectedAuthInfoProvider: tt.oauthCfg, } - c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures()) + c := ProvideOAuth(authn.ClientWithPrefix("azuread"), tt.cfg, mockService, fakeSocialSvc, &setting.OSSImpl{Cfg: tt.cfg}, featuremgmt.WithFeatures(), tracing.InitializeTracerForTest()) redirect, ok := c.Logout(context.Background(), &authn.Identity{ID: "1", Type: claims.TypeUser}, nil) @@ -549,7 +550,8 @@ func TestIsEnabled(t *testing.T) { nil, fakeSocialSvc, &setting.OSSImpl{Cfg: cfg}, - featuremgmt.WithFeatures()) + featuremgmt.WithFeatures(), + tracing.InitializeTracerForTest()) assert.Equal(t, tt.expected, c.IsEnabled()) }) } diff --git a/pkg/services/authn/clients/password.go b/pkg/services/authn/clients/password.go index 76e82d4fc59..f9b2483d2c5 100644 --- a/pkg/services/authn/clients/password.go +++ b/pkg/services/authn/clients/password.go @@ -4,6 +4,8 @@ import ( "context" "errors" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/authn" @@ -18,17 +20,20 @@ var ( var _ authn.PasswordClient = new(Password) -func ProvidePassword(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Password { - return &Password{loginAttempts, clients, log.New("authn.password")} +func ProvidePassword(loginAttempts loginattempt.Service, tracer trace.Tracer, clients ...authn.PasswordClient) *Password { + return &Password{loginAttempts, clients, log.New("authn.password"), tracer} } type Password struct { loginAttempts loginattempt.Service clients []authn.PasswordClient log log.Logger + tracer trace.Tracer } func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.password.AuthenticatePassword") + defer span.End() r.SetMeta(authn.MetaKeyUsername, username) ok, err := c.loginAttempts.Validate(ctx, username) diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index 6f7619ce1b7..fd7933a37e7 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -10,6 +10,7 @@ import ( claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" @@ -65,7 +66,7 @@ func TestPassword_AuthenticatePassword(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tt.clients...) + c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tracing.InitializeTracerForTest(), tt.clients...) r := &authn.Request{ OrgID: 12345, HTTPRequest: &http.Request{ diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index 237d02f13cc..f20b1534a02 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -13,6 +13,7 @@ import ( "time" claims "github.com/grafana/authlib/types" + "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" @@ -45,12 +46,12 @@ var ( _ authn.ContextAwareClient = new(Proxy) ) -func ProvideProxy(cfg *setting.Cfg, cache proxyCache, clients ...authn.ProxyClient) (*Proxy, error) { +func ProvideProxy(cfg *setting.Cfg, cache proxyCache, tracer trace.Tracer, clients ...authn.ProxyClient) (*Proxy, error) { list, err := parseAcceptList(cfg.AuthProxy.Whitelist) if err != nil { return nil, err } - return &Proxy{log.New(authn.ClientProxy), cfg, cache, clients, list}, nil + return &Proxy{log.New(authn.ClientProxy), cfg, cache, clients, list, tracer}, nil } type proxyCache interface { @@ -65,6 +66,7 @@ type Proxy struct { cache proxyCache clients []authn.ProxyClient acceptedIPs []*net.IPNet + tracer trace.Tracer } func (c *Proxy) Name() string { @@ -72,6 +74,8 @@ func (c *Proxy) Name() string { } func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.proxy.Authenticate") + defer span.End() if !c.isAllowedIP(r) { return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list") } @@ -115,6 +119,8 @@ func (c *Proxy) IsEnabled() bool { // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. // Error here means that we could not find anything in cache, so we can proceed as usual func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) { + ctx, span := c.tracer.Start(ctx, "authn.proxy.retrieveIDFromCache") + defer span.End() entry, err := c.cache.Get(ctx, cacheKey) if err != nil { return nil, err @@ -148,6 +154,8 @@ func (c *Proxy) Priority() uint { } func (c *Proxy) Hook(ctx context.Context, id *authn.Identity, r *authn.Request) error { + ctx, span := c.tracer.Start(ctx, "authn.proxy.Hook") + defer span.End() if id.ClientParams.CacheAuthProxyKey == "" { return nil } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index b0cefbd6d6c..7b1faaa9efa 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/setting" @@ -113,7 +114,7 @@ func TestProxy_Authenticate(t *testing.T) { calledAdditional = additional return nil, nil }} - c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, proxyClient) + c, err := ProvideProxy(cfg, &fakeCache{expectedErr: errors.New("")}, tracing.InitializeTracerForTest(), proxyClient) require.NoError(t, err) _, err = c.Authenticate(context.Background(), tt.req) @@ -169,7 +170,7 @@ func TestProxy_Test(t *testing.T) { cfg := setting.NewCfg() cfg.AuthProxy.HeaderName = "Proxy-Header" - c, _ := ProvideProxy(cfg, nil, nil, nil) + c, _ := ProvideProxy(cfg, nil, tracing.InitializeTracerForTest(), nil) assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req)) }) } @@ -208,7 +209,7 @@ func TestProxy_Hook(t *testing.T) { withRole := func(role string) func(t *testing.T) { cacheKey := fmt.Sprintf("users:johndoe-%s", role) return func(t *testing.T) { - c, err := ProvideProxy(cfg, cache, authntest.MockProxyClient{}) + c, err := ProvideProxy(cfg, cache, tracing.InitializeTracerForTest(), authntest.MockProxyClient{}) require.NoError(t, err) userIdentity := &authn.Identity{ ID: "1", diff --git a/pkg/services/authn/clients/session.go b/pkg/services/authn/clients/session.go index fa0f60b8e12..a644c4771af 100644 --- a/pkg/services/authn/clients/session.go +++ b/pkg/services/authn/clients/session.go @@ -7,6 +7,8 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/trace" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" @@ -18,12 +20,14 @@ import ( var _ authn.ContextAwareClient = new(Session) -func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService, authInfoService login.AuthInfoService) *Session { +func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService, + authInfoService login.AuthInfoService, tracer trace.Tracer) *Session { return &Session{ cfg: cfg, log: log.New(authn.ClientSession), sessionService: sessionService, authInfoService: authInfoService, + tracer: tracer, } } @@ -32,6 +36,7 @@ type Session struct { log log.Logger sessionService auth.UserTokenService authInfoService login.AuthInfoService + tracer trace.Tracer } func (s *Session) Name() string { diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index 946168f9f14..13195ba2b9b 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -11,6 +11,7 @@ import ( claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" @@ -31,7 +32,7 @@ func TestSession_Test(t *testing.T) { cfg := setting.NewCfg() cfg.LoginCookieName = "" cfg.LoginMaxLifetime = 20 * time.Second - s := ProvideSession(cfg, &authtest.FakeUserAuthTokenService{}, &authinfotest.FakeService{}) + s := ProvideSession(cfg, &authtest.FakeUserAuthTokenService{}, &authinfotest.FakeService{}, tracing.InitializeTracerForTest()) disabled := s.Test(context.Background(), &authn.Request{HTTPRequest: validHTTPReq}) assert.False(t, disabled) @@ -194,7 +195,7 @@ func TestSession_Authenticate(t *testing.T) { cfg.LoginCookieName = cookieName cfg.TokenRotationIntervalMinutes = 10 cfg.LoginMaxLifetime = 20 * time.Second - s := ProvideSession(cfg, tt.fields.sessionService, tt.fields.authInfoService) + s := ProvideSession(cfg, tt.fields.sessionService, tt.fields.authInfoService, tracing.InitializeTracerForTest()) got, err := s.Authenticate(context.Background(), tt.args.r) require.True(t, (err != nil) == tt.wantErr, err) diff --git a/pkg/services/authz/zanzana/server/openfga_server.go b/pkg/services/authz/zanzana/server/openfga_server.go index 3df2d944604..9baaafc017f 100644 --- a/pkg/services/authz/zanzana/server/openfga_server.go +++ b/pkg/services/authz/zanzana/server/openfga_server.go @@ -32,9 +32,21 @@ func NewOpenFGAServer(cfg setting.ZanzanaServerSettings, store storage.OpenFGADa opts := []server.OpenFGAServiceV1Option{ server.WithDatastore(store), server.WithLogger(zlogger.New(logger)), - server.WithCheckQueryCacheEnabled(cfg.CheckQueryCache), - server.WithCheckQueryCacheTTL(cfg.CheckQueryCacheTTL), + server.WithCheckCacheLimit(cfg.CacheSettings.CheckCacheLimit), + server.WithCacheControllerEnabled(cfg.CacheSettings.CacheControllerEnabled), + server.WithCacheControllerTTL(cfg.CacheSettings.CacheControllerTTL), + server.WithCheckQueryCacheEnabled(cfg.CacheSettings.CheckQueryCacheEnabled), + server.WithCheckQueryCacheTTL(cfg.CacheSettings.CheckQueryCacheTTL), + server.WithCheckIteratorCacheEnabled(cfg.CacheSettings.CheckIteratorCacheEnabled), + server.WithCheckIteratorCacheMaxResults(cfg.CacheSettings.CheckIteratorCacheMaxResults), + server.WithCheckIteratorCacheTTL(cfg.CacheSettings.CheckIteratorCacheTTL), server.WithListObjectsMaxResults(cfg.ListObjectsMaxResults), + server.WithListObjectsIteratorCacheEnabled(cfg.CacheSettings.ListObjectsIteratorCacheEnabled), + server.WithListObjectsIteratorCacheMaxResults(cfg.CacheSettings.ListObjectsIteratorCacheMaxResults), + server.WithListObjectsIteratorCacheTTL(cfg.CacheSettings.ListObjectsIteratorCacheTTL), + server.WithSharedIteratorEnabled(cfg.CacheSettings.SharedIteratorEnabled), + server.WithSharedIteratorLimit(cfg.CacheSettings.SharedIteratorLimit), + server.WithSharedIteratorTTL(cfg.CacheSettings.SharedIteratorTTL), server.WithListObjectsDeadline(cfg.ListObjectsDeadline), } diff --git a/pkg/services/authz/zanzana/server/server.go b/pkg/services/authz/zanzana/server/server.go index 6803fdb594f..c495f0504ac 100644 --- a/pkg/services/authz/zanzana/server/server.go +++ b/pkg/services/authz/zanzana/server/server.go @@ -62,7 +62,7 @@ func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger storesMU: &sync.Mutex{}, stores: make(map[string]storeInfo), cfg: cfg, - cache: localcache.New(cfg.CheckQueryCacheTTL, cacheCleanInterval), + cache: localcache.New(cfg.CacheSettings.CheckQueryCacheTTL, cacheCleanInterval), logger: logger, tracer: tracer, } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index ee08be863f7..d81c9a9f1f1 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -178,7 +178,7 @@ func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequ fn = s.streamedListObjects } - if s.cfg.CheckQueryCache { + if s.cfg.CacheSettings.CheckQueryCacheEnabled { return s.listObjectCached(ctx, req, fn) } diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index a9fe7ce855b..801480b910b 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -150,17 +150,19 @@ const ( type ItemErrorCode string const ( - ErrDatasourceNameConflict ItemErrorCode = "DATASOURCE_NAME_CONFLICT" - ErrDatasourceInvalidURL ItemErrorCode = "DATASOURCE_INVALID_URL" - ErrDatasourceAlreadyManaged ItemErrorCode = "DATASOURCE_ALREADY_MANAGED" - ErrFolderNameConflict ItemErrorCode = "FOLDER_NAME_CONFLICT" - ErrDashboardAlreadyManaged ItemErrorCode = "DASHBOARD_ALREADY_MANAGED" - ErrLibraryElementNameConflict ItemErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" - ErrUnsupportedDataType ItemErrorCode = "UNSUPPORTED_DATA_TYPE" - ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT" - ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE" - ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR" - ErrGeneric ItemErrorCode = "GENERIC_ERROR" + ErrAlertRulesQuotaReached ItemErrorCode = "ALERT_RULES_QUOTA_REACHED" + ErrAlertRulesGroupQuotaReached ItemErrorCode = "ALERT_RULES_GROUP_QUOTA_REACHED" + ErrDatasourceNameConflict ItemErrorCode = "DATASOURCE_NAME_CONFLICT" + ErrDatasourceInvalidURL ItemErrorCode = "DATASOURCE_INVALID_URL" + ErrDatasourceAlreadyManaged ItemErrorCode = "DATASOURCE_ALREADY_MANAGED" + ErrFolderNameConflict ItemErrorCode = "FOLDER_NAME_CONFLICT" + ErrDashboardAlreadyManaged ItemErrorCode = "DASHBOARD_ALREADY_MANAGED" + ErrLibraryElementNameConflict ItemErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" + ErrUnsupportedDataType ItemErrorCode = "UNSUPPORTED_DATA_TYPE" + ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT" + ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE" + ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR" + ErrGeneric ItemErrorCode = "GENERIC_ERROR" ) // swagger:parameters getCloudMigrationRun diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 41d27e02f94..a01a33c62d4 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -110,17 +110,19 @@ const ( type ResourceErrorCode string const ( - ErrDatasourceNameConflict ResourceErrorCode = "DATASOURCE_NAME_CONFLICT" - ErrDatasourceInvalidURL ResourceErrorCode = "DATASOURCE_INVALID_URL" - ErrDatasourceAlreadyManaged ResourceErrorCode = "DATASOURCE_ALREADY_MANAGED" - ErrFolderNameConflict ResourceErrorCode = "FOLDER_NAME_CONFLICT" - ErrDashboardAlreadyManaged ResourceErrorCode = "DASHBOARD_ALREADY_MANAGED" - ErrLibraryElementNameConflict ResourceErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" - ErrUnsupportedDataType ResourceErrorCode = "UNSUPPORTED_DATA_TYPE" - ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT" - ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE" - ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR" - ErrGeneric ResourceErrorCode = "GENERIC_ERROR" + ErrAlertRulesQuotaReached ResourceErrorCode = "ALERT_RULES_QUOTA_REACHED" + ErrAlertRulesGroupQuotaReached ResourceErrorCode = "ALERT_RULES_GROUP_QUOTA_REACHED" + ErrDatasourceNameConflict ResourceErrorCode = "DATASOURCE_NAME_CONFLICT" + ErrDatasourceInvalidURL ResourceErrorCode = "DATASOURCE_INVALID_URL" + ErrDatasourceAlreadyManaged ResourceErrorCode = "DATASOURCE_ALREADY_MANAGED" + ErrFolderNameConflict ResourceErrorCode = "FOLDER_NAME_CONFLICT" + ErrDashboardAlreadyManaged ResourceErrorCode = "DASHBOARD_ALREADY_MANAGED" + ErrLibraryElementNameConflict ResourceErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" + ErrUnsupportedDataType ResourceErrorCode = "UNSUPPORTED_DATA_TYPE" + ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT" + ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE" + ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR" + ErrGeneric ResourceErrorCode = "GENERIC_ERROR" ) type SnapshotResourceStats struct { diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 8dfa2408edd..dc759ffd0d3 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -6,10 +6,12 @@ import ( "fmt" "net/http" + "github.com/open-feature/go-sdk/openfeature" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" claims "github.com/grafana/authlib/types" + authnClients "github.com/grafana/grafana/pkg/services/authn/clients" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -141,6 +143,16 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { reqContext.Resp.Before(h.addIDHeaderEndOfRequestFunc(reqContext.SignedInUser)) } + // Set open feature evaluation context with namespace + ns := "default" + if id != nil { + ns = id.Namespace + } + evalCtx := openfeature.NewEvaluationContext(ns, map[string]any{ + "namespace": ns, + }) + ctx = openfeature.MergeTransactionContext(ctx, evalCtx) + // End the span to make next handlers not wrapped within middleware span span.End() next.ServeHTTP(w, r.WithContext(ctx)) diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 9aace92ed7f..8af7dfaed54 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -5,10 +5,12 @@ import ( "net/http" "testing" + "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" @@ -209,4 +211,50 @@ func TestContextHandler(t *testing.T) { require.NoError(t, res.Body.Close()) }) }) + + t.Run("openfeature evaluation context defaults", func(t *testing.T) { + handler := contexthandler.ProvideService( + setting.NewCfg(), + &authntest.FakeService{ExpectedErr: errors.New("some error")}, + featuremgmt.WithFeatures(), + ) + + server := webtest.NewServer(t, routing.NewRouteRegister()) + server.Mux.Use(handler.Middleware) + server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) { + evalCtx := openfeature.TransactionContext(c.Req.Context()) + require.NotNil(t, evalCtx) + require.Equal(t, "default", evalCtx.Attribute("namespace")) + }) + + res, err := server.Send(server.NewGetRequest("/api/handler")) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + }) + + t.Run("openfeature evaluation context with user namespace", func(t *testing.T) { + handler := contexthandler.ProvideService( + setting.NewCfg(), + &authntest.FakeService{ + ExpectedIdentity: &authn.Identity{ + ID: "1", + Type: claims.TypeUser, + Namespace: "org-3", + }, + }, + featuremgmt.WithFeatures(), + ) + + server := webtest.NewServer(t, routing.NewRouteRegister()) + server.Mux.Use(handler.Middleware) + server.Mux.Get("/api/handler", func(c *contextmodel.ReqContext) { + evalCtx := openfeature.TransactionContext(c.Req.Context()) + require.NotNil(t, evalCtx) + require.Equal(t, "org-3", evalCtx.Attribute("namespace")) + }) + + res, err := server.Send(server.NewGetRequest("/api/handler")) + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + }) } diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index 4b7863b27e6..016a20528f3 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboards diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index cba4d23820d..d20a9525622 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboards @@ -94,36 +94,6 @@ func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID return r0, r1 } -// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID -func (_m *FakeDashboardService) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { - ret := _m.Called(ctx, libraryPanelUID, orgID) - - if len(ret) == 0 { - panic("no return value specified for GetDashboardsByLibraryPanelUID") - } - - var r0 []*DashboardRef - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { - return rf(ctx, libraryPanelUID, orgID) - } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { - r0 = rf(ctx, libraryPanelUID, orgID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*DashboardRef) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, libraryPanelUID, orgID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) { ret := _m.Called(ctx, orgID, folderUIDs, user) @@ -368,6 +338,36 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDas return r0, r1 } +// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID +func (_m *FakeDashboardService) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { + ret := _m.Called(ctx, libraryPanelUID, orgID) + + if len(ret) == 0 { + panic("no return value specified for GetDashboardsByLibraryPanelUID") + } + + var r0 []*DashboardRef + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { + return rf(ctx, libraryPanelUID, orgID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { + r0 = rf(ctx, libraryPanelUID, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardRef) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, libraryPanelUID, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ImportDashboard provides a mock function with given fields: ctx, dto func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(ctx, dto) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 1a0b46c82cd..5b208c34b7f 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboards @@ -370,6 +370,36 @@ func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashb return r0, r1 } +// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID +func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { + ret := _m.Called(ctx, libraryPanelUID, orgID) + + if len(ret) == 0 { + panic("no return value specified for GetDashboardsByLibraryPanelUID") + } + + var r0 []*DashboardRef + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { + return rf(ctx, libraryPanelUID, orgID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { + r0 = rf(ctx, libraryPanelUID, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardRef) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { + r1 = rf(ctx, libraryPanelUID, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboardsByPluginID provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) @@ -628,36 +658,6 @@ func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64 return r0 } -// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID -func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) { - ret := _m.Called(ctx, libraryPanelUID, orgID) - - if len(ret) == 0 { - panic("no return value specified for GetDashboardsByLibraryPanelUID") - } - - var r0 []*DashboardRef - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]*DashboardRef, error)); ok { - return rf(ctx, libraryPanelUID, orgID) - } - if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*DashboardRef); ok { - r0 = rf(ctx, libraryPanelUID, orgID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*DashboardRef) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok { - r1 = rf(ctx, libraryPanelUID, orgID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // ValidateDashboardBeforeSave provides a mock function with given fields: ctx, dashboard, overwrite func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) { ret := _m.Called(ctx, dashboard, overwrite) diff --git a/pkg/services/dashboardsnapshots/service_mock.go b/pkg/services/dashboardsnapshots/service_mock.go index 4d3ef270349..a323aa5d7b2 100644 --- a/pkg/services/dashboardsnapshots/service_mock.go +++ b/pkg/services/dashboardsnapshots/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dashboardsnapshots @@ -17,6 +17,10 @@ type MockService struct { func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for CreateDashboardSnapshot") + } + var r0 *DashboardSnapshot var r1 error if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error)); ok { @@ -43,6 +47,10 @@ func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateD func (_m *MockService) DeleteDashboardSnapshot(_a0 context.Context, _a1 *DeleteDashboardSnapshotCommand) error { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for DeleteDashboardSnapshot") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteDashboardSnapshotCommand) error); ok { r0 = rf(_a0, _a1) @@ -57,6 +65,10 @@ func (_m *MockService) DeleteDashboardSnapshot(_a0 context.Context, _a1 *DeleteD func (_m *MockService) DeleteExpiredSnapshots(_a0 context.Context, _a1 *DeleteExpiredSnapshotsCommand) error { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for DeleteExpiredSnapshots") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *DeleteExpiredSnapshotsCommand) error); ok { r0 = rf(_a0, _a1) @@ -71,6 +83,10 @@ func (_m *MockService) DeleteExpiredSnapshots(_a0 context.Context, _a1 *DeleteEx func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboardSnapshotQuery) (*DashboardSnapshot, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for GetDashboardSnapshot") + } + var r0 *DashboardSnapshot var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) (*DashboardSnapshot, error)); ok { @@ -97,6 +113,10 @@ func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboa func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error) { ret := _m.Called(_a0, _a1) + if len(ret) == 0 { + panic("no return value specified for SearchDashboardSnapshots") + } + var r0 DashboardSnapshotsList var r1 error if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error)); ok { @@ -123,6 +143,10 @@ func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDas func (_m *MockService) ValidateDashboardExists(_a0 context.Context, _a1 int64, _a2 string) error { ret := _m.Called(_a0, _a1, _a2) + if len(ret) == 0 { + panic("no return value specified for ValidateDashboardExists") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok { r0 = rf(_a0, _a1, _a2) diff --git a/pkg/services/datasources/service/store.go b/pkg/services/datasources/service/store.go index d7a4056f165..f005516433a 100644 --- a/pkg/services/datasources/service/store.go +++ b/pkg/services/datasources/service/store.go @@ -83,6 +83,7 @@ func (ss *SqlStore) getDataSource(_ context.Context, query *datasources.GetDataS ss.logger.Error("Failed getting data source", "err", err, "uid", query.UID, "id", query.ID, "name", query.Name, "orgId", query.OrgID) // nolint:staticcheck return nil, err } else if !has { + ss.logger.Debug("Data source not found", "uid", query.UID, "id", query.ID, "name", query.Name, "orgId", query.OrgID) // nolint:staticcheck return nil, datasources.ErrDataSourceNotFound } diff --git a/pkg/services/datasources/service/store_test.go b/pkg/services/datasources/service/store_test.go index 9e8b64b44ec..7acb1dbafca 100644 --- a/pkg/services/datasources/service/store_test.go +++ b/pkg/services/datasources/service/store_test.go @@ -281,7 +281,10 @@ func TestIntegrationDataAccess(t *testing.T) { t.Run("Can not delete datasource with wrong orgID", func(t *testing.T) { db := db.InitTestDB(t) ds := initDatasource(db) - ss := SqlStore{db: db} + ss := SqlStore{ + db: db, + logger: log.NewNopLogger(), + } err := ss.DeleteDataSource(context.Background(), &datasources.DeleteDataSourceCommand{ID: ds.ID, OrgID: 123123}) @@ -322,7 +325,10 @@ func TestIntegrationDataAccess(t *testing.T) { t.Run("does not fire an event when the datasource is not deleted", func(t *testing.T) { db := db.InitTestDB(t) - ss := SqlStore{db: db} + ss := SqlStore{ + db: db, + logger: log.NewNopLogger(), + } var called bool db.Bus().AddEventListener(func(ctx context.Context, e *events.DataSourceDeleted) error { diff --git a/pkg/services/extsvcauth/tests/extsvcregmock.go b/pkg/services/extsvcauth/tests/extsvcregmock.go index 82b0a278108..7ef96c6c36e 100644 --- a/pkg/services/extsvcauth/tests/extsvcregmock.go +++ b/pkg/services/extsvcauth/tests/extsvcregmock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package tests @@ -18,6 +18,10 @@ type ExternalServiceRegistryMock struct { func (_m *ExternalServiceRegistryMock) GetExternalServiceNames(ctx context.Context) ([]string, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetExternalServiceNames") + } + var r0 []string var r1 error if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { @@ -44,6 +48,10 @@ func (_m *ExternalServiceRegistryMock) GetExternalServiceNames(ctx context.Conte func (_m *ExternalServiceRegistryMock) HasExternalService(ctx context.Context, name string) (bool, error) { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for HasExternalService") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { @@ -68,6 +76,10 @@ func (_m *ExternalServiceRegistryMock) HasExternalService(ctx context.Context, n func (_m *ExternalServiceRegistryMock) RemoveExternalService(ctx context.Context, name string) error { ret := _m.Called(ctx, name) + if len(ret) == 0 { + panic("no return value specified for RemoveExternalService") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, name) @@ -82,6 +94,10 @@ func (_m *ExternalServiceRegistryMock) RemoveExternalService(ctx context.Context func (_m *ExternalServiceRegistryMock) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for SaveExternalService") + } + var r0 *extsvcauth.ExternalService var r1 error if rf, ok := ret.Get(0).(func(context.Context, *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error)); ok { diff --git a/pkg/services/featuremgmt/feature_toggles_mock.go b/pkg/services/featuremgmt/feature_toggles_mock.go new file mode 100644 index 00000000000..db29d9a2159 --- /dev/null +++ b/pkg/services/featuremgmt/feature_toggles_mock.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package featuremgmt + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockFeatureToggles is an autogenerated mock type for the FeatureToggles type +type MockFeatureToggles struct { + mock.Mock +} + +type MockFeatureToggles_Expecter struct { + mock *mock.Mock +} + +func (_m *MockFeatureToggles) EXPECT() *MockFeatureToggles_Expecter { + return &MockFeatureToggles_Expecter{mock: &_m.Mock} +} + +// GetEnabled provides a mock function with given fields: ctx +func (_m *MockFeatureToggles) GetEnabled(ctx context.Context) map[string]bool { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetEnabled") + } + + var r0 map[string]bool + if rf, ok := ret.Get(0).(func(context.Context) map[string]bool); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]bool) + } + } + + return r0 +} + +// MockFeatureToggles_GetEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEnabled' +type MockFeatureToggles_GetEnabled_Call struct { + *mock.Call +} + +// GetEnabled is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockFeatureToggles_Expecter) GetEnabled(ctx interface{}) *MockFeatureToggles_GetEnabled_Call { + return &MockFeatureToggles_GetEnabled_Call{Call: _e.mock.On("GetEnabled", ctx)} +} + +func (_c *MockFeatureToggles_GetEnabled_Call) Run(run func(ctx context.Context)) *MockFeatureToggles_GetEnabled_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockFeatureToggles_GetEnabled_Call) Return(_a0 map[string]bool) *MockFeatureToggles_GetEnabled_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockFeatureToggles_GetEnabled_Call) RunAndReturn(run func(context.Context) map[string]bool) *MockFeatureToggles_GetEnabled_Call { + _c.Call.Return(run) + return _c +} + +// IsEnabled provides a mock function with given fields: ctx, flag +func (_m *MockFeatureToggles) IsEnabled(ctx context.Context, flag string) bool { + ret := _m.Called(ctx, flag) + + if len(ret) == 0 { + panic("no return value specified for IsEnabled") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, flag) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockFeatureToggles_IsEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabled' +type MockFeatureToggles_IsEnabled_Call struct { + *mock.Call +} + +// IsEnabled is a helper method to define mock.On call +// - ctx context.Context +// - flag string +func (_e *MockFeatureToggles_Expecter) IsEnabled(ctx interface{}, flag interface{}) *MockFeatureToggles_IsEnabled_Call { + return &MockFeatureToggles_IsEnabled_Call{Call: _e.mock.On("IsEnabled", ctx, flag)} +} + +func (_c *MockFeatureToggles_IsEnabled_Call) Run(run func(ctx context.Context, flag string)) *MockFeatureToggles_IsEnabled_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockFeatureToggles_IsEnabled_Call) Return(_a0 bool) *MockFeatureToggles_IsEnabled_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockFeatureToggles_IsEnabled_Call) RunAndReturn(run func(context.Context, string) bool) *MockFeatureToggles_IsEnabled_Call { + _c.Call.Return(run) + return _c +} + +// IsEnabledGlobally provides a mock function with given fields: flag +func (_m *MockFeatureToggles) IsEnabledGlobally(flag string) bool { + ret := _m.Called(flag) + + if len(ret) == 0 { + panic("no return value specified for IsEnabledGlobally") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(flag) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockFeatureToggles_IsEnabledGlobally_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabledGlobally' +type MockFeatureToggles_IsEnabledGlobally_Call struct { + *mock.Call +} + +// IsEnabledGlobally is a helper method to define mock.On call +// - flag string +func (_e *MockFeatureToggles_Expecter) IsEnabledGlobally(flag interface{}) *MockFeatureToggles_IsEnabledGlobally_Call { + return &MockFeatureToggles_IsEnabledGlobally_Call{Call: _e.mock.On("IsEnabledGlobally", flag)} +} + +func (_c *MockFeatureToggles_IsEnabledGlobally_Call) Run(run func(flag string)) *MockFeatureToggles_IsEnabledGlobally_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockFeatureToggles_IsEnabledGlobally_Call) Return(_a0 bool) *MockFeatureToggles_IsEnabledGlobally_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockFeatureToggles_IsEnabledGlobally_Call) RunAndReturn(run func(string) bool) *MockFeatureToggles_IsEnabledGlobally_Call { + _c.Call.Return(run) + return _c +} + +// NewMockFeatureToggles creates a new instance of MockFeatureToggles. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockFeatureToggles(t interface { + mock.TestingT + Cleanup(func()) +}) *MockFeatureToggles { + mock := &MockFeatureToggles{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index 018cb6046e6..4acca519bef 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -6,6 +6,7 @@ import ( "encoding/json" ) +//go:generate mockery --name FeatureToggles --structname MockFeatureToggles --inpackage --filename feature_toggles_mock.go --with-expecter type FeatureToggles interface { // IsEnabled checks if a feature is enabled for a given context. // The settings may be per user, tenant, or globally set in the cloud diff --git a/pkg/services/featuremgmt/openfeature.go b/pkg/services/featuremgmt/openfeature.go index 76c07e52bbe..aef992e1ca5 100644 --- a/pkg/services/featuremgmt/openfeature.go +++ b/pkg/services/featuremgmt/openfeature.go @@ -4,46 +4,37 @@ import ( "fmt" "net/url" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" "github.com/open-feature/go-sdk/openfeature" ) -type OpenFeatureService struct { - log log.Logger - provider openfeature.FeatureProvider - Client openfeature.IClient -} - -// ProvideOpenFeatureService is used for wiring dependencies in single tenant grafana -func ProvideOpenFeatureService(cfg *setting.Cfg) (*OpenFeatureService, error) { +func InitOpenFeatureWithCfg(cfg *setting.Cfg) error { confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles")) if err != nil { - return nil, fmt.Errorf("failed to read feature toggles from config: %w", err) + return fmt.Errorf("failed to read feature flags from config: %w", err) } + err = initOpenFeature(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, confFlags) + if err != nil { + return fmt.Errorf("failed to initialize OpenFeature: %w", err) + } openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(cfg.OpenFeature.TargetingKey, cfg.OpenFeature.ContextAttrs)) - return newOpenFeatureService(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, confFlags) + + return nil } -// TODO: might need to be public, so other MT services could set up open feature client -func newOpenFeatureService(pType string, u *url.URL, staticFlags map[string]bool) (*OpenFeatureService, error) { - p, err := createProvider(pType, u, staticFlags) +func initOpenFeature(providerType string, u *url.URL, staticFlags map[string]bool) error { + p, err := createProvider(providerType, u, staticFlags) if err != nil { - return nil, fmt.Errorf("failed to create feature provider: type %s, %w", pType, err) + return fmt.Errorf("failed to create feature provider: type %s, %w", providerType, err) } if err := openfeature.SetProviderAndWait(p); err != nil { - return nil, fmt.Errorf("failed to set global feature provider: %s, %w", pType, err) + return fmt.Errorf("failed to set global feature provider: %s, %w", providerType, err) } - client := openfeature.NewClient("grafana-openfeature-client") - return &OpenFeatureService{ - log: log.New("openfeatureservice"), - provider: p, - Client: client, - }, nil + return nil } func createProvider(providerType string, u *url.URL, staticFlags map[string]bool) (openfeature.FeatureProvider, error) { @@ -57,12 +48,3 @@ func createProvider(providerType string, u *url.URL, staticFlags map[string]bool return newGOFFProvider(u.String()) } - -func createClient(provider openfeature.FeatureProvider) (openfeature.IClient, error) { - if err := openfeature.SetProviderAndWait(provider); err != nil { - return nil, fmt.Errorf("failed to set global feature provider: %w", err) - } - - client := openfeature.NewClient("grafana-openfeature-client") - return client, nil -} diff --git a/pkg/services/featuremgmt/openfeature_test.go b/pkg/services/featuremgmt/openfeature_test.go index 7176fc1e6a1..a58c8ade9e0 100644 --- a/pkg/services/featuremgmt/openfeature_test.go +++ b/pkg/services/featuremgmt/openfeature_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestProvideOpenFeatureManager(t *testing.T) { +func TestCreateProvider(t *testing.T) { u, err := url.Parse("http://localhost:1031") require.NoError(t, err) @@ -44,17 +44,14 @@ func TestProvideOpenFeatureManager(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cfg := setting.NewCfg() - cfg.OpenFeature = tc.cfg - - p, err := ProvideOpenFeatureService(cfg) + provider, err := createProvider(tc.cfg.ProviderType, tc.cfg.URL, nil) require.NoError(t, err) if tc.expectedProvider == setting.GOFFProviderType { - _, ok := p.provider.(*gofeatureflag.Provider) + _, ok := provider.(*gofeatureflag.Provider) assert.True(t, ok, "expected provider to be of type goff.Provider") } else { - _, ok := p.provider.(*inMemoryBulkProvider) + _, ok := provider.(*inMemoryBulkProvider) assert.True(t, ok, "expected provider to be of type memprovider.InMemoryProvider") } }) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5ee6c7d0e5f..a82c3a77dbd 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -296,7 +296,8 @@ var ( { Name: "awsDatasourcesTempCredentials", Description: "Support temporary security credentials in AWS plugins for Grafana Cloud customers", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, + Expression: "true", // enabled by default Owner: awsDatasourcesSquad, }, { @@ -338,8 +339,8 @@ var ( Owner: grafanaAppPlatformSquad, }, { - Name: "nanoGit", - Description: "Use experimental git library for provisioning", + Name: "provisioningSecretsService", + Description: "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", Stage: FeatureStageExperimental, RequiresRestart: true, Owner: grafanaAppPlatformSquad, @@ -1261,13 +1262,6 @@ var ( Owner: grafanaObservabilityTracesAndProfilingSquad, FrontendOnly: true, }, - { - Name: "jaegerBackendMigration", - Description: "Enables querying the Jaeger data source without the proxy", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOSSBigTent, - Expression: "true", - }, { Name: "alertingUIOptimizeReducer", Description: "Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query", @@ -1320,6 +1314,8 @@ var ( FrontendOnly: true, }, { + // Remove this flag once Loki v4 is released and the min supported version is v3.0+, + // since users on v2.9 need it to disable the feature, as it doesn't work for them. Name: "lokiLabelNamesQueryApi", Description: "Defaults to using the Loki `/labels` API instead of `/series`", Stage: FeatureStageGeneralAvailability, @@ -1621,6 +1617,13 @@ var ( Owner: grafanaObservabilityTracesAndProfilingSquad, FrontendOnly: true, }, + { + Name: "tempoAlerting", + Description: "Enables creating alerts from Tempo data source", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityTracesAndProfilingSquad, + FrontendOnly: true, + }, { Name: "pluginsAutoUpdate", Description: "Enables auto-updating of users installed plugins", @@ -1678,11 +1681,11 @@ var ( { Name: "skipTokenRotationIfRecent", Description: "Skip token rotation if it was already rotated less than 5 seconds ago", - Stage: FeatureStagePrivatePreview, + Stage: FeatureStageGeneralAvailability, Owner: identityAccessTeam, HideFromAdminPage: true, HideFromDocs: true, - Expression: "false", + Expression: "true", // enabled by default }, { Name: "alertEnrichment", @@ -1702,6 +1705,14 @@ var ( HideFromDocs: true, Expression: "false", }, + { + Name: "sharingDashboardImage", + Description: "Enables image sharing functionality for dashboards", + Stage: FeatureStageExperimental, + Owner: grafanaSharingSquad, + HideFromDocs: true, + FrontendOnly: true, + }, { Name: "preferLibraryPanelTitle", Description: "Prefer library panel title over viz panel title.", @@ -1714,7 +1725,7 @@ var ( Description: "Use fixed-width numbers globally in the UI", Stage: FeatureStageGeneralAvailability, Owner: grafanaFrontendPlatformSquad, - Expression: "true", + Expression: "false", }, { Name: "newInfluxDSConfigPageDesign", @@ -1744,6 +1755,16 @@ var ( FrontendOnly: true, Expression: "false", }, + { + Name: "enablePluginImporter", + Description: "Set this to true to use the new PluginImporter functionality", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + HideFromAdminPage: true, + HideFromDocs: true, + FrontendOnly: true, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/static_evaluator.go b/pkg/services/featuremgmt/static_evaluator.go index f306c984f17..1d4b05d9ece 100644 --- a/pkg/services/featuremgmt/static_evaluator.go +++ b/pkg/services/featuremgmt/static_evaluator.go @@ -3,7 +3,6 @@ package featuremgmt import ( "context" "fmt" - "net/url" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" @@ -17,43 +16,32 @@ type StaticFlagEvaluator interface { EvalAllFlags(ctx context.Context) (OFREPBulkResponse, error) } -// ProvideStaticEvaluator creates a static evaluator from configuration -// This can be used in wire dependency injection -func ProvideStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) { - if cfg.OpenFeature.ProviderType == setting.GOFFProviderType { - l := log.New("static-evaluator") - l.Debug("cannot create static evaluator if configured provider is goff") - return &staticEvaluator{}, nil +// CreateStaticEvaluator is a dependency for ofrep APIBuilder +func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) { + if cfg.OpenFeature.ProviderType != setting.StaticProviderType { + return nil, fmt.Errorf("provider is not a static provider, type %s", setting.StaticProviderType) } - confFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles")) + staticFlags, err := setting.ReadFeatureTogglesFromInitFile(cfg.Raw.Section("feature_toggles")) if err != nil { - return nil, fmt.Errorf("failed to read feature toggles from config: %w", err) + return nil, fmt.Errorf("failed to read feature flags from config: %w", err) } - return createStaticEvaluator(cfg.OpenFeature.ProviderType, cfg.OpenFeature.URL, confFlags) -} - -// createStaticEvaluator evaluator that allows evaluating static flags from config.ini -func createStaticEvaluator(providerType string, u *url.URL, staticFlags map[string]bool) (StaticFlagEvaluator, error) { - provider, err := createProvider(providerType, u, staticFlags) + staticProvider, err := newStaticProvider(staticFlags) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create static provider: %w", err) } - staticProvider, ok := provider.(*inMemoryBulkProvider) + p, ok := staticProvider.(*inMemoryBulkProvider) if !ok { - return nil, fmt.Errorf("provider is not a static provider") + return nil, fmt.Errorf("static provider is not of type inMemoryBulkProvider") } - client, err := createClient(provider) - if err != nil { - return nil, err - } + c := openfeature.GetApiInstance().GetClient() return &staticEvaluator{ - provider: staticProvider, - client: client, + provider: p, + client: c, log: log.New("static-evaluator"), }, nil } @@ -110,7 +98,7 @@ type OFREPBulkResponse struct { // OFREPFlag represents a single flag in the bulk response type OFREPFlag struct { Key string `json:"key"` - Value bool `json:"value"` + Value any `json:"value"` Reason string `json:"reason"` Variant string `json:"variant,omitempty"` Metadata map[string]any `json:"metadata,omitempty"` diff --git a/pkg/services/featuremgmt/static_provider_test.go b/pkg/services/featuremgmt/static_provider_test.go index 4c244ed8cad..d47711f6269 100644 --- a/pkg/services/featuremgmt/static_provider_test.go +++ b/pkg/services/featuremgmt/static_provider_test.go @@ -20,9 +20,9 @@ func Test_StaticProvider(t *testing.T) { stFeatValue := stFeat.Expression == "true" t.Run("empty config loads standard flags", func(t *testing.T) { - p := setup(t, []byte(``)) + setup(t, []byte(``)) // Check for one of the standard flags - feat, err := p.Client.BooleanValueDetails(ctx, stFeatName, !stFeatValue, evalCtx) + feat, err := openfeature.GetApiInstance().GetClient().BooleanValueDetails(ctx, stFeatName, !stFeatValue, evalCtx) assert.NoError(t, err) assert.True(t, stFeatValue == feat.Value) }) @@ -32,40 +32,46 @@ func Test_StaticProvider(t *testing.T) { [feature_toggles] featureOne = true `) - p := setup(t, conf) - feat, err := p.Client.BooleanValueDetails(ctx, "featureOne", false, evalCtx) + setup(t, conf) + feat, err := openfeature.GetApiInstance().GetClient().BooleanValueDetails(ctx, "featureOne", false, evalCtx) assert.NoError(t, err) assert.True(t, feat.Value) }) t.Run("missing feature should return default evaluation value and an error", func(t *testing.T) { - p := setup(t, []byte(``)) - missingFeature, err := p.Client.BooleanValueDetails(ctx, "missingFeature", true, evalCtx) + setup(t, []byte(``)) + missingFeature, err := openfeature.GetApiInstance().GetClient().BooleanValueDetails(ctx, "missingFeature", true, evalCtx) assert.Error(t, err) assert.True(t, missingFeature.Value) assert.Equal(t, openfeature.ErrorCode("FLAG_NOT_FOUND"), missingFeature.ErrorCode) }) } -func setup(t *testing.T, conf []byte) *OpenFeatureService { +func setup(t *testing.T, conf []byte) { t.Helper() cfg, err := setting.NewCfgFromBytes(conf) require.NoError(t, err) - - p, err := ProvideOpenFeatureService(cfg) + err = InitOpenFeatureWithCfg(cfg) require.NoError(t, err) - return p } func Test_CompareStaticProviderWithFeatureManager(t *testing.T) { - cfg := setting.NewCfg() - sec, err := cfg.Raw.NewSection("feature_toggles") + conf := []byte(` +[feature_toggles] +ABCD = true +`) + + cfg, err := setting.NewCfgFromBytes(conf) require.NoError(t, err) - _, err = sec.NewKey("ABCD", "true") + + // InitOpenFeatureWithCfg needed to initialize OpenFeature with the static provider configuration, + // so that StaticFlagEvaluator can use an open feature client. + // In real scenarios, this would be done during server startup. + err = InitOpenFeatureWithCfg(cfg) require.NoError(t, err) // Use StaticFlagEvaluator instead of OpenFeatureService for static evaluation - staticEvaluator, err := ProvideStaticEvaluator(cfg) + staticEvaluator, err := CreateStaticEvaluator(cfg) require.NoError(t, err) ctx := openfeature.WithTransactionContext(context.Background(), openfeature.NewEvaluationContext("grafana", nil)) @@ -74,7 +80,7 @@ func Test_CompareStaticProviderWithFeatureManager(t *testing.T) { openFeatureEnabledFlags := map[string]bool{} for _, flag := range allFlags.Flags { - if flag.Value { + if v, ok := flag.Value.(bool); ok && v { openFeatureEnabledFlags[flag.Key] = true } } diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 3583570d940..81d6bf376f7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -37,13 +37,13 @@ pluginsDetailsRightPanel,GA,@grafana/plugins-platform-backend,false,false,true sqlDatasourceDatabaseSelection,preview,@grafana/oss-big-tent,false,false,true recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true -awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false +awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false transformationsRedesign,GA,@grafana/observability-metrics,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false -nanoGit,experimental,@grafana/grafana-app-platform-squad,false,true,false +provisioningSecretsService,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false @@ -165,7 +165,6 @@ prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,fal enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false enableSCIM,preview,@grafana/identity-access-team,false,false,false crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true -jaegerBackendMigration,GA,@grafana/oss-big-tent,false,false,false alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true @@ -212,6 +211,7 @@ localizationForPlugins,experimental,@grafana/plugins-platform-backend,false,fals unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true +tempoAlerting,experimental,@grafana/observability-traces-and-profiling,false,false,true pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false,false alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true @@ -219,11 +219,13 @@ alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false, alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false -skipTokenRotationIfRecent,privatePreview,@grafana/identity-access-team,false,false,false +skipTokenRotationIfRecent,GA,@grafana/identity-access-team,false,false,false alertEnrichment,experimental,@grafana/alerting-squad,false,false,false alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,false +sharingDashboardImage,experimental,@grafana/sharing-squad,false,false,true preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false enableAppChromeExtensions,experimental,@grafana/plugins-platform-backend,false,false,true foldersAppPlatformAPI,experimental,@grafana/grafana-search-navigate-organise,false,false,true +enablePluginImporter,experimental,@grafana/plugins-platform-backend,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e105464df6a..6069be22df0 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -183,9 +183,9 @@ const ( // Next generation provisioning... and git FlagProvisioning = "provisioning" - // FlagNanoGit - // Use experimental git library for provisioning - FlagNanoGit = "nanoGit" + // FlagProvisioningSecretsService + // Experimental feature to use the secrets service for provisioning instead of the legacy secrets + FlagProvisioningSecretsService = "provisioningSecretsService" // FlagGrafanaAPIServerEnsureKubectlAccess // Start an additional https handler and write kubectl options @@ -671,10 +671,6 @@ const ( // Enables browser crash detection reporting to Faro. FlagCrashDetection = "crashDetection" - // FlagJaegerBackendMigration - // Enables querying the Jaeger data source without the proxy - FlagJaegerBackendMigration = "jaegerBackendMigration" - // FlagAlertingUIOptimizeReducer // Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer" @@ -859,6 +855,10 @@ const ( // Enables creating metrics from profiles and storing them as recording rules FlagMetricsFromProfiles = "metricsFromProfiles" + // FlagTempoAlerting + // Enables creating alerts from Tempo data source + FlagTempoAlerting = "tempoAlerting" + // FlagPluginsAutoUpdate // Enables auto-updating of users installed plugins FlagPluginsAutoUpdate = "pluginsAutoUpdate" @@ -899,6 +899,10 @@ const ( // Enables the API to import Alertmanager configuration FlagAlertingImportAlertmanagerAPI = "alertingImportAlertmanagerAPI" + // FlagSharingDashboardImage + // Enables image sharing functionality for dashboards + FlagSharingDashboardImage = "sharingDashboardImage" + // FlagPreferLibraryPanelTitle // Prefer library panel title over viz panel title. FlagPreferLibraryPanelTitle = "preferLibraryPanelTitle" @@ -918,4 +922,8 @@ const ( // FlagFoldersAppPlatformAPI // Enables use of app platform API for folders FlagFoldersAppPlatformAPI = "foldersAppPlatformAPI" + + // FlagEnablePluginImporter + // Set this to true to use the new PluginImporter functionality + FlagEnablePluginImporter = "enablePluginImporter" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 92bf71778ba..793dd264e1c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -472,13 +472,17 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-07-06T15:06:11Z" + "resourceVersion": "1752523189825", + "creationTimestamp": "2023-07-06T15:06:11Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-14 19:59:49.825959 +0000 UTC" + } }, "spec": { "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", - "stage": "experimental", - "codeowner": "@grafana/aws-datasources" + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "expression": "true" } }, { @@ -999,6 +1003,22 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "enablePluginImporter", + "resourceVersion": "1752591728321", + "creationTimestamp": "2025-07-15T15:02:08Z" + }, + "spec": { + "description": "Set this to true to use the new PluginImporter functionality", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true, + "hideFromAdminPage": true, + "hideFromDocs": true, + "expression": "false" + } + }, { "metadata": { "name": "enableSCIM", @@ -1542,6 +1562,7 @@ "name": "jaegerBackendMigration", "resourceVersion": "1751465665226", "creationTimestamp": "2024-11-15T14:40:20Z", + "deletionTimestamp": "2025-07-07T14:12:35Z", "annotations": { "grafana.app/updatedTimestamp": "2025-07-02 14:14:25.226989 +0000 UTC" } @@ -2022,8 +2043,11 @@ { "metadata": { "name": "multiTenantTempCredentials", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-04-02T20:25:50Z" + "resourceVersion": "1752523189825", + "creationTimestamp": "2025-04-02T20:25:50Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-14 19:59:49.825959 +0000 UTC" + } }, "spec": { "description": "use multi-tenant path for awsTempCredentials", @@ -2048,7 +2072,8 @@ "metadata": { "name": "nanoGit", "resourceVersion": "1750434297879", - "creationTimestamp": "2025-06-17T17:07:30Z" + "creationTimestamp": "2025-06-17T17:07:30Z", + "deletionTimestamp": "2025-07-09T11:53:17Z" }, "spec": { "description": "Use experimental git library for provisioning", @@ -2308,6 +2333,23 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "pluginLoadingRefactor", + "resourceVersion": "1752218524617", + "creationTimestamp": "2025-07-11T07:22:04Z", + "deletionTimestamp": "2025-07-11T07:23:34Z" + }, + "spec": { + "description": "Set this to true to use the new plugin loading functionality", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true, + "hideFromAdminPage": true, + "hideFromDocs": true, + "expression": "false" + } + }, { "metadata": { "name": "pluginProxyPreserveTrailingSlash", @@ -2504,6 +2546,22 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "provisioningSecretsService", + "resourceVersion": "1752501981893", + "creationTimestamp": "2025-07-14T14:05:50Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-14 14:06:21.89322 +0000 UTC" + } + }, + "spec": { + "description": "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true + } + }, { "metadata": { "name": "publicDashboardsEmailSharing", @@ -2790,6 +2848,20 @@ "codeowner": "@grafana/grafana-operator-experience-squad" } }, + { + "metadata": { + "name": "sharingDashboardImage", + "resourceVersion": "1747090555040", + "creationTimestamp": "2025-05-12T22:55:55Z" + }, + "spec": { + "description": "Enables image sharing functionality for dashboards", + "stage": "experimental", + "codeowner": "@grafana/sharing-squad", + "frontend": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "showDashboardValidationWarnings", @@ -2805,16 +2877,19 @@ { "metadata": { "name": "skipTokenRotationIfRecent", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-06-03T06:59:40Z" + "resourceVersion": "1751872762065", + "creationTimestamp": "2025-06-03T06:59:40Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-07 07:19:22.065046 +0000 UTC" + } }, "spec": { "description": "Skip token rotation if it was already rotated less than 5 seconds ago", - "stage": "privatePreview", + "stage": "GA", "codeowner": "@grafana/identity-access-team", "hideFromAdminPage": true, "hideFromDocs": true, - "expression": "false" + "expression": "true" } }, { @@ -2940,17 +3015,17 @@ { "metadata": { "name": "tabularNumbers", - "resourceVersion": "1750754828442", + "resourceVersion": "1752078306230", "creationTimestamp": "2025-06-24T11:52:03Z", "annotations": { - "grafana.app/updatedTimestamp": "2025-06-24 08:47:08.442537461 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-07-09 16:25:06.230922635 +0000 UTC" } }, "spec": { "description": "Use fixed-width numbers globally in the UI", "stage": "GA", "codeowner": "@grafana/grafana-frontend-platform", - "expression": "true" + "expression": "false" } }, { @@ -2992,6 +3067,19 @@ "frontend": true } }, + { + "metadata": { + "name": "tempoAlerting", + "resourceVersion": "1750158458662", + "creationTimestamp": "2025-06-17T11:07:38Z" + }, + "spec": { + "description": "Enables creating alerts from Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, { "metadata": { "name": "timeRangeProvider", diff --git a/pkg/services/folder/foldertest/folder_store_mock.go b/pkg/services/folder/foldertest/folder_store_mock.go index 816dda5a4bc..1a88e59c4e0 100644 --- a/pkg/services/folder/foldertest/folder_store_mock.go +++ b/pkg/services/folder/foldertest/folder_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.32.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package foldertest @@ -14,10 +14,44 @@ type FakeFolderStore struct { mock.Mock } +// Get provides a mock function with given fields: ctx, q +func (_m *FakeFolderStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) { + ret := _m.Called(ctx, q) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *folder.Folder + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) (*folder.Folder, error)); ok { + return rf(ctx, q) + } + if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) *folder.Folder); ok { + r0 = rf(ctx, q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*folder.Folder) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, folder.GetFolderQuery) error); ok { + r1 = rf(ctx, q) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetFolderByID provides a mock function with given fields: ctx, orgID, id func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id int64) (*folder.Folder, error) { ret := _m.Called(ctx, orgID, id) + if len(ret) == 0 { + panic("no return value specified for GetFolderByID") + } + var r0 *folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*folder.Folder, error)); ok { @@ -40,36 +74,14 @@ func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id in return r0, r1 } -// Get provides a mock function with given fields: ctx, query -func (_m *FakeFolderStore) Get(ctx context.Context, query folder.GetFolderQuery) (*folder.Folder, error) { - ret := _m.Called(ctx, query) - - var r0 *folder.Folder - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) (*folder.Folder, error)); ok { - return rf(ctx, query) - } - if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) *folder.Folder); ok { - r0 = rf(ctx, query) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*folder.Folder) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, folder.GetFolderQuery) error); ok { - r1 = rf(ctx, query) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetFolderByUID provides a mock function with given fields: ctx, orgID, uid func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid string) (*folder.Folder, error) { ret := _m.Called(ctx, orgID, uid) + if len(ret) == 0 { + panic("no return value specified for GetFolderByUID") + } + var r0 *folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*folder.Folder, error)); ok { @@ -96,6 +108,10 @@ func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid func (_m *FakeFolderStore) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) { ret := _m.Called(ctx, orgID, uids) + if len(ret) == 0 { + panic("no return value specified for GetFolders") + } + var r0 map[string]*folder.Folder var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, []string) (map[string]*folder.Folder, error)); ok { @@ -118,29 +134,6 @@ func (_m *FakeFolderStore) GetFolders(ctx context.Context, orgID int64, uids []s return r0, r1 } -func (_m *FakeFolderStore) CountInOrg(ctx context.Context, orgID int64) (int64, error) { - ret := _m.Called(ctx, orgID) - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok { - return rf(ctx, orgID) - } - if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok { - r0 = rf(ctx, orgID) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { - r1 = rf(ctx, orgID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // NewFakeFolderStore creates a new instance of FakeFolderStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFakeFolderStore(t interface { diff --git a/pkg/services/frontend/frontend_service.go b/pkg/services/frontend/frontend_service.go index 81b67859ec2..b79876dd4c5 100644 --- a/pkg/services/frontend/frontend_service.go +++ b/pkg/services/frontend/frontend_service.go @@ -8,6 +8,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/log" @@ -15,6 +16,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/frontend") + type frontendService struct { *services.BasicService cfg *setting.Cfg diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index f7fdf617b35..ff18e6e5b7d 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -79,6 +79,9 @@ func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing) (*IndexProv } func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) { + _, span := tracer.Start(request.Context(), "frontend.index.HandleRequest") + defer span.End() + if request.Method != "GET" { writer.WriteHeader(http.StatusMethodNotAllowed) return diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index ea91df4fe79..96c5f82cd22 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -426,6 +426,11 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI if folderFilter.parseError != nil { return model.LibraryElementSearchResult{}, folderFilter.parseError } + + foldersWithMatchingTitles, err := getFoldersWithMatchingTitles(c, l, signedInUser, query) + if err != nil { + return model.LibraryElementSearchResult{}, err + } err = l.SQLStore.WithDbSession(c, func(session *db.Session) error { builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported) if folderFilter.includeGeneralFolder { @@ -434,7 +439,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI builder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect())) builder.Write(` WHERE le.org_id=? AND le.folder_id=0`, signedInUser.GetOrgID()) writeKindSQL(query, &builder) - writeSearchStringSQL(query, l.SQLStore, &builder) + writeSearchStringSQL(query, l.SQLStore, &builder, foldersWithMatchingTitles) writeExcludeSQL(query, &builder) writeTypeFilterSQL(typeFilter, &builder) builder.Write(" ") @@ -446,7 +451,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI builder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect())) builder.Write(` WHERE le.org_id=? AND le.folder_id<>0`, signedInUser.GetOrgID()) writeKindSQL(query, &builder) - writeSearchStringSQL(query, l.SQLStore, &builder) + writeSearchStringSQL(query, l.SQLStore, &builder, foldersWithMatchingTitles) writeExcludeSQL(query, &builder) writeTypeFilterSQL(typeFilter, &builder) if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil { @@ -525,7 +530,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect())) countBuilder.Write(` WHERE le.org_id=? AND le.folder_id=0`, signedInUser.GetOrgID()) writeKindSQL(query, &countBuilder) - writeSearchStringSQL(query, l.SQLStore, &countBuilder) + writeSearchStringSQL(query, l.SQLStore, &countBuilder, foldersWithMatchingTitles) writeExcludeSQL(query, &countBuilder) writeTypeFilterSQL(typeFilter, &countBuilder) countBuilder.Write(" ") @@ -536,7 +541,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect())) countBuilder.Write(` WHERE le.org_id=? AND le.folder_id<>0`, signedInUser.GetOrgID()) writeKindSQL(query, &countBuilder) - writeSearchStringSQL(query, l.SQLStore, &countBuilder) + writeSearchStringSQL(query, l.SQLStore, &countBuilder, foldersWithMatchingTitles) writeExcludeSQL(query, &countBuilder) writeTypeFilterSQL(typeFilter, &countBuilder) if err := folderFilter.writeFolderFilterSQL(true, &countBuilder); err != nil { @@ -882,3 +887,45 @@ func contains(slice []string, element string) bool { } return false } + +func getFoldersWithMatchingTitles(c context.Context, l *LibraryElementService, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) ([]string, error) { + if len(strings.TrimSpace(query.SearchString)) <= 0 { + return nil, nil + } + + if l.features.IsEnabled(c, featuremgmt.FlagKubernetesClientDashboardsFolders) { + searchQuery := folder.SearchFoldersQuery{ + OrgID: signedInUser.GetOrgID(), + Title: query.SearchString, + SignedInUser: signedInUser, + } + + folderHits, err := l.folderService.SearchFolders(c, searchQuery) + if err != nil { + return nil, err + } + + foldersWithMatchingTitles := make([]string, 0, len(folderHits)) + for _, hit := range folderHits { + foldersWithMatchingTitles = append(foldersWithMatchingTitles, hit.UID) + } + return foldersWithMatchingTitles, nil + } + + // Fallback to GetFolders + fs, err := l.folderService.GetFolders(c, folder.GetFoldersQuery{ + OrgID: signedInUser.GetOrgID(), + SignedInUser: signedInUser, + }) + if err != nil { + return nil, err + } + + foldersWithMatchingTitles := make([]string, 0, len(fs)) + for _, f := range fs { + if strings.Contains(strings.ToLower(f.Title), strings.ToLower(query.SearchString)) { + foldersWithMatchingTitles = append(foldersWithMatchingTitles, f.UID) + } + } + return foldersWithMatchingTitles, nil +} diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 3c61d08b8ab..970730399c9 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -1164,4 +1164,183 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } }) + + // Folder name search integration tests + scenarioWithPanel(t, "When searching by folder name, it should return panels in that folder", + func(t *testing.T, sc scenarioContext) { + // Create a panel in the existing folder + // nolint:staticcheck + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Panel in ScenarioFolder") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Search by folder name + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("searchString", "ScenarioFolder") + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // Verify folder search finds both panels + require.Equal(t, int64(2), result.Result.TotalCount) + require.Equal(t, 2, len(result.Result.Elements)) + require.Equal(t, 1, result.Result.Page) + require.Equal(t, 100, result.Result.PerPage) + for _, element := range result.Result.Elements { + require.Equal(t, int64(1), element.OrgID, "Should be in org 1") + require.Equal(t, sc.folder.UID, element.FolderUID, "Should have correct folder UID") + require.Equal(t, "ScenarioFolder", element.Meta.FolderName, "All panels should be in ScenarioFolder") + require.Equal(t, int64(model.PanelElement), element.Kind, "Should be a panel element") + require.Equal(t, "text", element.Type, "Should be text panel") + require.NotEmpty(t, element.UID, "Should have a UID") + require.NotEmpty(t, element.Name, "Should have a name") + require.Equal(t, int64(0), element.Meta.ConnectedDashboards, "Should have no connected dashboards") + } + }) + + scenarioWithPanel(t, "When searching by folder name that doesn't exist, it should return no results", + func(t *testing.T, sc scenarioContext) { + // Search by non-existent folder name + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("searchString", "NonExistentFolder") + resp := sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // Verify no results for non-existent folder + expected := libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Elements: []libraryElement{}, + Page: 1, + PerPage: 100, + }, + } + if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + require.Equal(t, 0, len(result.Result.Elements), "Should return empty elements array") + }) + + scenarioWithPanel(t, "When searching with folder filter applied, folder name search should be disabled", + func(t *testing.T, sc scenarioContext) { + // Create a panel in the existing folder + // nolint:staticcheck + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Panel for folder filter test") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Search with both searchString and folderFilterUIDs + // This should NOT search folder names, only panel names/descriptions within the specified folder + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("searchString", "ScenarioFolder") + sc.reqContext.Req.Form.Add("folderFilterUIDs", sc.folder.UID) + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // Folder name search disabled when folder filter applied + expected := libraryElementsSearch{ + Result: libraryElementsSearchResult{ + TotalCount: 0, + Elements: []libraryElement{}, + Page: 1, + PerPage: 100, + }, + } + if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + + require.Equal(t, int64(0), result.Result.TotalCount, "Should not find panels by folder name when folder filter is applied") + require.Equal(t, 0, len(result.Result.Elements), "Should return empty elements array") + }) + + scenarioWithPanel(t, "When searching by partial folder name, it should return panels in matching folders", + func(t *testing.T, sc scenarioContext) { + // Create a panel in the existing folder + // nolint:staticcheck + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Test Panel") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Search by partial folder name + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("searchString", "Scenario") + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // Verify partial folder name matching + require.Equal(t, int64(2), result.Result.TotalCount, "Should find exactly 2 panels by partial folder name") + require.Equal(t, 2, len(result.Result.Elements), "Should return 2 elements") + require.Equal(t, 1, result.Result.Page, "Should be on page 1") + require.Equal(t, 100, result.Result.PerPage, "Should have perPage 100") + + for _, element := range result.Result.Elements { + require.Equal(t, int64(1), element.OrgID, "Should be in org 1") + require.Equal(t, sc.folder.UID, element.FolderUID, "Should have correct folder UID") + require.Contains(t, element.Meta.FolderName, "Scenario", "Folder name should contain 'Scenario'") + require.Equal(t, "ScenarioFolder", element.Meta.FolderName, "Should be in ScenarioFolder") + require.Equal(t, int64(model.PanelElement), element.Kind, "Should be a panel element") + require.NotEmpty(t, element.UID, "Should have a UID") + require.NotEmpty(t, element.Name, "Should have a name") + } + }) + + scenarioWithPanel(t, "When searching combines panel name and folder name matches, it should return both", + func(t *testing.T, sc scenarioContext) { + // Create a panel with a specific name + // nolint:staticcheck + command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Marketing Report Panel") + sc.reqContext.Req.Body = mockRequestBody(command) + resp := sc.service.createHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + // Search for "Marketing" which could match both panel name and folder name + err := sc.reqContext.Req.ParseForm() + require.NoError(t, err) + sc.reqContext.Req.Form.Add("searchString", "Marketing") + resp = sc.service.getAllHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + + var result libraryElementsSearch + err = json.Unmarshal(resp.Body(), &result) + require.NoError(t, err) + + // Verify panel name matching + require.Equal(t, int64(1), result.Result.TotalCount, "Should find exactly 1 panel with 'Marketing' in name") + require.Equal(t, 1, len(result.Result.Elements), "Should return 1 element") + require.Equal(t, 1, result.Result.Page, "Should be on page 1") + require.Equal(t, 100, result.Result.PerPage, "Should have perPage 100") + + element := result.Result.Elements[0] + require.Equal(t, "Marketing Report Panel", element.Name, "Should find panel with correct name") + require.Equal(t, int64(1), element.OrgID, "Should be in org 1") + require.Equal(t, sc.folder.UID, element.FolderUID, "Should have correct folder UID") + require.Equal(t, "ScenarioFolder", element.Meta.FolderName, "Should be in ScenarioFolder") + require.Equal(t, int64(model.PanelElement), element.Kind, "Should be a panel element") + require.Equal(t, "text", element.Type, "Should be text panel") + require.NotEmpty(t, element.UID, "Should have a UID") + require.Equal(t, int64(0), element.Meta.ConnectedDashboards, "Should have no connected dashboards") + }) } diff --git a/pkg/services/libraryelements/writers.go b/pkg/services/libraryelements/writers.go index c35dedcd737..39bfadbb312 100644 --- a/pkg/services/libraryelements/writers.go +++ b/pkg/services/libraryelements/writers.go @@ -58,13 +58,28 @@ func writeTypeFilterSQL(typeFilter []string, builder *db.SQLBuilder) { } } -func writeSearchStringSQL(query model.SearchLibraryElementsQuery, sqlStore db.DB, builder *db.SQLBuilder) { +func writeSearchStringSQL(query model.SearchLibraryElementsQuery, sqlStore db.DB, builder *db.SQLBuilder, foldersWithMatchingTitles []string) { if len(strings.TrimSpace(query.SearchString)) > 0 { + // Search element names across all accessible folders sql, param := sqlStore.GetDialect().LikeOperator("le.name", true, query.SearchString, true) builder.Write(" AND ("+sql, param) + // Search element descriptions across all accessible folders sql, param = sqlStore.GetDialect().LikeOperator("le.description", true, query.SearchString, true) - builder.Write(" OR "+sql+")", param) + builder.Write(" OR "+sql, param) + + // Include ALL elements from folders whose titles match the search string + hasFolderFilter := len(strings.TrimSpace(query.FolderFilterUIDs)) > 0 + if !hasFolderFilter && len(foldersWithMatchingTitles) > 0 { + folderUIDsSQL := "?" + strings.Repeat(",?", len(foldersWithMatchingTitles)-1) + params := make([]any, 0, len(foldersWithMatchingTitles)) + for _, folderUID := range foldersWithMatchingTitles { + params = append(params, folderUID) + } + builder.Write(" OR le.folder_uid IN ("+folderUIDsSQL+")", params...) + } + + builder.Write(")") } } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 737fd5b9782..42770b6ee96 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -341,7 +341,7 @@ func (s *ServiceImpl) readNavigationSettings() { s.navigationAppConfig["grafana-advisor-app"] = NavigationAppConfig{ SectionID: navtree.NavIDCfg, Text: "Advisor", - SubTitle: "Keep Grafana running smoothly and securely", + SubTitle: "Run checks and get suggestions to fix issues", IsNew: true, } } diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index a7517a927f4..7ae65815da3 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -37,7 +37,6 @@ type ServiceImpl struct { pluginSettings pluginsettings.Service starService star.Service features featuremgmt.FeatureToggles - openFeature *featuremgmt.OpenFeatureService dashboardService dashboards.DashboardService accesscontrolService ac.Service kvStore kvstore.KVStore @@ -60,7 +59,7 @@ type NavigationAppConfig struct { func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, starService star.Service, features featuremgmt.FeatureToggles, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service, - license licensing.Licensing, authnService authn.Service, openFeature *featuremgmt.OpenFeatureService) navtree.Service { + license licensing.Licensing, authnService authn.Service) navtree.Service { service := &ServiceImpl{ cfg: cfg, log: log.New("navtree service"), @@ -70,7 +69,6 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor pluginSettings: pluginSettings, starService: starService, features: features, - openFeature: openFeature, dashboardService: dashboardService, accesscontrolService: accesscontrolService, kvStore: kvStore, @@ -189,7 +187,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere treeRoot.RemoveSectionByID(navtree.NavIDCfg) } - enabled := s.openFeature.Client.Boolean(ctx, featuremgmt.FlagPinNavItems, true, openfeature.TransactionContext(ctx)) + enabled := openfeature.GetApiInstance().GetClient().Boolean(ctx, featuremgmt.FlagPinNavItems, true, openfeature.TransactionContext(ctx)) if enabled && c.IsSignedIn { treeRoot.AddSection(&navtree.NavLink{ Text: "Bookmarks", diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 1cdacebed4e..00f57988df9 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -2034,7 +2034,7 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv { func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv { t.Helper() tracer := tracing.InitializeTracerForTest() - configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs) + configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs, notifier.NewExtraConfigsCrypto(env.secrets)) receiverSvc := notifier.NewReceiverService( ac.NewReceiverAccess[*models.Receiver](env.ac, true), configStore, diff --git a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go index 4400a37dfae..1a345e7be84 100644 --- a/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go +++ b/pkg/services/ngalert/eval/eval_mocks/ConditionEvaluator.go @@ -1,15 +1,17 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package eval_mocks import ( context "context" - time "time" backend "github.com/grafana/grafana-plugin-sdk-go/backend" - mock "github.com/stretchr/testify/mock" eval "github.com/grafana/grafana/pkg/services/ngalert/eval" + + mock "github.com/stretchr/testify/mock" + + time "time" ) // ConditionEvaluatorMock is an autogenerated mock type for the ConditionEvaluator type @@ -29,7 +31,15 @@ func (_m *ConditionEvaluatorMock) EXPECT() *ConditionEvaluatorMock_Expecter { func (_m *ConditionEvaluatorMock) Evaluate(ctx context.Context, now time.Time) (eval.Results, error) { ret := _m.Called(ctx, now) + if len(ret) == 0 { + panic("no return value specified for Evaluate") + } + var r0 eval.Results + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time) (eval.Results, error)); ok { + return rf(ctx, now) + } if rf, ok := ret.Get(0).(func(context.Context, time.Time) eval.Results); ok { r0 = rf(ctx, now) } else { @@ -38,7 +48,6 @@ func (_m *ConditionEvaluatorMock) Evaluate(ctx context.Context, now time.Time) ( } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok { r1 = rf(ctx, now) } else { @@ -56,7 +65,7 @@ type ConditionEvaluatorMock_Evaluate_Call struct { // Evaluate is a helper method to define mock.On call // - ctx context.Context // - now time.Time -func (_e *ConditionEvaluatorMock_Expecter) Evaluate(ctx any, now any) *ConditionEvaluatorMock_Evaluate_Call { +func (_e *ConditionEvaluatorMock_Expecter) Evaluate(ctx interface{}, now interface{}) *ConditionEvaluatorMock_Evaluate_Call { return &ConditionEvaluatorMock_Evaluate_Call{Call: _e.mock.On("Evaluate", ctx, now)} } @@ -72,11 +81,24 @@ func (_c *ConditionEvaluatorMock_Evaluate_Call) Return(_a0 eval.Results, _a1 err return _c } +func (_c *ConditionEvaluatorMock_Evaluate_Call) RunAndReturn(run func(context.Context, time.Time) (eval.Results, error)) *ConditionEvaluatorMock_Evaluate_Call { + _c.Call.Return(run) + return _c +} + // EvaluateRaw provides a mock function with given fields: ctx, now func (_m *ConditionEvaluatorMock) EvaluateRaw(ctx context.Context, now time.Time) (*backend.QueryDataResponse, error) { ret := _m.Called(ctx, now) + if len(ret) == 0 { + panic("no return value specified for EvaluateRaw") + } + var r0 *backend.QueryDataResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time) (*backend.QueryDataResponse, error)); ok { + return rf(ctx, now) + } if rf, ok := ret.Get(0).(func(context.Context, time.Time) *backend.QueryDataResponse); ok { r0 = rf(ctx, now) } else { @@ -85,7 +107,6 @@ func (_m *ConditionEvaluatorMock) EvaluateRaw(ctx context.Context, now time.Time } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok { r1 = rf(ctx, now) } else { @@ -103,7 +124,7 @@ type ConditionEvaluatorMock_EvaluateRaw_Call struct { // EvaluateRaw is a helper method to define mock.On call // - ctx context.Context // - now time.Time -func (_e *ConditionEvaluatorMock_Expecter) EvaluateRaw(ctx any, now any) *ConditionEvaluatorMock_EvaluateRaw_Call { +func (_e *ConditionEvaluatorMock_Expecter) EvaluateRaw(ctx interface{}, now interface{}) *ConditionEvaluatorMock_EvaluateRaw_Call { return &ConditionEvaluatorMock_EvaluateRaw_Call{Call: _e.mock.On("EvaluateRaw", ctx, now)} } @@ -119,13 +140,17 @@ func (_c *ConditionEvaluatorMock_EvaluateRaw_Call) Return(resp *backend.QueryDat return _c } -type mockConstructorTestingTNewConditionEvaluatorMock interface { - mock.TestingT - Cleanup(func()) +func (_c *ConditionEvaluatorMock_EvaluateRaw_Call) RunAndReturn(run func(context.Context, time.Time) (*backend.QueryDataResponse, error)) *ConditionEvaluatorMock_EvaluateRaw_Call { + _c.Call.Return(run) + return _c } // NewConditionEvaluatorMock creates a new instance of ConditionEvaluatorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewConditionEvaluatorMock(t mockConstructorTestingTNewConditionEvaluatorMock) *ConditionEvaluatorMock { +// The first argument is typically a *testing.T value. +func NewConditionEvaluatorMock(t interface { + mock.TestingT + Cleanup(func()) +}) *ConditionEvaluatorMock { mock := &ConditionEvaluatorMock{} mock.Mock.Test(t) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index dcb3b571bee..9c386d3254a 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -425,7 +425,7 @@ func (ng *AlertNG) init() error { ng.stateManager = stateManager ng.schedule = scheduler - configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store) + configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store, notifier.NewExtraConfigsCrypto(ng.SecretsService)) receiverService := notifier.NewReceiverService( ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, false), configStore, diff --git a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go index a3419fdc508..20f4e14d0ab 100644 --- a/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager_mock/Alertmanager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.44.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package alertmanager_mock @@ -585,7 +585,7 @@ func (_c *AlertmanagerMock_PutAlerts_Call) RunAndReturn(run func(context.Context return _c } -// Ready provides a mock function with given fields: +// Ready provides a mock function with no fields func (_m *AlertmanagerMock) Ready() bool { ret := _m.Called() @@ -781,7 +781,7 @@ func (_c *AlertmanagerMock_SilenceState_Call) RunAndReturn(run func(context.Cont return _c } -// StopAndWait provides a mock function with given fields: +// StopAndWait provides a mock function with no fields func (_m *AlertmanagerMock) StopAndWait() { _m.Called() } @@ -809,7 +809,7 @@ func (_c *AlertmanagerMock_StopAndWait_Call) Return() *AlertmanagerMock_StopAndW } func (_c *AlertmanagerMock_StopAndWait_Call) RunAndReturn(run func()) *AlertmanagerMock_StopAndWait_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/pkg/services/ngalert/notifier/alerts.go b/pkg/services/ngalert/notifier/alerts.go index 2052b2a99d6..ed55a72e4ef 100644 --- a/pkg/services/ngalert/notifier/alerts.go +++ b/pkg/services/ngalert/notifier/alerts.go @@ -13,3 +13,7 @@ func (am *alertmanager) GetAlerts(_ context.Context, active, silenced, inhibited func (am *alertmanager) GetAlertGroups(_ context.Context, active, silenced, inhibited bool, filter []string, receivers string) (alertingNotify.AlertGroups, error) { return am.Base.GetAlertGroups(active, silenced, inhibited, filter, receivers) } + +func (am *alertmanager) MergeNflog(b []byte) error { + return am.Base.MergeNflog(b) +} diff --git a/pkg/services/ngalert/notifier/crypto.go b/pkg/services/ngalert/notifier/crypto.go index 7a80a70c27a..5d682ad8f62 100644 --- a/pkg/services/ngalert/notifier/crypto.go +++ b/pkg/services/ngalert/notifier/crypto.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -14,6 +15,16 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" ) +const ( + // encryptedContentPrefix is a marker that identifies encrypted Alertmanager configurations. + // When this prefix is present at the beginning of a configuration string: + // 1. During encryption: It indicates the content is already encrypted and should be skipped + // 2. During decryption: It indicates the content (minus this prefix) should be base64 decoded + // and then decrypted using the secrets service + // This prefix helps maintain idempotency in encryption/decryption operations. + cryptoPrefix = "crypto_" +) + // Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads. type Crypto interface { LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error @@ -28,16 +39,16 @@ type Crypto interface { // alertmanagerCrypto implements decryption of Alertmanager configuration and encryption of arbitrary payloads based on Grafana's encryptions. type alertmanagerCrypto struct { - secrets secrets.Service + *ExtraConfigsCrypto configs configurationStore log log.Logger } func NewCrypto(secrets secrets.Service, configs configurationStore, log log.Logger) Crypto { return &alertmanagerCrypto{ - secrets: secrets, - configs: configs, - log: log, + ExtraConfigsCrypto: NewExtraConfigsCrypto(secrets), + configs: configs, + log: log, } } @@ -241,27 +252,45 @@ func (c *alertmanagerCrypto) Decrypt(ctx context.Context, payload []byte) ([]byt return c.secrets.Decrypt(ctx, payload) } -func (c *alertmanagerCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error { +type ExtraConfigsCrypto struct { + secrets secretService +} + +func NewExtraConfigsCrypto(secrets secretService) *ExtraConfigsCrypto { + return &ExtraConfigsCrypto{ + secrets: secrets, + } +} + +func (c *ExtraConfigsCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error { for i := range config.ExtraConfigs { + // If it has prefix, consider it encrypted already + if strings.HasPrefix(config.ExtraConfigs[i].AlertmanagerConfig, cryptoPrefix) { + continue + } + encryptedValue, err := c.secrets.Encrypt(ctx, []byte(config.ExtraConfigs[i].AlertmanagerConfig), secrets.WithoutScope()) if err != nil { return fmt.Errorf("failed to encrypt extra configuration: %w", err) } - config.ExtraConfigs[i].AlertmanagerConfig = base64.StdEncoding.EncodeToString(encryptedValue) + config.ExtraConfigs[i].AlertmanagerConfig = cryptoPrefix + base64.StdEncoding.EncodeToString(encryptedValue) } return nil } -func (c *alertmanagerCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error { +func (c *ExtraConfigsCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error { for i := range config.ExtraConfigs { - // Check if the config is encrypted by trying to base64 decode it - encryptedValue, err := base64.StdEncoding.DecodeString(config.ExtraConfigs[i].AlertmanagerConfig) - if err != nil { - // If it can't be base64 decoded, assume it's already decrypted and skip + // If it does not have prefix, consider it decrypted already + if !strings.HasPrefix(config.ExtraConfigs[i].AlertmanagerConfig, cryptoPrefix) { continue } + // Check if the config is encrypted by trying to base64 decode it + encryptedValue, err := base64.StdEncoding.DecodeString(config.ExtraConfigs[i].AlertmanagerConfig[len(cryptoPrefix):]) + if err != nil { + return fmt.Errorf("failed to decode extra configuration: %w", err) + } decryptedValue, err := c.secrets.Decrypt(ctx, encryptedValue) if err != nil { diff --git a/pkg/services/ngalert/notifier/crypto_test.go b/pkg/services/ngalert/notifier/crypto_test.go new file mode 100644 index 00000000000..94a78c7d402 --- /dev/null +++ b/pkg/services/ngalert/notifier/crypto_test.go @@ -0,0 +1,110 @@ +package notifier + +import ( + "context" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/secrets/fakes" +) + +func TestEncryptExtraConfigs(t *testing.T) { + config := "plain-text-config" + encryptedConfig := base64.StdEncoding.EncodeToString([]byte(config)) + tests := []struct { + name string + inputConfig string + expectedConfig string + }{ + { + name: "Encrypts unencrypted configs", + inputConfig: config, + expectedConfig: cryptoPrefix + encryptedConfig, + }, + { + name: "Skips already encrypted configs", + inputConfig: cryptoPrefix + "very-encrypted-data", + expectedConfig: cryptoPrefix + "very-encrypted-data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := fakes.NewFakeSecretsService() + + c := &alertmanagerCrypto{ + ExtraConfigsCrypto: &ExtraConfigsCrypto{ + secrets: m, + }, + } + + cfg := &definitions.PostableUserConfig{ + ExtraConfigs: []definitions.ExtraConfiguration{ + {AlertmanagerConfig: tt.inputConfig}, + }, + } + + err := c.EncryptExtraConfigs(context.Background(), cfg) + + require.NoError(t, err) + require.Equal(t, tt.expectedConfig, cfg.ExtraConfigs[0].AlertmanagerConfig) + }) + } +} + +func TestDecryptExtraConfigs(t *testing.T) { + decryptedData := "derypted-data" + decryptedDataBase64 := base64.StdEncoding.EncodeToString([]byte(decryptedData)) + tests := []struct { + name string + inputConfig string + expectedError string + expectedConfig string + }{ + { + name: "Decrypts encrypted configs", + inputConfig: cryptoPrefix + decryptedDataBase64, + expectedConfig: decryptedData, + }, + { + name: "Skips already encrypted configs", + inputConfig: "very-decrypted-data", + expectedConfig: "very-decrypted-data", + }, + { + name: "Fails if not base64 encoded", + inputConfig: cryptoPrefix + "plain-text-config", + expectedError: "failed to decode extra configuration", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := fakes.NewFakeSecretsService() + c := &alertmanagerCrypto{ + ExtraConfigsCrypto: &ExtraConfigsCrypto{ + secrets: m, + }, + } + + cfg := &definitions.PostableUserConfig{ + ExtraConfigs: []definitions.ExtraConfiguration{ + {AlertmanagerConfig: tt.inputConfig}, + }, + } + + err := c.DecryptExtraConfigs(context.Background(), cfg) + + if tt.expectedError != "" { + require.ErrorContains(t, err, tt.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectedConfig, cfg.ExtraConfigs[0].AlertmanagerConfig) + }) + } +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/config.go b/pkg/services/ngalert/notifier/legacy_storage/config.go index 0a7e904544f..026d48f5139 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/config.go +++ b/pkg/services/ngalert/notifier/legacy_storage/config.go @@ -9,6 +9,11 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) +type crypto interface { + EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error + DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error +} + type amConfigStore interface { GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error @@ -31,9 +36,17 @@ type ConfigRevision struct { ConcurrencyToken string Version string } +type alertmanagerConfigStoreImpl struct { + store amConfigStore + crypto crypto +} -func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore) (*ConfigRevision, error) { - alertManagerConfig, err := store.GetLatestAlertmanagerConfiguration(ctx, orgID) +func NewAlertmanagerConfigStore(store amConfigStore, crypto crypto) *alertmanagerConfigStoreImpl { + return &alertmanagerConfigStoreImpl{store: store, crypto: crypto} +} + +func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*ConfigRevision, error) { + alertManagerConfig, err := a.store.GetLatestAlertmanagerConfiguration(ctx, orgID) if err != nil { return nil, err } @@ -48,6 +61,11 @@ func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore) return nil, err } + err = a.crypto.DecryptExtraConfigs(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("failed to decrypt extra configurations: %w", err) + } + return &ConfigRevision{ Config: cfg, ConcurrencyToken: concurrencyToken, @@ -55,38 +73,21 @@ func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore) }, nil } -type alertmanagerConfigStoreImpl struct { - store amConfigStore -} - -func NewAlertmanagerConfigStore(store amConfigStore) *alertmanagerConfigStoreImpl { - return &alertmanagerConfigStoreImpl{store: store} -} - -func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*ConfigRevision, error) { - return getLastConfiguration(ctx, orgID, a.store) -} - func (a alertmanagerConfigStoreImpl) Save(ctx context.Context, revision *ConfigRevision, orgID int64) error { + err := a.crypto.EncryptExtraConfigs(ctx, revision.Config) + if err != nil { + return fmt.Errorf("failed to encrypt extra configurations: %w", err) + } + serialized, err := SerializeAlertmanagerConfig(*revision.Config) if err != nil { return err } - cmd := models.SaveAlertmanagerConfigurationCmd{ + return a.store.UpdateAlertmanagerConfiguration(ctx, &models.SaveAlertmanagerConfigurationCmd{ AlertmanagerConfiguration: string(serialized), ConfigurationVersion: revision.Version, FetchedConfigurationHash: revision.ConcurrencyToken, Default: false, OrgID: orgID, - } - return a.PersistConfig(ctx, &cmd) -} - -// PersistConfig validates to config before eventually persisting it if no error occurs -func (a alertmanagerConfigStoreImpl) PersistConfig(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { - cfg := &definitions.PostableUserConfig{} - if err := json.Unmarshal([]byte(cmd.AlertmanagerConfiguration), cfg); err != nil { - return fmt.Errorf("change would result in an invalid configuration state: %w", err) - } - return a.store.UpdateAlertmanagerConfiguration(ctx, cmd) + }) } diff --git a/pkg/services/ngalert/notifier/legacy_storage/config_test.go b/pkg/services/ngalert/notifier/legacy_storage/config_test.go index 549fdab12ea..7019ad04e37 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/config_test.go +++ b/pkg/services/ngalert/notifier/legacy_storage/config_test.go @@ -22,8 +22,9 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) { orgID := int64(1) t.Run("should read the latest config for giving organization", func(t *testing.T) { + cryptoMock := newFakeCrypto() storeMock := &MockAMConfigStore{} - store := &alertmanagerConfigStoreImpl{store: storeMock} + store := NewAlertmanagerConfigStore(storeMock, cryptoMock) expected := models.AlertConfiguration{ ID: 1, @@ -48,12 +49,18 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) { require.Equal(t, expectedCfg, *revision.Config) storeMock.AssertCalled(t, "GetLatestAlertmanagerConfiguration", mock.Anything, orgID) + + t.Run("should decrypt extra configs ", func(t *testing.T) { + require.Len(t, cryptoMock.Calls, 1) + require.Equal(t, "DecryptExtraConfigs", cryptoMock.Calls[0].Method) + require.Equal(t, &expectedCfg, cryptoMock.Calls[0].Args[1]) + }) }) t.Run("propagate errors", func(t *testing.T) { t.Run("when underlying store fails", func(t *testing.T) { storeMock := &MockAMConfigStore{} - store := &alertmanagerConfigStoreImpl{store: storeMock} + store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto()) expectedErr := errors.New("test=err") storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil, expectedErr) @@ -63,7 +70,7 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) { t.Run("return ErrNoAlertmanagerConfiguration config does not exist", func(t *testing.T) { storeMock := &MockAMConfigStore{} - store := &alertmanagerConfigStoreImpl{store: storeMock} + store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto()) storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil, nil) _, err := store.Get(context.Background(), orgID) @@ -72,7 +79,7 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) { t.Run("when config cannot be unmarshalled", func(t *testing.T) { storeMock := &MockAMConfigStore{} - store := &alertmanagerConfigStoreImpl{store: storeMock} + store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto()) storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&models.AlertConfiguration{ AlertmanagerConfiguration: "invalid-json", }, nil) @@ -80,6 +87,23 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) { _, err := store.Get(context.Background(), orgID) require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) + + t.Run("when decrypting extra configs fails", func(t *testing.T) { + cryptoMock := newFakeCrypto() + storeMock := &MockAMConfigStore{} + store := NewAlertmanagerConfigStore(storeMock, cryptoMock) + + expectedErr := errors.New("test-err") + cryptoMock.DecryptExtraConfigsFunc = func(ctx context.Context, config *definitions.PostableUserConfig) error { + return expectedErr + } + storeMock.EXPECT().GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&models.AlertConfiguration{ + AlertmanagerConfiguration: defaultConfig, + }, nil) + + _, err := store.Get(context.Background(), orgID) + require.ErrorIs(t, err, expectedErr) + }) }) } @@ -98,8 +122,9 @@ func TestAlertmanagerConfigStoreSave(t *testing.T) { } t.Run("should save the config to store", func(t *testing.T) { + cryptoMock := newFakeCrypto() storeMock := &MockAMConfigStore{} - store := &alertmanagerConfigStoreImpl{store: storeMock} + store := NewAlertmanagerConfigStore(storeMock, cryptoMock) storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { assert.Equal(t, string(expectedCfg), cmd.AlertmanagerConfiguration) @@ -114,17 +139,39 @@ func TestAlertmanagerConfigStoreSave(t *testing.T) { require.NoError(t, err) storeMock.AssertCalled(t, "UpdateAlertmanagerConfiguration", mock.Anything, mock.Anything) + + t.Run("should encrypt extra configs ", func(t *testing.T) { + require.Len(t, cryptoMock.Calls, 1) + require.Equal(t, "EncryptExtraConfigs", cryptoMock.Calls[0].Method) + require.Equal(t, &cfg, cryptoMock.Calls[0].Args[1]) + }) }) - t.Run("propagates errors when underlying storage returns error", func(t *testing.T) { - storeMock := &MockAMConfigStore{} - store := &alertmanagerConfigStoreImpl{store: storeMock} + t.Run("propagates errors", func(t *testing.T) { + t.Run("when underlying storage returns error", func(t *testing.T) { + storeMock := &MockAMConfigStore{} + store := NewAlertmanagerConfigStore(storeMock, newFakeCrypto()) - expectedErr := errors.New("test-err") - storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(expectedErr) + expectedErr := errors.New("test-err") + storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(expectedErr) - err := store.Save(context.Background(), &revision, orgID) + err := store.Save(context.Background(), &revision, orgID) - require.ErrorIs(t, err, expectedErr) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("when encrypting extra configs fails", func(t *testing.T) { + cryptoMock := newFakeCrypto() + storeMock := &MockAMConfigStore{} + store := NewAlertmanagerConfigStore(storeMock, cryptoMock) + + expectedErr := errors.New("test-err") + cryptoMock.EncryptExtraConfigsFunc = func(ctx context.Context, config *definitions.PostableUserConfig) error { + return expectedErr + } + + err := store.Save(context.Background(), &revision, orgID) + require.ErrorIs(t, err, expectedErr) + }) }) } diff --git a/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go b/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go index 03b5f825775..03b7b893757 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go +++ b/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.44.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package legacy_storage diff --git a/pkg/services/ngalert/notifier/legacy_storage/testing.go b/pkg/services/ngalert/notifier/legacy_storage/testing.go index a4b5d9d5eba..59ead936575 100644 --- a/pkg/services/ngalert/notifier/legacy_storage/testing.go +++ b/pkg/services/ngalert/notifier/legacy_storage/testing.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/mock" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -59,3 +60,39 @@ func (a *AlertmanagerConfigStoreFake) Save(ctx context.Context, revision *Config } return nil } + +type fakeCrypto struct { + Calls []methodCall + EncryptExtraConfigsFunc func(context.Context, *definitions.PostableUserConfig) error + DecryptExtraConfigsFunc func(context.Context, *definitions.PostableUserConfig) error +} + +func newFakeCrypto() *fakeCrypto { + return &fakeCrypto{ + Calls: []methodCall{}, + } +} + +func (f *fakeCrypto) EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error { + f.Calls = append(f.Calls, methodCall{ + Method: "EncryptExtraConfigs", + Args: []interface{}{ctx, config}, + }) + + if f.EncryptExtraConfigsFunc != nil { + return f.EncryptExtraConfigsFunc(ctx, config) + } + return nil +} + +func (f *fakeCrypto) DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error { + f.Calls = append(f.Calls, methodCall{ + Method: "DecryptExtraConfigs", + Args: []interface{}{ctx, config}, + }) + + if f.DecryptExtraConfigsFunc != nil { + return f.DecryptExtraConfigsFunc(ctx, config) + } + return nil +} diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager.go b/pkg/services/ngalert/notifier/multiorg_alertmanager.go index fcc4a4d48d9..7b52cb68c3b 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager.go @@ -78,6 +78,12 @@ type Alertmanager interface { Ready() bool } +// StateMerger describes a type that is able to merge external state (nflog, silences) with its own. +type StateMerger interface { + MergeNflog([]byte) error + MergeSilences([]byte) error +} + type MultiOrgAlertmanager struct { Crypto Crypto ProvStore provisioningStore diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go index 4c617b3a3b4..81b8aebb766 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go @@ -109,7 +109,7 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { // It should send config and state on startup. var lastConfig *remoteClient.UserGrafanaConfig - var lastState *remoteClient.UserGrafanaState + var lastState *remoteClient.UserState { // We should start with no config and no state in the external Alertmanager. require.Empty(t, fakeAM.config) @@ -175,7 +175,7 @@ func newFakeRemoteAlertmanager(t *testing.T, user, pass string) *fakeRemoteAlert type fakeRemoteAlertmanager struct { t *testing.T config *remoteClient.UserGrafanaConfig - state *remoteClient.UserGrafanaState + state *remoteClient.UserState username string password string } @@ -241,7 +241,7 @@ func (f *fakeRemoteAlertmanager) getConfig(w http.ResponseWriter) { } func (f *fakeRemoteAlertmanager) postState(w http.ResponseWriter, r *http.Request) { - var state remoteClient.UserGrafanaState + var state remoteClient.UserState require.NoError(f.t, json.NewDecoder(r.Body).Decode(&state)) f.state = &state diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 7cab4c03442..5ed24d2db2f 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -275,7 +275,7 @@ func TestReceiverService_Delete(t *testing.T) { deleteUID: baseReceiver.UID, callerProvenance: definitions.Provenance(models.ProvenanceFile), existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI))), - //expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceAPI, models.ProvenanceFile), + // expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceAPI, models.ProvenanceFile), }, { name: "delete receiver with optimistic version mismatch fails", @@ -673,7 +673,7 @@ func TestReceiverService_Update(t *testing.T) { user: writer, receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI)), existing: util.Pointer(models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceFile))), - //expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceAPI), + // expectedErr: validation.MakeErrProvenanceChangeNotAllowed(models.ProvenanceFile, models.ProvenanceAPI), expectedUpdate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithProvenance(models.ProvenanceAPI), rm.Encrypted(models.Base64Enrypt)), @@ -1529,7 +1529,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverS return NewReceiverService( ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), false), - legacy_storage.NewAlertmanagerConfigStore(store), + legacy_storage.NewAlertmanagerConfigStore(store, NewExtraConfigsCrypto(encryptSvc)), provisioningStore, &fakeAlertRuleNotificationStore{}, encryptSvc, diff --git a/pkg/services/ngalert/notifier/silences.go b/pkg/services/ngalert/notifier/silences.go index 905fe231f8d..dca86cb12cf 100644 --- a/pkg/services/ngalert/notifier/silences.go +++ b/pkg/services/ngalert/notifier/silences.go @@ -21,3 +21,7 @@ func (am *alertmanager) CreateSilence(_ context.Context, ps *alertingNotify.Post func (am *alertmanager) DeleteSilence(_ context.Context, silenceID string) error { return am.Base.DeleteSilence(silenceID) } + +func (am *alertmanager) MergeSilences(b []byte) error { + return am.Base.MergeSilences(b) +} diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index e042272a063..b7c2803f3c0 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -493,7 +493,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec receiverService := notifier.NewReceiverService( ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), true), - legacy_storage.NewAlertmanagerConfigStore(configStore), + legacy_storage.NewAlertmanagerConfigStore(configStore, notifier.NewExtraConfigsCrypto(secretService)), provisioningStore, &fakeAlertRuleNotificationStore{}, secretService, @@ -504,7 +504,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec ) return NewContactPointService( - legacy_storage.NewAlertmanagerConfigStore(configStore), + legacy_storage.NewAlertmanagerConfigStore(configStore, notifier.NewExtraConfigsCrypto(secretService)), secretService, provisioningStore, xact, diff --git a/pkg/services/ngalert/provisioning/errors.go b/pkg/services/ngalert/provisioning/errors.go index 3d8b4c85608..407f45efed6 100644 --- a/pkg/services/ngalert/provisioning/errors.go +++ b/pkg/services/ngalert/provisioning/errors.go @@ -37,6 +37,10 @@ var ( "Invalid format of the submitted route.", errutil.WithPublic("Invalid format of the submitted route: {{.Public.Error}}. Correct the payload and try again."), ) + + ErrRouteConflictingMatchers = errutil.BadRequest("alerting.notifications.routes.conflictingMatchers").MustTemplate("Routing tree conflicts with the external configuration", + errutil.WithPublic("Cannot add\\update route: matchers conflict with an external routing tree merging matchers {{ .Public.Matchers }}, making the added\\updated route unreachable."), + ) ) // MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template @@ -109,6 +113,14 @@ func MakeErrRouteInvalidFormat(err error) error { }) } +func MakeErrRouteConflictingMatchers(matchers string) error { + return ErrRouteConflictingMatchers.Build(errutil.TemplateData{ + Public: map[string]any{ + "Matchers": matchers, + }, + }) +} + func MakeErrContactPointUidExists(uid, name string) error { return ErrContactPointUidExists.Build(errutil.TemplateData{ Public: map[string]any{ diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index f6563fe45ac..220783b962c 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -3,12 +3,14 @@ package provisioning import ( "context" "encoding/binary" + "errors" "fmt" "hash" "hash/fnv" "slices" "unsafe" + "github.com/grafana/alerting/definition" "github.com/prometheus/common/model" "golang.org/x/exp/maps" @@ -113,7 +115,11 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI _, err = revision.Config.GetMergedAlertmanagerConfig() if err != nil { - return definitions.Route{}, "", fmt.Errorf("new routing tree is not compatible with extra configuration: %w", err) + if errors.Is(err, definition.ErrSubtreeMatchersConflict) { + // TODO temporarily get the conflicting matchers + return definitions.Route{}, "", MakeErrRouteConflictingMatchers(fmt.Sprintf("%s", revision.Config.ExtraConfigs[0].MergeMatchers)) + } + nps.log.Warn("Unable to validate the combined routing tree because of an error during merging. This could be a sign of broken external configuration. Skipping", "error", err) } err = nps.xact.InTransaction(ctx, func(ctx context.Context) error { diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index e52460c5c04..6a2804087bb 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -189,6 +189,93 @@ func TestUpdatePolicyTree(t *testing.T) { assert.Equal(t, orgID, prov.Calls[0].Arguments[2].(int64)) }) + t.Run("ErrRouteConflictingMatchers if the new route has conflicting matchers ", func(t *testing.T) { + rev := getDefaultConfigRevision() + rev.Config.ExtraConfigs = append(rev.Config.ExtraConfigs, definitions.ExtraConfiguration{ + Identifier: "test", + MergeMatchers: config.Matchers{ + { + Type: labels.MatchEqual, + Name: "imported", + Value: "true", + }, + }, + AlertmanagerConfig: `{"route":{"receiver":"mimir-receiver"},"receivers":[{"name":"mimir-receiver"}]}`, + }) + + route := definitions.Route{ + Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name, + Routes: []*definitions.Route{ + { + ObjectMatchers: definitions.ObjectMatchers{ + { + Type: labels.MatchEqual, + Name: "imported", + Value: "true", + }, + { + Type: labels.MatchEqual, + Name: "label", + Value: "value", + }, + }, + }, + }, + } + + sut, store, _ := createNotificationPolicyServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &rev, nil + } + + _, _, err := sut.UpdatePolicyTree(context.Background(), orgID, route, models.ProvenanceAPI, defaultVersion) + require.ErrorIs(t, err, ErrRouteConflictingMatchers) + }) + + t.Run("should ignore extra config validation if it is invalid", func(t *testing.T) { + extra := definitions.ExtraConfiguration{ + MergeMatchers: config.Matchers{ + { + Type: labels.MatchEqual, + Name: "imported", + Value: "true", + }, + }, + } + rev := getDefaultConfigRevision() + rev.Config.ExtraConfigs = append(rev.Config.ExtraConfigs, extra) + + route := definitions.Route{ + Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name, + Routes: []*definitions.Route{ + { + ObjectMatchers: definitions.ObjectMatchers{ + { + Type: labels.MatchEqual, + Name: "imported", + Value: "true", + }, + { + Type: labels.MatchEqual, + Name: "label", + Value: "value", + }, + }, + }, + }, + } + + sut, store, _ := createNotificationPolicyServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &rev, nil + } + + result, version, err := sut.UpdatePolicyTree(context.Background(), orgID, route, models.ProvenanceAPI, defaultVersion) + require.NoError(t, err) + assert.Equal(t, route, result) + assert.Equal(t, calculateRouteFingerprint(route), version) + }) + t.Run("updates Route and sets provenance in transaction if route is valid and version matches", func(t *testing.T) { sut, store, prov := createNotificationPolicyServiceSut() store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { diff --git a/pkg/services/ngalert/provisioning/provisioning_store_mock.go b/pkg/services/ngalert/provisioning/provisioning_store_mock.go index 65df0062a8a..31cc77e26a6 100644 --- a/pkg/services/ngalert/provisioning/provisioning_store_mock.go +++ b/pkg/services/ngalert/provisioning/provisioning_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package provisioning @@ -26,6 +26,10 @@ func (_m *MockProvisioningStore) EXPECT() *MockProvisioningStore_Expecter { func (_m *MockProvisioningStore) DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error { ret := _m.Called(ctx, o, org) + if len(ret) == 0 { + panic("no return value specified for DeleteProvenance") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64) error); ok { r0 = rf(ctx, o, org) @@ -70,6 +74,10 @@ func (_c *MockProvisioningStore_DeleteProvenance_Call) RunAndReturn(run func(con func (_m *MockProvisioningStore) GetProvenance(ctx context.Context, o models.Provisionable, org int64) (models.Provenance, error) { ret := _m.Called(ctx, o, org) + if len(ret) == 0 { + panic("no return value specified for GetProvenance") + } + var r0 models.Provenance var r1 error if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64) (models.Provenance, error)); ok { @@ -124,6 +132,10 @@ func (_c *MockProvisioningStore_GetProvenance_Call) RunAndReturn(run func(contex func (_m *MockProvisioningStore) GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) { ret := _m.Called(ctx, org, resourceType) + if len(ret) == 0 { + panic("no return value specified for GetProvenances") + } + var r0 map[string]models.Provenance var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (map[string]models.Provenance, error)); ok { @@ -180,6 +192,10 @@ func (_c *MockProvisioningStore_GetProvenances_Call) RunAndReturn(run func(conte func (_m *MockProvisioningStore) SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error { ret := _m.Called(ctx, o, org, p) + if len(ret) == 0 { + panic("no return value specified for SetProvenance") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, models.Provisionable, int64, models.Provenance) error); ok { r0 = rf(ctx, o, org, p) diff --git a/pkg/services/ngalert/provisioning/quota_checker_mock.go b/pkg/services/ngalert/provisioning/quota_checker_mock.go index 596e6c92d61..0639fccf984 100644 --- a/pkg/services/ngalert/provisioning/quota_checker_mock.go +++ b/pkg/services/ngalert/provisioning/quota_checker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package provisioning @@ -26,6 +26,10 @@ func (_m *MockQuotaChecker) EXPECT() *MockQuotaChecker_Expecter { func (_m *MockQuotaChecker) CheckQuotaReached(ctx context.Context, target quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error) { ret := _m.Called(ctx, target, scopeParams) + if len(ret) == 0 { + panic("no return value specified for CheckQuotaReached") + } + var r0 bool var r1 error if rf, ok := ret.Get(0).(func(context.Context, quota.TargetSrv, *quota.ScopeParameters) (bool, error)); ok { diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 56cf9d8ec26..444fe0d9625 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -293,18 +293,20 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config if err := am.mergeExtraConfigs(ctx, decryptedCfg); err != nil { return fmt.Errorf("unable to merge extra configurations: %w", err) } - rawDecrypted, err := json.Marshal(decryptedCfg) + payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg) + rawPayload, err := json.Marshal(payload) if err != nil { return fmt.Errorf("unable to marshal decrypted configuration: %w", err) } - configHash := md5.Sum(rawDecrypted) + + configHash := md5.Sum(rawPayload) // Send the configuration only if we need to. if !am.shouldSendConfig(ctx, configHash) { return nil } - return am.sendConfiguration(ctx, decryptedCfg, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash)) + return am.sendConfiguration(ctx, payload, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash)) } func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool { @@ -370,11 +372,11 @@ func (am *Alertmanager) mergeExtraConfigs(ctx context.Context, config *apimodels return nil } -func (am *Alertmanager) sendConfiguration(ctx context.Context, decrypted *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error { +func (am *Alertmanager) sendConfiguration(ctx context.Context, cfg *remoteClient.GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error { am.metrics.ConfigSyncsTotal.Inc() if err := am.mimirClient.CreateGrafanaAlertmanagerConfig( ctx, - decrypted, + cfg, hash, createdAt, isDefault, @@ -387,6 +389,48 @@ func (am *Alertmanager) sendConfiguration(ctx context.Context, decrypted *apimod return nil } +// RemoteState represents the state (silences, nflog) in use by a remote Alertmanager. +type RemoteState struct { + Silences []byte + Nflog []byte +} + +// GetRemoteState gets the remote Alertmanager's internal state. +func (am *Alertmanager) GetRemoteState(ctx context.Context) (RemoteState, error) { + var rs RemoteState + + s, err := am.mimirClient.GetFullState(ctx) + if err != nil { + return rs, fmt.Errorf("failed to pull remote state: %w", err) + } + + // Decode and unmarshal the base64-encoded state we got from Mimir. + decoded, err := base64.StdEncoding.DecodeString(s.State) + if err != nil { + return rs, fmt.Errorf("failed to base64-decode remote state: %w", err) + } + protoState := &alertingClusterPB.FullState{} + if err := protoState.Unmarshal(decoded); err != nil { + return rs, fmt.Errorf("failed to unmarshal remote state: %w", err) + } + + // Mimir state has two parts: + // - "sil:": silences + // - "nfl:": notification log entries + for _, p := range protoState.Parts { + switch p.Key { + case "sil:" + am.tenantID: + rs.Silences = p.Data + case "nfl:" + am.tenantID: + rs.Nflog = p.Data + default: + return rs, fmt.Errorf("unknown part key %q", p.Key) + } + } + + return rs, nil +} + // SendState gets the Alertmanager's internal state and sends it to the remote Alertmanager. func (am *Alertmanager) SendState(ctx context.Context) error { am.metrics.StateSyncsTotal.Inc() @@ -420,14 +464,14 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P if err := am.mergeExtraConfigs(ctx, decryptedCfg); err != nil { return fmt.Errorf("unable to merge extra configurations: %w", err) } - - rawCfg, err := json.Marshal(decryptedCfg) + payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg) + rawCfg, err := json.Marshal(payload) if err != nil { return err } hash := fmt.Sprintf("%x", md5.Sum(rawCfg)) - return am.sendConfiguration(ctx, decryptedCfg, hash, time.Now().Unix(), false) + return am.sendConfiguration(ctx, payload, hash, time.Now().Unix(), false) } // SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager. @@ -446,10 +490,17 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { return err } + payload := PostableUserConfigToGrafanaAlertmanagerConfig(decryptedCfg) + rawCfg, err := json.Marshal(payload) + if err != nil { + return err + } + hash := fmt.Sprintf("%x", md5.Sum(rawCfg)) + return am.sendConfiguration( ctx, - decryptedCfg, - am.defaultConfigHash, + payload, + hash, time.Now().Unix(), true, ) diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 3bbcb03de6b..5aad1a47a1a 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -3,6 +3,7 @@ package remote import ( "context" "crypto/md5" + "embed" "encoding/base64" "encoding/json" "errors" @@ -11,6 +12,7 @@ import ( "net/http" "net/http/httptest" "os" + "path" "slices" "strings" "testing" @@ -23,6 +25,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + alertingClusterPB "github.com/grafana/alerting/cluster/clusterpb" "github.com/grafana/alerting/definition" alertingModels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" @@ -47,6 +50,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) +//go:embed test-data/*.* +var testData embed.FS + var ( defaultGrafanaConfig = setting.GetAlertmanagerDefaultConfiguration() errTest = errors.New("test") @@ -131,6 +137,125 @@ func TestNewAlertmanager(t *testing.T) { } } +func TestGetRemoteState(t *testing.T) { + const tenantID = "test" + ctx := context.Background() + store := ngfakes.NewFakeKVStore(t) + fstore := notifier.NewFileStore(1, store) + secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t))) + tc := notifier.NewCrypto(secretsService, nil, log.NewNopLogger()) + m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) + + // getOkHandler allows us to specify a full state the test server is going to respond with. + getOkHandler := func(state string) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, tenantID, r.Header.Get(client.MimirTenantHeader)) + require.Equal(t, "true", r.Header.Get(client.RemoteAlertmanagerHeader)) + + res := map[string]any{ + "status": "success", + "data": map[string]any{ + "state": state, + }, + } + w.Header().Add("content-type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(res)) + }) + } + + // errorHandler makes the test server return a 500 status code and a non-JSON response. + errorHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("content-type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusInternalServerError) + }) + + // Test full states: + // - One with unknown part keys + // - One with the expected part keys + badState := alertingClusterPB.FullState{ + Parts: []alertingClusterPB.Part{ + {Key: "unknown", Data: []byte("data")}, + }, + } + rawBadState, err := badState.Marshal() + require.NoError(t, err) + + state := alertingClusterPB.FullState{ + Parts: []alertingClusterPB.Part{ + {Key: "nfl:test", Data: []byte("test-nflog")}, + {Key: "sil:test", Data: []byte("test-silences")}, + }, + } + rawState, err := state.Marshal() + require.NoError(t, err) + + tests := []struct { + name string + handler http.Handler + expNflog []byte + expSilences []byte + expErr string + }{ + { + name: "non base64-encoded state", + handler: getOkHandler("invalid state"), + expErr: "failed to base64-decode remote state: illegal base64 data at input byte 7", + }, + { + name: "error from the Mimir API", + handler: errorHandler, + expErr: "failed to pull remote state: Response content-type is not application/json: text/html; charset=utf-8", + }, + { + name: "invalid state, base64-encoded", + handler: getOkHandler(base64.StdEncoding.EncodeToString([]byte("invalid state"))), + expErr: "failed to unmarshal remote state: proto: FullState: wiretype end group for non-group", + }, + { + name: "unknown part key", + handler: getOkHandler(base64.StdEncoding.EncodeToString(rawBadState)), + expErr: "unknown part key \"unknown\"", + }, + { + name: "success", + handler: getOkHandler(base64.StdEncoding.EncodeToString(rawState)), + expNflog: []byte("test-nflog"), + expSilences: []byte("test-silences"), + }, + } + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + server := httptest.NewServer(test.handler) + cfg := AlertmanagerConfig{ + OrgID: 1, + TenantID: tenantID, + URL: server.URL, + DefaultConfig: defaultGrafanaConfig, + } + am, err := NewAlertmanager(ctx, + cfg, + fstore, + tc, + NoopAutogenFn, + m, + tracing.InitializeTracerForTest(), + ) + require.NoError(t, err) + + s, err := am.GetRemoteState(ctx) + if test.expErr != "" { + require.Error(t, err) + require.Equal(t, test.expErr, err.Error()) + return + } + + require.NoError(t, err) + require.Equal(t, test.expNflog, s.Nflog) + require.Equal(t, test.expSilences, s.Silences) + }) + } +} + func TestIntegrationApplyConfig(t *testing.T) { const tenantID = "test" // errorHandler returns an error response for the readiness check and state sync. @@ -357,12 +482,14 @@ func TestCompareAndSendConfiguration(t *testing.T) { testGrafanaConfigWithBadEncryption, err := json.Marshal(inputCfg) require.NoError(t, err) - cfgWithDecryptedSecret, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) + test, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) require.NoError(t, err) + cfgWithDecryptedSecret := PostableUserConfigToGrafanaAlertmanagerConfig(test) - cfgWithAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) + testAutogenRoutes, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) require.NoError(t, err) - require.NoError(t, testAutogenFn(nil, nil, 0, &cfgWithAutogenRoutes.AlertmanagerConfig, false)) + require.NoError(t, testAutogenFn(nil, nil, 0, &testAutogenRoutes.AlertmanagerConfig, false)) + cfgWithAutogenRoutes := PostableUserConfigToGrafanaAlertmanagerConfig(testAutogenRoutes) // Calculate hashes for expected configurations cfgWithDecryptedSecretBytes, err := json.Marshal(cfgWithDecryptedSecret) @@ -373,6 +500,20 @@ func TestCompareAndSendConfiguration(t *testing.T) { require.NoError(t, err) cfgWithAutogenRoutesHash := fmt.Sprintf("%x", md5.Sum(cfgWithAutogenRoutesBytes)) + cfgWithExtraUnmergedBytes, err := testData.ReadFile(path.Join("test-data", "config-with-extra.json")) + require.NoError(t, err) + cfgWithExtraUnmerged, err := notifier.Load(cfgWithExtraUnmergedBytes) + require.NoError(t, err) + r, err := cfgWithExtraUnmerged.GetMergedAlertmanagerConfig() + require.NoError(t, err) + cfgWithExtraMerged := &client.GrafanaAlertmanagerConfig{ + TemplateFiles: cfgWithExtraUnmerged.TemplateFiles, + AlertmanagerConfig: r.Config, + } + cfgWithExtraMergedBytes, err := json.Marshal(cfgWithExtraMerged) + require.NoError(t, err) + cfgWithExtraMergedHash := fmt.Sprintf("%x", md5.Sum(cfgWithExtraMergedBytes)) + tests := []struct { name string config string @@ -428,6 +569,15 @@ func TestCompareAndSendConfiguration(t *testing.T) { }, nil, }, + { + name: "no error, with extra configurations", + config: string(cfgWithExtraUnmergedBytes), + autogenFn: NoopAutogenFn, + expCfg: &client.UserGrafanaConfig{ + GrafanaAlertmanagerConfig: cfgWithExtraMerged, + Hash: cfgWithExtraMergedHash, + }, + }, } for _, test := range tests { @@ -677,7 +827,7 @@ func TestCompareAndSendConfigurationWithExtraConfigs(t *testing.T) { // Return an empty config to ensure it gets replaced w.Header().Add("content-type", "application/json") require.NoError(t, json.NewEncoder(w).Encode(client.UserGrafanaConfig{ - GrafanaAlertmanagerConfig: &apimodels.PostableUserConfig{}, + GrafanaAlertmanagerConfig: &client.GrafanaAlertmanagerConfig{}, })) return } diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index 164a3949011..35239700ea7 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/grafana/alerting/definition" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -15,14 +16,25 @@ const ( grafanaAlertmanagerReceiversPath = "/api/v1/grafana/receivers" ) +type GrafanaAlertmanagerConfig struct { + TemplateFiles map[string]string `yaml:"template_files" json:"template_files"` + AlertmanagerConfig definition.PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"` +} + +func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) { + // this is special marshaling that makes sure that secrets are not masked + type cfg GrafanaAlertmanagerConfig + return definition.MarshalJSONWithSecrets((*cfg)(u)) +} + type UserGrafanaConfig struct { - GrafanaAlertmanagerConfig *apimodels.PostableUserConfig `json:"configuration"` - Hash string `json:"configuration_hash"` - CreatedAt int64 `json:"created"` - Default bool `json:"default"` - Promoted bool `json:"promoted"` - ExternalURL string `json:"external_url"` - SmtpConfig SmtpConfig `json:"smtp_config"` + GrafanaAlertmanagerConfig *GrafanaAlertmanagerConfig `json:"configuration"` + Hash string `json:"configuration_hash"` + CreatedAt int64 `json:"created"` + Default bool `json:"default"` + Promoted bool `json:"promoted"` + ExternalURL string `json:"external_url"` + SmtpConfig SmtpConfig `json:"smtp_config"` // TODO: Remove once everything can be sent in the 'SmtpConfig' field. SmtpFrom string `json:"smtp_from"` @@ -52,7 +64,7 @@ func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafana return gc, nil } -func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error { +func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error { payload, err := definition.MarshalJSONWithSecrets(&UserGrafanaConfig{ GrafanaAlertmanagerConfig: cfg, Hash: hash, diff --git a/pkg/services/ngalert/remote/client/alertmanager_state.go b/pkg/services/ngalert/remote/client/alertmanager_state.go index cac59c01192..0c7048138b1 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_state.go +++ b/pkg/services/ngalert/remote/client/alertmanager_state.go @@ -9,15 +9,25 @@ import ( ) const ( + fullStatePath = "/api/v1/grafana/full_state" grafanaAlertmanagerStatePath = "/api/v1/grafana/state" ) -type UserGrafanaState struct { +// UserState is used for both Grafana state and Mimir full state. +type UserState struct { State string `json:"state"` } -func (mc *Mimir) GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaState, error) { - gs := &UserGrafanaState{} +func (mc *Mimir) GetFullState(ctx context.Context) (*UserState, error) { + return mc.getState(ctx, fullStatePath) +} + +func (mc *Mimir) GetGrafanaAlertmanagerState(ctx context.Context) (*UserState, error) { + return mc.getState(ctx, grafanaAlertmanagerStatePath) +} + +func (mc *Mimir) getState(ctx context.Context, path string) (*UserState, error) { + gs := &UserState{} response := successResponse{ Data: gs, } @@ -36,7 +46,7 @@ func (mc *Mimir) GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaS } func (mc *Mimir) CreateGrafanaAlertmanagerState(ctx context.Context, state string) error { - payload, err := json.Marshal(&UserGrafanaState{ + payload, err := json.Marshal(&UserState{ State: state, }) if err != nil { diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index 931e2f7aa3d..3bf10465076 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -24,12 +24,13 @@ import ( // MimirClient contains all the methods to query the migration critical endpoints of Mimir instance, it's an interface to allow multiple implementations. type MimirClient interface { - GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaState, error) + GetFullState(ctx context.Context) (*UserState, error) + GetGrafanaAlertmanagerState(ctx context.Context) (*UserState, error) CreateGrafanaAlertmanagerState(ctx context.Context, state string) error DeleteGrafanaAlertmanagerState(ctx context.Context) error GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) - CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error + CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *GrafanaAlertmanagerConfig, hash string, createdAt int64, isDefault bool) error DeleteGrafanaAlertmanagerConfig(ctx context.Context) error TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error) diff --git a/pkg/services/ngalert/remote/compat.go b/pkg/services/ngalert/remote/compat.go new file mode 100644 index 00000000000..8914bc96728 --- /dev/null +++ b/pkg/services/ngalert/remote/compat.go @@ -0,0 +1,13 @@ +package remote + +import ( + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/remote/client" +) + +func PostableUserConfigToGrafanaAlertmanagerConfig(config *definitions.PostableUserConfig) *client.GrafanaAlertmanagerConfig { + return &client.GrafanaAlertmanagerConfig{ + TemplateFiles: config.TemplateFiles, + AlertmanagerConfig: config.AlertmanagerConfig, + } +} diff --git a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go index 09cfb04bc67..33a77c10f1c 100644 --- a/pkg/services/ngalert/remote/mock/remoteAlertmanager.go +++ b/pkg/services/ngalert/remote/mock/remoteAlertmanager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.44.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package alertmanager_mock @@ -125,52 +125,6 @@ func (_c *RemoteAlertmanagerMock_CompareAndSendConfiguration_Call) RunAndReturn( return _c } -// SendState provides a mock function with given fields: _a0 -func (_m *RemoteAlertmanagerMock) SendState(_a0 context.Context) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for SendState") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// RemoteAlertmanagerMock_SendState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendState' -type RemoteAlertmanagerMock_SendState_Call struct { - *mock.Call -} - -// SendState is a helper method to define mock.On call -// - _a0 context.Context -func (_e *RemoteAlertmanagerMock_Expecter) SendState(_a0 interface{}) *RemoteAlertmanagerMock_SendState_Call { - return &RemoteAlertmanagerMock_SendState_Call{Call: _e.mock.On("SendState", _a0)} -} - -func (_c *RemoteAlertmanagerMock_SendState_Call) Run(run func(_a0 context.Context)) *RemoteAlertmanagerMock_SendState_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *RemoteAlertmanagerMock_SendState_Call) Return(_a0 error) *RemoteAlertmanagerMock_SendState_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *RemoteAlertmanagerMock_SendState_Call) RunAndReturn(run func(context.Context) error) *RemoteAlertmanagerMock_SendState_Call { - _c.Call.Return(run) - return _c -} - // CreateSilence provides a mock function with given fields: _a0, _a1 func (_m *RemoteAlertmanagerMock) CreateSilence(_a0 context.Context, _a1 *v2models.PostableSilence) (string, error) { ret := _m.Called(_a0, _a1) @@ -678,7 +632,7 @@ func (_c *RemoteAlertmanagerMock_PutAlerts_Call) RunAndReturn(run func(context.C return _c } -// Ready provides a mock function with given fields: +// Ready provides a mock function with no fields func (_m *RemoteAlertmanagerMock) Ready() bool { ret := _m.Called() @@ -816,6 +770,52 @@ func (_c *RemoteAlertmanagerMock_SaveAndApplyDefaultConfig_Call) RunAndReturn(ru return _c } +// SendState provides a mock function with given fields: _a0 +func (_m *RemoteAlertmanagerMock) SendState(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for SendState") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoteAlertmanagerMock_SendState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendState' +type RemoteAlertmanagerMock_SendState_Call struct { + *mock.Call +} + +// SendState is a helper method to define mock.On call +// - _a0 context.Context +func (_e *RemoteAlertmanagerMock_Expecter) SendState(_a0 interface{}) *RemoteAlertmanagerMock_SendState_Call { + return &RemoteAlertmanagerMock_SendState_Call{Call: _e.mock.On("SendState", _a0)} +} + +func (_c *RemoteAlertmanagerMock_SendState_Call) Run(run func(_a0 context.Context)) *RemoteAlertmanagerMock_SendState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *RemoteAlertmanagerMock_SendState_Call) Return(_a0 error) *RemoteAlertmanagerMock_SendState_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RemoteAlertmanagerMock_SendState_Call) RunAndReturn(run func(context.Context) error) *RemoteAlertmanagerMock_SendState_Call { + _c.Call.Return(run) + return _c +} + // SilenceState provides a mock function with given fields: _a0 func (_m *RemoteAlertmanagerMock) SilenceState(_a0 context.Context) (notify.SilenceState, error) { ret := _m.Called(_a0) @@ -874,7 +874,7 @@ func (_c *RemoteAlertmanagerMock_SilenceState_Call) RunAndReturn(run func(contex return _c } -// StopAndWait provides a mock function with given fields: +// StopAndWait provides a mock function with no fields func (_m *RemoteAlertmanagerMock) StopAndWait() { _m.Called() } @@ -902,7 +902,7 @@ func (_c *RemoteAlertmanagerMock_StopAndWait_Call) Return() *RemoteAlertmanagerM } func (_c *RemoteAlertmanagerMock_StopAndWait_Call) RunAndReturn(run func()) *RemoteAlertmanagerMock_StopAndWait_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/pkg/services/ngalert/remote/test-data/config-with-extra.json b/pkg/services/ngalert/remote/test-data/config-with-extra.json new file mode 100644 index 00000000000..4f15ddfc590 --- /dev/null +++ b/pkg/services/ngalert/remote/test-data/config-with-extra.json @@ -0,0 +1,41 @@ +{ + "template_files": { + "test": "{{ define \"my_templ\" }}TEST{{ end }}" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email", + "group_by": [ + "grafana_folder", + "alertname" + ] + }, + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "", + "name": "some other name", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "" + } + } + ] + } + ] + }, + "extra_config": [ + { + "identifier": "imported", + "merge_matchers": ["imported=\"true\""], + "template_files": + { + "extra_template": "{{ define \"my_message\" }}TEST{{ end }}" + }, + "alertmanager_config": "{\"receivers\":[{\"webhook_configs\":[{\"url\":\"http://localhost\"}],\"name\":\"webhook\"}],\"route\":{\"receiver\":\"webhook\"}}" + } + ] +} \ No newline at end of file diff --git a/pkg/services/ngalert/schedule/alerts_sender_mock.go b/pkg/services/ngalert/schedule/alerts_sender_mock.go index 75a9e2ecc5e..6ba9367193e 100644 --- a/pkg/services/ngalert/schedule/alerts_sender_mock.go +++ b/pkg/services/ngalert/schedule/alerts_sender_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.34.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package schedule @@ -55,7 +55,7 @@ func (_c *AlertsSenderMock_Send_Call) Return() *AlertsSenderMock_Send_Call { } func (_c *AlertsSenderMock_Send_Call) RunAndReturn(run func(context.Context, models.AlertRuleKey, definitions.PostableAlerts)) *AlertsSenderMock_Send_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/pkg/services/ngalert/store/admin_configuration_store_mock.go b/pkg/services/ngalert/store/admin_configuration_store_mock.go index 4dfc88ff5cc..080a185ef5b 100644 --- a/pkg/services/ngalert/store/admin_configuration_store_mock.go +++ b/pkg/services/ngalert/store/admin_configuration_store_mock.go @@ -1,11 +1,10 @@ -// Code generated by mockery v2.10.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package store import ( - mock "github.com/stretchr/testify/mock" - models "github.com/grafana/grafana/pkg/services/ngalert/models" + mock "github.com/stretchr/testify/mock" ) // AdminConfigurationStoreMock is an autogenerated mock type for the AdminConfigurationStore type @@ -25,6 +24,10 @@ func (_m *AdminConfigurationStoreMock) EXPECT() *AdminConfigurationStoreMock_Exp func (_m *AdminConfigurationStoreMock) DeleteAdminConfiguration(orgID int64) error { ret := _m.Called(orgID) + if len(ret) == 0 { + panic("no return value specified for DeleteAdminConfiguration") + } + var r0 error if rf, ok := ret.Get(0).(func(int64) error); ok { r0 = rf(orgID) @@ -42,7 +45,7 @@ type AdminConfigurationStoreMock_DeleteAdminConfiguration_Call struct { // DeleteAdminConfiguration is a helper method to define mock.On call // - orgID int64 -func (_e *AdminConfigurationStoreMock_Expecter) DeleteAdminConfiguration(orgID any) *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call { +func (_e *AdminConfigurationStoreMock_Expecter) DeleteAdminConfiguration(orgID interface{}) *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call { return &AdminConfigurationStoreMock_DeleteAdminConfiguration_Call{Call: _e.mock.On("DeleteAdminConfiguration", orgID)} } @@ -58,11 +61,24 @@ func (_c *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call) Return(_a0 return _c } +func (_c *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call) RunAndReturn(run func(int64) error) *AdminConfigurationStoreMock_DeleteAdminConfiguration_Call { + _c.Call.Return(run) + return _c +} + // GetAdminConfiguration provides a mock function with given fields: orgID func (_m *AdminConfigurationStoreMock) GetAdminConfiguration(orgID int64) (*models.AdminConfiguration, error) { ret := _m.Called(orgID) + if len(ret) == 0 { + panic("no return value specified for GetAdminConfiguration") + } + var r0 *models.AdminConfiguration + var r1 error + if rf, ok := ret.Get(0).(func(int64) (*models.AdminConfiguration, error)); ok { + return rf(orgID) + } if rf, ok := ret.Get(0).(func(int64) *models.AdminConfiguration); ok { r0 = rf(orgID) } else { @@ -71,7 +87,6 @@ func (_m *AdminConfigurationStoreMock) GetAdminConfiguration(orgID int64) (*mode } } - var r1 error if rf, ok := ret.Get(1).(func(int64) error); ok { r1 = rf(orgID) } else { @@ -88,7 +103,7 @@ type AdminConfigurationStoreMock_GetAdminConfiguration_Call struct { // GetAdminConfiguration is a helper method to define mock.On call // - orgID int64 -func (_e *AdminConfigurationStoreMock_Expecter) GetAdminConfiguration(orgID any) *AdminConfigurationStoreMock_GetAdminConfiguration_Call { +func (_e *AdminConfigurationStoreMock_Expecter) GetAdminConfiguration(orgID interface{}) *AdminConfigurationStoreMock_GetAdminConfiguration_Call { return &AdminConfigurationStoreMock_GetAdminConfiguration_Call{Call: _e.mock.On("GetAdminConfiguration", orgID)} } @@ -104,11 +119,24 @@ func (_c *AdminConfigurationStoreMock_GetAdminConfiguration_Call) Return(_a0 *mo return _c } -// GetAdminConfigurations provides a mock function with given fields: +func (_c *AdminConfigurationStoreMock_GetAdminConfiguration_Call) RunAndReturn(run func(int64) (*models.AdminConfiguration, error)) *AdminConfigurationStoreMock_GetAdminConfiguration_Call { + _c.Call.Return(run) + return _c +} + +// GetAdminConfigurations provides a mock function with no fields func (_m *AdminConfigurationStoreMock) GetAdminConfigurations() ([]*models.AdminConfiguration, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for GetAdminConfigurations") + } + var r0 []*models.AdminConfiguration + var r1 error + if rf, ok := ret.Get(0).(func() ([]*models.AdminConfiguration, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() []*models.AdminConfiguration); ok { r0 = rf() } else { @@ -117,7 +145,6 @@ func (_m *AdminConfigurationStoreMock) GetAdminConfigurations() ([]*models.Admin } } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -149,10 +176,19 @@ func (_c *AdminConfigurationStoreMock_GetAdminConfigurations_Call) Return(_a0 [] return _c } +func (_c *AdminConfigurationStoreMock_GetAdminConfigurations_Call) RunAndReturn(run func() ([]*models.AdminConfiguration, error)) *AdminConfigurationStoreMock_GetAdminConfigurations_Call { + _c.Call.Return(run) + return _c +} + // UpdateAdminConfiguration provides a mock function with given fields: _a0 func (_m *AdminConfigurationStoreMock) UpdateAdminConfiguration(_a0 UpdateAdminConfigurationCmd) error { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for UpdateAdminConfiguration") + } + var r0 error if rf, ok := ret.Get(0).(func(UpdateAdminConfigurationCmd) error); ok { r0 = rf(_a0) @@ -170,7 +206,7 @@ type AdminConfigurationStoreMock_UpdateAdminConfiguration_Call struct { // UpdateAdminConfiguration is a helper method to define mock.On call // - _a0 UpdateAdminConfigurationCmd -func (_e *AdminConfigurationStoreMock_Expecter) UpdateAdminConfiguration(_a0 any) *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call { +func (_e *AdminConfigurationStoreMock_Expecter) UpdateAdminConfiguration(_a0 interface{}) *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call { return &AdminConfigurationStoreMock_UpdateAdminConfiguration_Call{Call: _e.mock.On("UpdateAdminConfiguration", _a0)} } @@ -185,3 +221,22 @@ func (_c *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call) Return(_a0 _c.Call.Return(_a0) return _c } + +func (_c *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call) RunAndReturn(run func(UpdateAdminConfigurationCmd) error) *AdminConfigurationStoreMock_UpdateAdminConfiguration_Call { + _c.Call.Return(run) + return _c +} + +// NewAdminConfigurationStoreMock creates a new instance of AdminConfigurationStoreMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAdminConfigurationStoreMock(t interface { + mock.TestingT + Cleanup(func()) +}) *AdminConfigurationStoreMock { + mock := &AdminConfigurationStoreMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/oauthtoken/oauthtokentest/service_mock.go b/pkg/services/oauthtoken/oauthtokentest/service_mock.go index 925432a6190..e1643e0e848 100644 --- a/pkg/services/oauthtoken/oauthtokentest/service_mock.go +++ b/pkg/services/oauthtoken/oauthtokentest/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package oauthtokentest diff --git a/pkg/services/org/orgtest/mock.go b/pkg/services/org/orgtest/mock.go index 7dc2b16d34d..ea598a2be0c 100644 --- a/pkg/services/org/orgtest/mock.go +++ b/pkg/services/org/orgtest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package orgtest @@ -62,24 +62,6 @@ func (_m *MockService) CreateWithMember(_a0 context.Context, _a1 *org.CreateOrgC return r0, r1 } -// Delete provides a mock function with given fields: _a0, _a1 -func (_m *MockService) Delete(_a0 context.Context, _a1 *org.DeleteOrgCommand) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *org.DeleteOrgCommand) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // DeleteUserFromAll provides a mock function with given fields: _a0, _a1 func (_m *MockService) DeleteUserFromAll(_a0 context.Context, _a1 int64) error { ret := _m.Called(_a0, _a1) diff --git a/pkg/services/pluginsintegration/advisor/advisor.go b/pkg/services/pluginsintegration/advisor/advisor.go index c9782de28bf..98989679aa8 100644 --- a/pkg/services/pluginsintegration/advisor/advisor.go +++ b/pkg/services/pluginsintegration/advisor/advisor.go @@ -86,7 +86,7 @@ func (s *Service) ReportSummary(ctx context.Context) (*ReportInfo, error) { latestDatasourceCheck := findLatestCheck(checkList.GetItems(), datasourcecheck.CheckID) reportInfo := &ReportInfo{} if latestPluginCheck != nil { - for _, failure := range latestPluginCheck.CheckStatus.Report.Failures { + for _, failure := range latestPluginCheck.Status.Report.Failures { switch failure.StepID { case plugincheck.UpdateStepID: reportInfo.PluginsOutdated++ @@ -96,7 +96,7 @@ func (s *Service) ReportSummary(ctx context.Context) (*ReportInfo, error) { } } if latestDatasourceCheck != nil { - for _, failure := range latestDatasourceCheck.CheckStatus.Report.Failures { + for _, failure := range latestDatasourceCheck.Status.Report.Failures { if failure.StepID == datasourcecheck.HealthCheckStepID { reportInfo.DatasourcesUnhealthy++ } diff --git a/pkg/services/pluginsintegration/advisor/advisor_test.go b/pkg/services/pluginsintegration/advisor/advisor_test.go index c9c9b8d1609..5fce5f23f35 100644 --- a/pkg/services/pluginsintegration/advisor/advisor_test.go +++ b/pkg/services/pluginsintegration/advisor/advisor_test.go @@ -41,8 +41,8 @@ func TestService_ReportSummary(t *testing.T) { checks.TypeLabel: plugincheck.CheckID, }, }, - CheckStatus: advisorv0alpha1.CheckStatus{ - Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Status: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckReport{ Failures: []advisorv0alpha1.CheckReportFailure{ {StepID: plugincheck.UpdateStepID}, }, @@ -56,8 +56,8 @@ func TestService_ReportSummary(t *testing.T) { checks.TypeLabel: plugincheck.CheckID, }, }, - CheckStatus: advisorv0alpha1.CheckStatus{ - Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Status: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckReport{ Failures: []advisorv0alpha1.CheckReportFailure{ {StepID: plugincheck.UpdateStepID}, {StepID: plugincheck.DeprecationStepID}, @@ -72,8 +72,8 @@ func TestService_ReportSummary(t *testing.T) { checks.TypeLabel: datasourcecheck.CheckID, }, }, - CheckStatus: advisorv0alpha1.CheckStatus{ - Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Status: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckReport{ Failures: []advisorv0alpha1.CheckReportFailure{ {StepID: datasourcecheck.HealthCheckStepID}, {StepID: datasourcecheck.HealthCheckStepID}, diff --git a/pkg/services/pluginsintegration/loader/loader_test.go b/pkg/services/pluginsintegration/loader/loader_test.go index bca3a42339b..727eb8d2952 100644 --- a/pkg/services/pluginsintegration/loader/loader_test.go +++ b/pkg/services/pluginsintegration/loader/loader_test.go @@ -83,8 +83,8 @@ func TestLoader_Load(t *testing.T) { Description: "Data source for Amazon AWS monitoring service", Keywords: []string{"aws", "amazon"}, Logos: plugins.Logos{ - Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", + Small: "public/plugins/cloudwatch/img/amazon-web-services.png", + Large: "public/plugins/cloudwatch/img/amazon-web-services.png", }, Links: []plugins.InfoLink{ {Name: "Raise issue", URL: "https://github.com/grafana/grafana/issues/new"}, @@ -121,7 +121,7 @@ func TestLoader_Load(t *testing.T) { QueryOptions: map[string]bool{"minInterval": true}, }, Module: "core:plugin/cloudwatch", - BaseURL: "public/app/plugins/datasource/cloudwatch", + BaseURL: "public/plugins/cloudwatch", FS: mustNewStaticFSForTests(t, filepath.Join(corePluginDir(t), "app/plugins/datasource/cloudwatch")), Signature: plugins.SignatureStatusInternal, Class: plugins.ClassCore, @@ -1325,8 +1325,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/test-datasource/nested/module.js", - BaseURL: "public/plugins/test-datasource/nested", + Module: "public/plugins/test-panel/module.js", + BaseURL: "public/plugins/test-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")), Signature: plugins.SignatureStatusValid, SignatureType: plugins.SignatureTypeGrafana, @@ -1497,8 +1497,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { {Name: "License", URL: "https://github.com/grafana/grafana-starter-panel/blob/master/LICENSE"}, }, Logos: plugins.Logos{ - Small: "public/plugins/myorgid-simple-app/child/img/logo.svg", - Large: "public/plugins/myorgid-simple-app/child/img/logo.svg", + Small: "public/plugins/myorgid-simple-panel/img/logo.svg", + Large: "public/plugins/myorgid-simple-panel/img/logo.svg", }, Screenshots: []plugins.Screenshots{}, Description: "Grafana Panel Plugin Template", @@ -1523,8 +1523,8 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { ExtensionPoints: []plugins.ExtensionPoint{}, }, }, - Module: "public/plugins/myorgid-simple-app/child/module.js", - BaseURL: "public/plugins/myorgid-simple-app/child", + Module: "public/plugins/myorgid-simple-panel/module.js", + BaseURL: "public/plugins/myorgid-simple-panel", FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")), IncludedInAppID: parent.ID, Signature: plugins.SignatureStatusValid, diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 2ffba23f4b5..1a564788287 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/auth" + "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/envvars" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" @@ -41,9 +42,16 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service) } func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap { + disableAlertingForTempoDecorateFunc := func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.ID == coreplugin.Tempo && !cfg.Features.TempoAlertingEnabled { + p.Alerting = false + } + return p, nil + } + return bootstrap.New(cfg, bootstrap.Opts{ ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, a), - DecorateFuncs: bootstrap.DefaultDecorateFuncs(cfg), + DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg), disableAlertingForTempoDecorateFunc), }) } diff --git a/pkg/services/pluginsintegration/pluginconfig/config.go b/pkg/services/pluginsintegration/pluginconfig/config.go index 9fd70c38d1a..1c8b2325a6d 100644 --- a/pkg/services/pluginsintegration/pluginconfig/config.go +++ b/pkg/services/pluginsintegration/pluginconfig/config.go @@ -35,6 +35,7 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks), PluginsCDNSyncLoaderEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsCDNSyncLoader), LocalizationForPlugins: features.IsEnabledGlobally(featuremgmt.FlagLocalizationForPlugins), + TempoAlertingEnabled: features.IsEnabledGlobally(featuremgmt.FlagTempoAlerting), }, cfg.GrafanaComAPIURL, cfg.DisablePlugins, diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 8edca88d855..6972eb22213 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -294,7 +294,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error ps.alertingStore, ps.alertingStore, ps.folderService, - //ps.dashboardService, + // ps.dashboardService, ps.quotaService, ps.SQLStore, int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), @@ -304,7 +304,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error notifier.NewCachedNotificationSettingsValidationService(ps.alertingStore), alertingauthz.NewRuleService(ps.ac), ) - configStore := legacy_storage.NewAlertmanagerConfigStore(ps.alertingStore) + configStore := legacy_storage.NewAlertmanagerConfigStore(ps.alertingStore, notifier.NewExtraConfigsCrypto(ps.secretService)) receiverSvc := notifier.NewReceiverService( alertingauthz.NewReceiverAccess[*ngmodels.Receiver](ps.ac, true), configStore, diff --git a/pkg/services/publicdashboards/public_dashboard_middleware_mock.go b/pkg/services/publicdashboards/public_dashboard_middleware_mock.go index f47e99dc0e2..79ebc20e6ce 100644 --- a/pkg/services/publicdashboards/public_dashboard_middleware_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_middleware_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.36.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 8e07012e294..b54ef3ec6db 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards diff --git a/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go b/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go index 646a6a033e2..e4c828a4faf 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_wrapper_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index f485ed445e6..32902d98df2 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package publicdashboards @@ -264,36 +264,6 @@ func (_m *FakePublicDashboardStore) FindByDashboardUid(ctx context.Context, orgI return r0, r1 } -// FindByFolder provides a mock function with given fields: ctx, orgId, folderUid -func (_m *FakePublicDashboardStore) FindByFolder(ctx context.Context, orgId int64, folderUid string) ([]*models.PublicDashboard, error) { - ret := _m.Called(ctx, orgId, folderUid) - - if len(ret) == 0 { - panic("no return value specified for FindByFolder") - } - - var r0 []*models.PublicDashboard - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int64, string) ([]*models.PublicDashboard, error)); ok { - return rf(ctx, orgId, folderUid) - } - if rf, ok := ret.Get(0).(func(context.Context, int64, string) []*models.PublicDashboard); ok { - r0 = rf(ctx, orgId, folderUid) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*models.PublicDashboard) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { - r1 = rf(ctx, orgId, folderUid) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetMetrics provides a mock function with given fields: ctx func (_m *FakePublicDashboardStore) GetMetrics(ctx context.Context) (*models.Metrics, error) { ret := _m.Called(ctx) diff --git a/pkg/services/query/query_service_mock.go b/pkg/services/query/query_service_mock.go index 5c5b4b69d37..800c822290d 100644 --- a/pkg/services/query/query_service_mock.go +++ b/pkg/services/query/query_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package query @@ -9,9 +9,9 @@ import ( dtos "github.com/grafana/grafana/pkg/api/dtos" - mock "github.com/stretchr/testify/mock" - identity "github.com/grafana/grafana/pkg/apimachinery/identity" + + mock "github.com/stretchr/testify/mock" ) // FakeQueryService is an autogenerated mock type for the Service type @@ -19,22 +19,29 @@ type FakeQueryService struct { mock.Mock } -// QueryData provides a mock function with given fields: ctx, _a1, skipDSCache, reqDTO -func (_m *FakeQueryService) QueryData(ctx context.Context, _a1 identity.Requester, skipDSCache bool, reqDTO dtos.MetricRequest) (*backend.QueryDataResponse, error) { - ret := _m.Called(ctx, _a1, skipDSCache, reqDTO) +// QueryData provides a mock function with given fields: ctx, user, skipDSCache, reqDTO +func (_m *FakeQueryService) QueryData(ctx context.Context, user identity.Requester, skipDSCache bool, reqDTO dtos.MetricRequest) (*backend.QueryDataResponse, error) { + ret := _m.Called(ctx, user, skipDSCache, reqDTO) + + if len(ret) == 0 { + panic("no return value specified for QueryData") + } var r0 *backend.QueryDataResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, bool, dtos.MetricRequest) (*backend.QueryDataResponse, error)); ok { + return rf(ctx, user, skipDSCache, reqDTO) + } if rf, ok := ret.Get(0).(func(context.Context, identity.Requester, bool, dtos.MetricRequest) *backend.QueryDataResponse); ok { - r0 = rf(ctx, _a1, skipDSCache, reqDTO) + r0 = rf(ctx, user, skipDSCache, reqDTO) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*backend.QueryDataResponse) } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, identity.Requester, bool, dtos.MetricRequest) error); ok { - r1 = rf(ctx, _a1, skipDSCache, reqDTO) + r1 = rf(ctx, user, skipDSCache, reqDTO) } else { r1 = ret.Error(1) } @@ -46,6 +53,10 @@ func (_m *FakeQueryService) QueryData(ctx context.Context, _a1 identity.Requeste func (_m *FakeQueryService) Run(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Run") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -56,13 +67,12 @@ func (_m *FakeQueryService) Run(ctx context.Context) error { return r0 } -type mockConstructorTestingTNewFakeQueryService interface { +// NewFakeQueryService creates a new instance of FakeQueryService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFakeQueryService(t interface { mock.TestingT Cleanup(func()) -} - -// NewFakeQueryService creates a new instance of FakeQueryService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakeQueryService(t mockConstructorTestingTNewFakeQueryService) *FakeQueryService { +}) *FakeQueryService { mock := &FakeQueryService{} mock.Mock.Test(t) diff --git a/pkg/services/scimutil/scim_util.go b/pkg/services/scimutil/scim_util.go index 022277ba994..92d0ac74270 100644 --- a/pkg/services/scimutil/scim_util.go +++ b/pkg/services/scimutil/scim_util.go @@ -83,11 +83,7 @@ func (s *SCIMUtil) fetchDynamicSCIMSetting(ctx context.Context, orgID int64, set case "group": enabled = scimConfig.EnableGroupSync case "allowNonProvisionedUsers": - if scimConfig.AllowNonProvisionedUsers != nil { - enabled = *scimConfig.AllowNonProvisionedUsers - } else { - enabled = false - } + enabled = scimConfig.AllowNonProvisionedUsers default: s.logger.Error("Invalid setting type provided to fetchDynamicSCIMSetting", "settingType", settingType) return false, false @@ -112,9 +108,9 @@ func (s *SCIMUtil) getOrgSCIMConfig(ctx context.Context, orgID int64) (*SCIMConf // SCIMConfigSpec represents the spec part of a SCIMConfig resource type SCIMConfigSpec struct { - EnableUserSync bool `json:"enableUserSync"` - EnableGroupSync bool `json:"enableGroupSync"` - AllowNonProvisionedUsers *bool `json:"allowNonProvisionedUsers,omitempty"` + EnableUserSync bool `json:"enableUserSync"` + EnableGroupSync bool `json:"enableGroupSync"` + AllowNonProvisionedUsers bool `json:"allowNonProvisionedUsers"` } // unstructuredToSCIMConfig converts an unstructured object to a SCIMConfigSpec @@ -139,6 +135,6 @@ func (s *SCIMUtil) unstructuredToSCIMConfig(obj *unstructured.Unstructured) (*SC return &SCIMConfigSpec{ EnableUserSync: enableUserSync, EnableGroupSync: enableGroupSync, - AllowNonProvisionedUsers: &allowNonProvisionedUsers, + AllowNonProvisionedUsers: allowNonProvisionedUsers, }, nil } diff --git a/pkg/services/scimutil/scim_util_test.go b/pkg/services/scimutil/scim_util_test.go index 2e371d32fd8..0e9d3b39ee5 100644 --- a/pkg/services/scimutil/scim_util_test.go +++ b/pkg/services/scimutil/scim_util_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "github.com/grafana/grafana/pkg/util" ) // MockK8sHandler is a mock implementation of client.K8sHandler for testing @@ -573,7 +572,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: true, EnableGroupSync: true, - AllowNonProvisionedUsers: util.Pointer(false), + AllowNonProvisionedUsers: false, }, }, { @@ -582,7 +581,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: false, EnableGroupSync: false, - AllowNonProvisionedUsers: util.Pointer(false), + AllowNonProvisionedUsers: false, }, }, { @@ -591,7 +590,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: true, EnableGroupSync: false, - AllowNonProvisionedUsers: util.Pointer(false), + AllowNonProvisionedUsers: false, }, }, { @@ -600,7 +599,7 @@ func TestSCIMUtil_unstructuredToSCIMConfig(t *testing.T) { expectedSpec: SCIMConfigSpec{ EnableUserSync: false, EnableGroupSync: false, - AllowNonProvisionedUsers: util.Pointer(true), + AllowNonProvisionedUsers: true, }, }, { diff --git a/pkg/services/searchV2/search_service_mock.go b/pkg/services/searchV2/search_service_mock.go index 63f7214c067..920b0621291 100644 --- a/pkg/services/searchV2/search_service_mock.go +++ b/pkg/services/searchV2/search_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.10.6. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package searchV2 @@ -6,6 +6,7 @@ import ( context "context" backend "github.com/grafana/grafana-plugin-sdk-go/backend" + mock "github.com/stretchr/testify/mock" user "github.com/grafana/grafana/pkg/services/user" @@ -20,6 +21,10 @@ type MockSearchService struct { func (_m *MockSearchService) DoDashboardQuery(ctx context.Context, _a1 *backend.User, orgId int64, query DashboardQuery) *backend.DataResponse { ret := _m.Called(ctx, _a1, orgId, query) + if len(ret) == 0 { + panic("no return value specified for DoDashboardQuery") + } + var r0 *backend.DataResponse if rf, ok := ret.Get(0).(func(context.Context, *backend.User, int64, DashboardQuery) *backend.DataResponse); ok { r0 = rf(ctx, _a1, orgId, query) @@ -32,10 +37,14 @@ func (_m *MockSearchService) DoDashboardQuery(ctx context.Context, _a1 *backend. return r0 } -// IsDisabled provides a mock function with given fields: +// IsDisabled provides a mock function with no fields func (_m *MockSearchService) IsDisabled() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for IsDisabled") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -50,6 +59,10 @@ func (_m *MockSearchService) IsDisabled() bool { func (_m *MockSearchService) IsReady(ctx context.Context, orgId int64) IsSearchReadyResponse { ret := _m.Called(ctx, orgId) + if len(ret) == 0 { + panic("no return value specified for IsReady") + } + var r0 IsSearchReadyResponse if rf, ok := ret.Get(0).(func(context.Context, int64) IsSearchReadyResponse); ok { r0 = rf(ctx, orgId) @@ -69,6 +82,10 @@ func (_m *MockSearchService) RegisterDashboardIndexExtender(ext DashboardIndexEx func (_m *MockSearchService) Run(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Run") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -79,7 +96,7 @@ func (_m *MockSearchService) Run(ctx context.Context) error { return r0 } -// TriggerReIndex provides a mock function with given fields: +// TriggerReIndex provides a mock function with no fields func (_m *MockSearchService) TriggerReIndex() { _m.Called() } @@ -88,6 +105,10 @@ func (_m *MockSearchService) TriggerReIndex() { func (_m *MockSearchService) doDashboardQuery(ctx context.Context, _a1 *user.SignedInUser, orgId int64, query DashboardQuery) *backend.DataResponse { ret := _m.Called(ctx, _a1, orgId, query) + if len(ret) == 0 { + panic("no return value specified for doDashboardQuery") + } + var r0 *backend.DataResponse if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64, DashboardQuery) *backend.DataResponse); ok { r0 = rf(ctx, _a1, orgId, query) @@ -99,3 +120,17 @@ func (_m *MockSearchService) doDashboardQuery(ctx context.Context, _a1 *user.Sig return r0 } + +// NewMockSearchService creates a new instance of MockSearchService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockSearchService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockSearchService { + mock := &MockSearchService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/secrets/fakes/mock_service.go b/pkg/services/secrets/fakes/mock_service.go index 30ac1628653..a138659559c 100644 --- a/pkg/services/secrets/fakes/mock_service.go +++ b/pkg/services/secrets/fakes/mock_service.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package fakes @@ -18,6 +18,10 @@ type MockService struct { func (_m *MockService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) { ret := _m.Called(ctx, payload) + if len(ret) == 0 { + panic("no return value specified for Decrypt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok { @@ -44,6 +48,10 @@ func (_m *MockService) Decrypt(ctx context.Context, payload []byte) ([]byte, err func (_m *MockService) DecryptJsonData(ctx context.Context, sjd map[string][]byte) (map[string]string, error) { ret := _m.Called(ctx, sjd) + if len(ret) == 0 { + panic("no return value specified for DecryptJsonData") + } + var r0 map[string]string var r1 error if rf, ok := ret.Get(0).(func(context.Context, map[string][]byte) (map[string]string, error)); ok { @@ -70,6 +78,10 @@ func (_m *MockService) DecryptJsonData(ctx context.Context, sjd map[string][]byt func (_m *MockService) Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error) { ret := _m.Called(ctx, payload, opt) + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, []byte, secrets.EncryptionOptions) ([]byte, error)); ok { @@ -96,6 +108,10 @@ func (_m *MockService) Encrypt(ctx context.Context, payload []byte, opt secrets. func (_m *MockService) EncryptJsonData(ctx context.Context, kv map[string]string, opt secrets.EncryptionOptions) (map[string][]byte, error) { ret := _m.Called(ctx, kv, opt) + if len(ret) == 0 { + panic("no return value specified for EncryptJsonData") + } + var r0 map[string][]byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, map[string]string, secrets.EncryptionOptions) (map[string][]byte, error)); ok { @@ -122,6 +138,10 @@ func (_m *MockService) EncryptJsonData(ctx context.Context, kv map[string]string func (_m *MockService) GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key string, fallback string) string { ret := _m.Called(ctx, sjd, key, fallback) + if len(ret) == 0 { + panic("no return value specified for GetDecryptedValue") + } + var r0 string if rf, ok := ret.Get(0).(func(context.Context, map[string][]byte, string, string) string); ok { r0 = rf(ctx, sjd, key, fallback) @@ -136,6 +156,10 @@ func (_m *MockService) GetDecryptedValue(ctx context.Context, sjd map[string][]b func (_m *MockService) ReEncryptDataKeys(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for ReEncryptDataKeys") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -150,6 +174,10 @@ func (_m *MockService) ReEncryptDataKeys(ctx context.Context) error { func (_m *MockService) RotateDataKeys(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for RotateDataKeys") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) diff --git a/pkg/services/serviceaccounts/tests/extsvcaccmock.go b/pkg/services/serviceaccounts/tests/extsvcaccmock.go index a81afeb50a8..3f1fdf31152 100644 --- a/pkg/services/serviceaccounts/tests/extsvcaccmock.go +++ b/pkg/services/serviceaccounts/tests/extsvcaccmock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package tests @@ -18,6 +18,10 @@ type MockExtSvcAccountsService struct { func (_m *MockExtSvcAccountsService) EnableExtSvcAccount(ctx context.Context, cmd *serviceaccounts.EnableExtSvcAccountCmd) error { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for EnableExtSvcAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.EnableExtSvcAccountCmd) error); ok { r0 = rf(ctx, cmd) @@ -32,6 +36,10 @@ func (_m *MockExtSvcAccountsService) EnableExtSvcAccount(ctx context.Context, cm func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *serviceaccounts.ManageExtSvcAccountCmd) (int64, error) { ret := _m.Called(ctx, cmd) + if len(ret) == 0 { + panic("no return value specified for ManageExtSvcAccount") + } + var r0 int64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.ManageExtSvcAccountCmd) (int64, error)); ok { @@ -56,6 +64,10 @@ func (_m *MockExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cm func (_m *MockExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID int64, extSvcSlug string) error { ret := _m.Called(ctx, orgID, extSvcSlug) + if len(ret) == 0 { + panic("no return value specified for RemoveExtSvcAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) error); ok { r0 = rf(ctx, orgID, extSvcSlug) @@ -70,6 +82,10 @@ func (_m *MockExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, or func (_m *MockExtSvcAccountsService) RetrieveExtSvcAccount(ctx context.Context, orgID int64, saID int64) (*serviceaccounts.ExtSvcAccount, error) { ret := _m.Called(ctx, orgID, saID) + if len(ret) == 0 { + panic("no return value specified for RetrieveExtSvcAccount") + } + var r0 *serviceaccounts.ExtSvcAccount var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*serviceaccounts.ExtSvcAccount, error)); ok { diff --git a/pkg/services/serviceaccounts/tests/mocks.go b/pkg/services/serviceaccounts/tests/mocks.go index a33fc5ae5b4..28df9752b24 100644 --- a/pkg/services/serviceaccounts/tests/mocks.go +++ b/pkg/services/serviceaccounts/tests/mocks.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.35.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package tests @@ -21,6 +21,10 @@ type MockServiceAccountService struct { func (_m *MockServiceAccountService) AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error) { ret := _m.Called(ctx, serviceAccountID, cmd) + if len(ret) == 0 { + panic("no return value specified for AddServiceAccountToken") + } + var r0 *apikey.APIKey var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.AddServiceAccountTokenCommand) (*apikey.APIKey, error)); ok { @@ -47,6 +51,10 @@ func (_m *MockServiceAccountService) AddServiceAccountToken(ctx context.Context, func (_m *MockServiceAccountService) CreateServiceAccount(ctx context.Context, orgID int64, saForm *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) { ret := _m.Called(ctx, orgID, saForm) + if len(ret) == 0 { + panic("no return value specified for CreateServiceAccount") + } + var r0 *serviceaccounts.ServiceAccountDTO var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, *serviceaccounts.CreateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error)); ok { @@ -73,6 +81,10 @@ func (_m *MockServiceAccountService) CreateServiceAccount(ctx context.Context, o func (_m *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64) error { ret := _m.Called(ctx, orgID, serviceAccountID) + if len(ret) == 0 { + panic("no return value specified for DeleteServiceAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok { r0 = rf(ctx, orgID, serviceAccountID) @@ -87,6 +99,10 @@ func (_m *MockServiceAccountService) DeleteServiceAccount(ctx context.Context, o func (_m *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Context, orgID int64, serviceAccountID int64, tokenID int64) error { ret := _m.Called(ctx, orgID, serviceAccountID, tokenID) + if len(ret) == 0 { + panic("no return value specified for DeleteServiceAccountToken") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64, int64) error); ok { r0 = rf(ctx, orgID, serviceAccountID, tokenID) @@ -101,6 +117,10 @@ func (_m *MockServiceAccountService) DeleteServiceAccountToken(ctx context.Conte func (_m *MockServiceAccountService) EnableServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, enable bool) error { ret := _m.Called(ctx, orgID, serviceAccountID, enable) + if len(ret) == 0 { + panic("no return value specified for EnableServiceAccount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64, bool) error); ok { r0 = rf(ctx, orgID, serviceAccountID, enable) @@ -115,6 +135,10 @@ func (_m *MockServiceAccountService) EnableServiceAccount(ctx context.Context, o func (_m *MockServiceAccountService) ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for ListTokens") + } + var r0 []apikey.APIKey var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)); ok { @@ -141,6 +165,10 @@ func (_m *MockServiceAccountService) ListTokens(ctx context.Context, query *serv func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) { ret := _m.Called(ctx, orgID) + if len(ret) == 0 { + panic("no return value specified for MigrateApiKeysToServiceAccounts") + } + var r0 *serviceaccounts.MigrationResult var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64) (*serviceaccounts.MigrationResult, error)); ok { @@ -163,10 +191,14 @@ func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context return r0, r1 } -// RetrieveServiceAccount provides a mock function with given fields: ctx, orgID, serviceAccountID +// RetrieveServiceAccount provides a mock function with given fields: ctx, query func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for RetrieveServiceAccount") + } + var r0 *serviceaccounts.ServiceAccountProfileDTO var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { @@ -193,6 +225,10 @@ func (_m *MockServiceAccountService) RetrieveServiceAccount(ctx context.Context, func (_m *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { ret := _m.Called(ctx, orgID, name) + if len(ret) == 0 { + panic("no return value specified for RetrieveServiceAccountIdByName") + } + var r0 int64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, string) (int64, error)); ok { @@ -217,6 +253,10 @@ func (_m *MockServiceAccountService) RetrieveServiceAccountIdByName(ctx context. func (_m *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Context, query *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error) { ret := _m.Called(ctx, query) + if len(ret) == 0 { + panic("no return value specified for SearchOrgServiceAccounts") + } + var r0 *serviceaccounts.SearchOrgServiceAccountsResult var r1 error if rf, ok := ret.Get(0).(func(context.Context, *serviceaccounts.SearchOrgServiceAccountsQuery) (*serviceaccounts.SearchOrgServiceAccountsResult, error)); ok { @@ -243,6 +283,10 @@ func (_m *MockServiceAccountService) SearchOrgServiceAccounts(ctx context.Contex func (_m *MockServiceAccountService) UpdateServiceAccount(ctx context.Context, orgID int64, serviceAccountID int64, saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) { ret := _m.Called(ctx, orgID, serviceAccountID, saForm) + if len(ret) == 0 { + panic("no return value specified for UpdateServiceAccount") + } + var r0 *serviceaccounts.ServiceAccountProfileDTO var r1 error if rf, ok := ret.Get(0).(func(context.Context, int64, int64, *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error)); ok { diff --git a/pkg/services/sqlstore/database_config.go b/pkg/services/sqlstore/database_config.go index 171ff4972b9..f5ba069df15 100644 --- a/pkg/services/sqlstore/database_config.go +++ b/pkg/services/sqlstore/database_config.go @@ -39,6 +39,7 @@ type DatabaseConfig struct { WALEnabled bool UrlQueryParams map[string][]string SkipMigrations bool + EnsureDefaultOrgAndUser bool MigrationLock bool MigrationLockAttemptTimeout int LogQueries bool @@ -114,6 +115,7 @@ func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error { dbCfg.CacheMode = sec.Key("cache_mode").MustString("private") dbCfg.WALEnabled = sec.Key("wal").MustBool(false) dbCfg.SkipMigrations = sec.Key("skip_migrations").MustBool() + dbCfg.EnsureDefaultOrgAndUser = sec.Key("ensure_default_org_and_user").MustBool(true) dbCfg.MigrationLock = sec.Key("migration_locking").MustBool(true) dbCfg.MigrationLockAttemptTimeout = sec.Key("locking_attempt_timeout_sec").MustInt() diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 0cfb63fb9e9..b54a177cf02 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -47,7 +47,6 @@ type SQLStore struct { engine *xorm.Engine log log.Logger dialect migrator.Dialect - skipEnsureDefaultOrgAndUser bool migrations registry.DatabaseMigrator tracer tracing.Tracer recursiveQueriesAreSupported *bool @@ -62,7 +61,7 @@ func ProvideService(cfg *setting.Cfg, // by that mimic the functionality of how it was functioning before // xorm's changes above. xorm.DefaultPostgresSchema = "" - s, err := newStore(cfg, nil, features, migrations, bus, tracer, false) + s, err := newStore(cfg, nil, features, migrations, bus, tracer) if err != nil { return nil, err } @@ -80,7 +79,7 @@ func ProvideService(cfg *setting.Cfg, } func ProvideServiceForTests(t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, bus bus.Bus, migrations registry.DatabaseMigrator) (*SQLStore, error) { - return initTestDB(t, cfg, features, migrations, bus, InitTestDBOpt{EnsureDefaultOrgAndUser: true}) + return initTestDB(t, cfg, features, migrations, bus, InitTestDBOpt{}) } // NewSQLStoreWithoutSideEffects creates a new *SQLStore without side-effects such as @@ -88,20 +87,20 @@ func ProvideServiceForTests(t sqlutil.ITestDB, cfg *setting.Cfg, features featur func NewSQLStoreWithoutSideEffects(cfg *setting.Cfg, features featuremgmt.FeatureToggles, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) { - return newStore(cfg, nil, features, nil, bus, tracer, true) + cfgDBSection := cfg.Raw.Section("database") + cfgDBSection.Key("ensure_default_org_and_user").SetValue("false") + return newStore(cfg, nil, features, nil, bus, tracer) } func newStore(cfg *setting.Cfg, engine *xorm.Engine, features featuremgmt.FeatureToggles, - migrations registry.DatabaseMigrator, bus bus.Bus, tracer tracing.Tracer, - skipEnsureDefaultOrgAndUser bool) (*SQLStore, error) { + migrations registry.DatabaseMigrator, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) { ss := &SQLStore{ - cfg: cfg, - log: log.New("sqlstore"), - skipEnsureDefaultOrgAndUser: skipEnsureDefaultOrgAndUser, - migrations: migrations, - bus: bus, - tracer: tracer, - features: features, + cfg: cfg, + log: log.New("sqlstore"), + migrations: migrations, + bus: bus, + tracer: tracer, + features: features, } if err := ss.initEngine(engine); err != nil { @@ -148,11 +147,10 @@ func (ss *SQLStore) Migrate(isDatabaseLockingEnabled bool) error { // Reset resets database state. // If default org and user creation is enabled, it will be ensured they exist in the database. func (ss *SQLStore) Reset() error { - if ss.skipEnsureDefaultOrgAndUser { - return nil + if ss.dbCfg.EnsureDefaultOrgAndUser { + return ss.ensureMainOrgAndAdminUser(false) } - - return ss.ensureMainOrgAndAdminUser(false) + return nil } // Quote quotes the value in the used SQL dialect @@ -412,10 +410,8 @@ var testSQLStoreCleanup []func() // InitTestDBOpt contains options for InitTestDB. type InitTestDBOpt struct { - // EnsureDefaultOrgAndUser flags whether to ensure that default org and user exist. - EnsureDefaultOrgAndUser bool - FeatureFlags []string - Cfg *setting.Cfg + FeatureFlags []string + Cfg *setting.Cfg } // InitTestDBWithMigration initializes the test DB given custom migrations. @@ -533,7 +529,8 @@ func TestMain(m *testing.M) { } if len(opts) == 0 { - opts = []InitTestDBOpt{{EnsureDefaultOrgAndUser: false, FeatureFlags: []string{}}} + cfgDBSec := testCfg.Raw.Section("database") + cfgDBSec.Key("ensure_default_org_and_user").SetValue("false") } if testSQLStore == nil { @@ -566,16 +563,8 @@ func TestMain(m *testing.M) { engine.DatabaseTZ = time.UTC engine.TZLocation = time.UTC - skipEnsureDefaultOrgAndUser := false - for _, opt := range opts { - if !opt.EnsureDefaultOrgAndUser { - skipEnsureDefaultOrgAndUser = true - break - } - } - tracer := tracing.InitializeTracerForTest() - testSQLStore, err = newStore(testCfg, engine, features, migration, bus, tracer, skipEnsureDefaultOrgAndUser) + testSQLStore, err = newStore(testCfg, engine, features, migration, bus, tracer) if err != nil { return nil, err } diff --git a/pkg/services/sqlstore/sqlstore_testinfra.go b/pkg/services/sqlstore/sqlstore_testinfra.go index 194cf843c2f..5ac0b8e8288 100644 --- a/pkg/services/sqlstore/sqlstore_testinfra.go +++ b/pkg/services/sqlstore/sqlstore_testinfra.go @@ -174,8 +174,12 @@ func NewTestStore(tb TestingTB, opts ...TestOption) *SQLStore { engine.DatabaseTZ = time.UTC engine.TZLocation = time.UTC + cfgDBSec := cfg.Raw.Section("database") + shouldEnsure := fmt.Sprintf("%t", !options.NoDefaultUserOrg && !options.Truncate) + cfgDBSec.Key("ensure_default_org_and_user").SetValue(shouldEnsure) + store, err := newStore(cfg, engine, features, options.MigratorFactory(features), - options.Bus, options.Tracer, options.NoDefaultUserOrg || options.Truncate) + options.Bus, options.Tracer) if err != nil { tb.Fatalf("failed to create a new SQLStore: %v", err) panic("unreachable") diff --git a/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go b/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go index b02bc013056..069f5d2ea7b 100644 --- a/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package ssosettingstests diff --git a/pkg/services/ssosettings/ssosettingstests/service_mock.go b/pkg/services/ssosettings/ssosettingstests/service_mock.go index 45bd2ab9dc9..528e3e5a059 100644 --- a/pkg/services/ssosettings/ssosettingstests/service_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package ssosettingstests diff --git a/pkg/services/ssosettings/ssosettingstests/store_mock.go b/pkg/services/ssosettings/ssosettingstests/store_mock.go index 009660cd3e6..3ac1d6c1096 100644 --- a/pkg/services/ssosettings/ssosettingstests/store_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package ssosettingstests diff --git a/pkg/services/ssosettings/strategies/oauth_strategy_test.go b/pkg/services/ssosettings/strategies/oauth_strategy_test.go index c76aba9c839..568e94f6439 100644 --- a/pkg/services/ssosettings/strategies/oauth_strategy_test.go +++ b/pkg/services/ssosettings/strategies/oauth_strategy_test.go @@ -128,6 +128,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { force_use_graph_api = true allowed_organizations = org1, org2 workload_identity_token_file = azuread_token_file + domain_hint = my-domain [auth.github] team_ids = first, second @@ -163,6 +164,7 @@ func TestGetProviderConfig_ExtraFields(t *testing.T) { require.Equal(t, true, result["force_use_graph_api"]) require.Equal(t, "org1, org2", result["allowed_organizations"]) require.Equal(t, "azuread_token_file", result["workload_identity_token_file"]) + require.Equal(t, "my-domain", result["domain_hint"]) }) t.Run(social.GitHubProviderName, func(t *testing.T) { diff --git a/pkg/services/ssosettings/validation/oauth_validators.go b/pkg/services/ssosettings/validation/oauth_validators.go index 7899bce8706..1ef1d2434ed 100644 --- a/pkg/services/ssosettings/validation/oauth_validators.go +++ b/pkg/services/ssosettings/validation/oauth_validators.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "net/url" + "regexp" "slices" "strings" @@ -11,6 +12,8 @@ import ( "github.com/grafana/grafana/pkg/services/ssosettings" ) +var domainRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,6}$`) + func AllowAssignGrafanaAdminValidator(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester identity.Requester) ssosettings.ValidateFunc[social.OAuthInfo] { return func(info *social.OAuthInfo, requester identity.Requester) error { hasChanged := info.AllowAssignGrafanaAdmin != oldInfo.AllowAssignGrafanaAdmin @@ -66,6 +69,18 @@ func UrlValidator(value string, name string) ssosettings.ValidateFunc[social.OAu } } +func DomainValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] { + return func(info *social.OAuthInfo, requester identity.Requester) error { + if value == "" { + return nil + } + if !domainRegexp.MatchString(value) { + return ssosettings.ErrInvalidOAuthConfig(fmt.Sprintf("%s contains an invalid domain.", name)) + } + return nil + } +} + func RequiredUrlValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] { return func(info *social.OAuthInfo, requester identity.Requester) error { if err := RequiredValidator(value, name)(info, requester); err != nil { diff --git a/pkg/services/ssosettings/validation/oauth_validators_test.go b/pkg/services/ssosettings/validation/oauth_validators_test.go index 9bb17f3eb79..09c865821fe 100644 --- a/pkg/services/ssosettings/validation/oauth_validators_test.go +++ b/pkg/services/ssosettings/validation/oauth_validators_test.go @@ -49,6 +49,113 @@ func TestUrlValidator(t *testing.T) { } } +func TestDomainValidator(t *testing.T) { + tc := []testCase{ + { + name: "passes when domain is valid", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "example.com"}, + }, + wantErr: nil, + }, + { + name: "passes when domain is empty", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": ""}, + }, + wantErr: nil, + }, + { + name: "passes when domain has subdomain", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "sub.example.com"}, + }, + wantErr: nil, + }, + { + name: "fails when domain is invalid", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "invalid-domain"}, + }, + wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."), + }, + { + name: "fails when domain has invalid characters", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "example.com!"}, + }, + wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."), + }, + { + name: "fails when TLD is too short (1 character)", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "example.x"}, + }, + wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."), + }, + { + name: "passes when TLD is minimum length (2 characters)", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "example.co"}, + }, + wantErr: nil, + }, + { + name: "passes when TLD is maximum length (6 characters)", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "example.museum"}, + }, + wantErr: nil, + }, + { + name: "fails when TLD is too long (7+ characters)", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "example.toolong"}, + }, + wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."), + }, + { + name: "passes when domain is maximum reasonable length", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "very-long-subdomain-name-that-is-still-valid.example-organization.museum"}, + }, + wantErr: nil, + }, + { + name: "fails when domain segment is too long (over 63 characters)", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "this-is-a-very-long-subdomain-name-that-exceeds-the-maximum-allowed-length-for-a-dns-label.example.com"}, + }, + wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."), + }, + { + name: "passes when domain segment is exactly 63 characters", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "this-is-exactly-sixty-three-characters-long-which-is-the-max.com"}, + }, + wantErr: nil, + }, + { + name: "fails when domain is just a single character", + input: &social.OAuthInfo{ + Extra: map[string]string{"domain_hint": "a"}, + }, + wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."), + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + err := DomainValidator(tt.input.Extra["domain_hint"], "Domain Hint")(tt.input, tt.requester) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + func TestRequiredValidator(t *testing.T) { tc := []testCase{ { diff --git a/pkg/services/store/entity_events_mock.go b/pkg/services/store/entity_events_mock.go index 52dc2bdff45..460f81a75c4 100644 --- a/pkg/services/store/entity_events_mock.go +++ b/pkg/services/store/entity_events_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package store @@ -18,7 +18,15 @@ type MockEntityEventsService struct { func (_m *MockEntityEventsService) GetAllEventsAfter(ctx context.Context, id int64) ([]*EntityEvent, error) { ret := _m.Called(ctx, id) + if len(ret) == 0 { + panic("no return value specified for GetAllEventsAfter") + } + var r0 []*EntityEvent + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]*EntityEvent, error)); ok { + return rf(ctx, id) + } if rf, ok := ret.Get(0).(func(context.Context, int64) []*EntityEvent); ok { r0 = rf(ctx, id) } else { @@ -27,7 +35,6 @@ func (_m *MockEntityEventsService) GetAllEventsAfter(ctx context.Context, id int } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { r1 = rf(ctx, id) } else { @@ -41,7 +48,15 @@ func (_m *MockEntityEventsService) GetAllEventsAfter(ctx context.Context, id int func (_m *MockEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEvent, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetLastEvent") + } + var r0 *EntityEvent + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*EntityEvent, error)); ok { + return rf(ctx) + } if rf, ok := ret.Get(0).(func(context.Context) *EntityEvent); ok { r0 = rf(ctx) } else { @@ -50,7 +65,6 @@ func (_m *MockEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEve } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context) error); ok { r1 = rf(ctx) } else { @@ -60,10 +74,14 @@ func (_m *MockEntityEventsService) GetLastEvent(ctx context.Context) (*EntityEve return r0, r1 } -// IsDisabled provides a mock function with given fields: +// IsDisabled provides a mock function with no fields func (_m *MockEntityEventsService) IsDisabled() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for IsDisabled") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -74,15 +92,14 @@ func (_m *MockEntityEventsService) IsDisabled() bool { return r0 } -// OnEvent provides a mock function with given fields: handler -func (_m *MockEntityEventsService) OnEvent(handler EventHandler) { - _m.Called(handler) -} - // Run provides a mock function with given fields: ctx func (_m *MockEntityEventsService) Run(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for Run") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -93,24 +110,14 @@ func (_m *MockEntityEventsService) Run(ctx context.Context) error { return r0 } -// SaveEvent provides a mock function with given fields: ctx, cmd -func (_m *MockEntityEventsService) SaveEvent(ctx context.Context, cmd SaveEventCmd) error { - ret := _m.Called(ctx, cmd) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, SaveEventCmd) error); ok { - r0 = rf(ctx, cmd) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // deleteEventsOlderThan provides a mock function with given fields: ctx, duration func (_m *MockEntityEventsService) deleteEventsOlderThan(ctx context.Context, duration time.Duration) error { ret := _m.Called(ctx, duration) + if len(ret) == 0 { + panic("no return value specified for deleteEventsOlderThan") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, time.Duration) error); ok { r0 = rf(ctx, duration) @@ -121,13 +128,12 @@ func (_m *MockEntityEventsService) deleteEventsOlderThan(ctx context.Context, du return r0 } -type NewMockEntityEventsServiceT interface { +// NewMockEntityEventsService creates a new instance of MockEntityEventsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockEntityEventsService(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockEntityEventsService creates a new instance of MockEntityEventsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockEntityEventsService(t NewMockEntityEventsServiceT) *MockEntityEventsService { +}) *MockEntityEventsService { mock := &MockEntityEventsService{} mock.Mock.Test(t) diff --git a/pkg/services/store/system_users_mock.go b/pkg/services/store/system_users_mock.go index 525a807dd69..d733148bb35 100644 --- a/pkg/services/store/system_users_mock.go +++ b/pkg/services/store/system_users_mock.go @@ -1,11 +1,11 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package store import ( + filestorage "github.com/grafana/grafana/pkg/infra/filestorage" mock "github.com/stretchr/testify/mock" - filestorage "github.com/grafana/grafana/pkg/infra/filestorage" user "github.com/grafana/grafana/pkg/services/user" ) @@ -18,7 +18,15 @@ type FakeSystemUsers struct { func (_m *FakeSystemUsers) GetFilter(_a0 *user.SignedInUser) (map[string]filestorage.PathFilter, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetFilter") + } + var r0 map[string]filestorage.PathFilter + var r1 error + if rf, ok := ret.Get(0).(func(*user.SignedInUser) (map[string]filestorage.PathFilter, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(*user.SignedInUser) map[string]filestorage.PathFilter); ok { r0 = rf(_a0) } else { @@ -27,7 +35,6 @@ func (_m *FakeSystemUsers) GetFilter(_a0 *user.SignedInUser) (map[string]filesto } } - var r1 error if rf, ok := ret.Get(1).(func(*user.SignedInUser) error); ok { r1 = rf(_a0) } else { @@ -41,7 +48,15 @@ func (_m *FakeSystemUsers) GetFilter(_a0 *user.SignedInUser) (map[string]filesto func (_m *FakeSystemUsers) GetUser(userType SystemUserType, orgID int64) (*user.SignedInUser, error) { ret := _m.Called(userType, orgID) + if len(ret) == 0 { + panic("no return value specified for GetUser") + } + var r0 *user.SignedInUser + var r1 error + if rf, ok := ret.Get(0).(func(SystemUserType, int64) (*user.SignedInUser, error)); ok { + return rf(userType, orgID) + } if rf, ok := ret.Get(0).(func(SystemUserType, int64) *user.SignedInUser); ok { r0 = rf(userType, orgID) } else { @@ -50,7 +65,6 @@ func (_m *FakeSystemUsers) GetUser(userType SystemUserType, orgID int64) (*user. } } - var r1 error if rf, ok := ret.Get(1).(func(SystemUserType, int64) error); ok { r1 = rf(userType, orgID) } else { @@ -65,13 +79,12 @@ func (_m *FakeSystemUsers) RegisterUser(userType SystemUserType, filterFn func() _m.Called(userType, filterFn) } -type mockConstructorTestingTNewFakeSystemUsers interface { +// NewFakeSystemUsers creates a new instance of FakeSystemUsers. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFakeSystemUsers(t interface { mock.TestingT Cleanup(func()) -} - -// NewFakeSystemUsers creates a new instance of FakeSystemUsers. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakeSystemUsers(t mockConstructorTestingTNewFakeSystemUsers) *FakeSystemUsers { +}) *FakeSystemUsers { mock := &FakeSystemUsers{} mock.Mock.Test(t) diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index b60bd4ac306..fc8d9589357 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -16,6 +16,8 @@ const ( HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota HelpFlagDashboardHelp1 HelpFlagEnterpriseAuth1 + HelpFlagSyntheticMonitoring1 + HelpFlagIRM1 ) type UpdateEmailActionType string @@ -93,8 +95,9 @@ type UpdateUserCommand struct { // 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:"-"` + OrgID *int64 `json:"-"` + HelpFlags1 *HelpFlags1 `json:"-"` + IsProvisioned *bool `json:"-"` } type UpdateUserLastSeenAtCommand struct { diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index ccbef1138f8..49d437e8cb9 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -278,6 +278,10 @@ func (ss *sqlStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) err q = q.MustCols("help_flags1") usr.HelpFlags1 = *cmd.HelpFlags1 }) + setOptional(cmd.IsProvisioned, func(v bool) { + q = q.UseBool("is_provisioned") + usr.IsProvisioned = v + }) if _, err := q.Update(&usr); err != nil { return err diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index a3dfe791296..9dcceee9576 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -739,6 +739,47 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.True(t, usr.IsDisabled) }) + t.Run("Update IsProvisioned", func(t *testing.T) { + // Create a user with IsProvisioned set to false (default) + id, err := userStore.Insert(context.Background(), &user.User{ + Name: "provisioned_user", + Email: "provisioned@test.com", + Login: "provisioned_user", + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + // Verify initial state + usr, err := userStore.GetByID(context.Background(), id) + require.NoError(t, err) + require.False(t, usr.IsProvisioned) + + // Update user to set IsProvisioned to true + err = userStore.Update(context.Background(), &user.UpdateUserCommand{ + UserID: id, + IsProvisioned: boolPtr(true), + }) + require.NoError(t, err) + + // Verify IsProvisioned is now true + usr, err = userStore.GetByID(context.Background(), id) + require.NoError(t, err) + require.True(t, usr.IsProvisioned) + + // Update user to set IsProvisioned to false + err = userStore.Update(context.Background(), &user.UpdateUserCommand{ + UserID: id, + IsProvisioned: boolPtr(false), + }) + require.NoError(t, err) + + // Verify IsProvisioned is now false + usr, err = userStore.GetByID(context.Background(), id) + require.NoError(t, err) + require.False(t, usr.IsProvisioned) + }) + t.Run("Testing DB - multiple users", func(t *testing.T) { ss = db.InitTestDB(t) diff --git a/pkg/services/user/usertest/mock.go b/pkg/services/user/usertest/mock.go index 943b1520fb1..36da416d1ef 100644 --- a/pkg/services/user/usertest/mock.go +++ b/pkg/services/user/usertest/mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package usertest @@ -230,35 +230,6 @@ func (_m *MockService) GetByUID(_a0 context.Context, _a1 *user.GetUserByUIDQuery return r0, r1 } -func (_m *MockService) ListByIdOrUID(_a0 context.Context, _a1 []string, _a2 []int64) ([]*user.User, error) { - ret := _m.Called(_a0, _a1, _a2) - - if len(ret) == 0 { - panic("no return value specified for ListByIdOrUID") - } - - var r0 []*user.User - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) ([]*user.User, error)); ok { - return rf(_a0, _a1, _a2) - } - if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) []*user.User); ok { - r0 = rf(_a0, _a1, _a2) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*user.User) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, []string, []int64) error); ok { - r1 = rf(_a0, _a1, _a2) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetProfile provides a mock function with given fields: _a0, _a1 func (_m *MockService) GetProfile(_a0 context.Context, _a1 *user.GetUserProfileQuery) (*user.UserProfileDTO, error) { ret := _m.Called(_a0, _a1) @@ -339,6 +310,36 @@ func (_m *MockService) GetUsageStats(ctx context.Context) map[string]interface{} return r0 } +// ListByIdOrUID provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockService) ListByIdOrUID(_a0 context.Context, _a1 []string, _a2 []int64) ([]*user.User, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for ListByIdOrUID") + } + + var r0 []*user.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) ([]*user.User, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, []int64) []*user.User); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*user.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, []int64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Search provides a mock function with given fields: _a0, _a1 func (_m *MockService) Search(_a0 context.Context, _a1 *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) { ret := _m.Called(_a0, _a1) diff --git a/pkg/setting/setting_secrets_manager.go b/pkg/setting/setting_secrets_manager.go index f8a3b186e53..b855307635f 100644 --- a/pkg/setting/setting_secrets_manager.go +++ b/pkg/setting/setting_secrets_manager.go @@ -1,22 +1,37 @@ package setting import ( - "regexp" + "strings" +) - "github.com/grafana/grafana/pkg/services/kmsproviders" +const ( + ProviderPrefix = "secrets_manager.encryption." + MisconfiguredProvider = "misconfigured" ) type SecretsManagerSettings struct { - SecretKey string - EncryptionProvider string - AvailableProviders []string + CurrentEncryptionProvider string + + // ConfiguredKMSProviders is a map of KMS providers found in the config file. The keys are in the format of ., and the values are a map of the properties in that section + // In OSS, the provider type can only be "secret_key". In Enterprise, it can additionally be one of: "aws_kms", "azure_keyvault", "google_kms", "hashicorp_vault" + ConfiguredKMSProviders map[string]map[string]string } func (cfg *Cfg) readSecretsManagerSettings() { secretsMgmt := cfg.Raw.Section("secrets_manager") - cfg.SecretsManagement.EncryptionProvider = secretsMgmt.Key("encryption_provider").MustString(kmsproviders.Default) + cfg.SecretsManagement.CurrentEncryptionProvider = secretsMgmt.Key("encryption_provider").MustString(MisconfiguredProvider) - // TODO: These are not used yet by the secrets manager because we need to distentagle the dependencies with OSS. - cfg.SecretsManagement.SecretKey = secretsMgmt.Key("secret_key").MustString("") - cfg.SecretsManagement.AvailableProviders = regexp.MustCompile(`\s*,\s*`).Split(secretsMgmt.Key("available_encryption_providers").MustString(""), -1) // parse comma separated list + // Extract available KMS providers from configuration sections + providers := make(map[string]map[string]string) + for _, section := range cfg.Raw.Sections() { + sectionName := section.Name() + if strings.HasPrefix(sectionName, ProviderPrefix) { + // Extract the provider name (everything after the prefix) + providerName := strings.TrimPrefix(sectionName, ProviderPrefix) + if providerName != "" { + providers[providerName] = section.KeysHash() + } + } + } + cfg.SecretsManagement.ConfiguredKMSProviders = providers } diff --git a/pkg/setting/setting_secrets_manager_test.go b/pkg/setting/setting_secrets_manager_test.go new file mode 100644 index 00000000000..324acbe27ad --- /dev/null +++ b/pkg/setting/setting_secrets_manager_test.go @@ -0,0 +1,173 @@ +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadSecretsManagerSettings(t *testing.T) { + t.Run("should parse basic encryption provider", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, "aws_kms", cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Empty(t, cfg.SecretsManagement.ConfiguredKMSProviders) + }) + + t.Run("should parse single KMS provider configuration", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms.v1 + +[secrets_manager.encryption.aws_kms.v1] +region = us-east-1 +key_id = arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012 +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 1) + + awsProvider := cfg.SecretsManagement.ConfiguredKMSProviders["aws_kms.v1"] + assert.Equal(t, "us-east-1", awsProvider["region"]) + assert.Equal(t, "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012", awsProvider["key_id"]) + }) + + t.Run("should parse multiple KMS providers", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms.v1 + +[secrets_manager.encryption.aws_kms.v1] +region = us-east-1 +key_id = arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012 + +[secrets_manager.encryption.azure_kv.v1] +vault_url = https://myvault.vault.azure.net/ +key_name = mykey +tenant_id = 12345678-1234-1234-1234-123456789012 + +[secrets_manager.encryption.secret_key.v1] +key = my-secret-key +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 3) + + // Check AWS KMS provider + awsProvider := cfg.SecretsManagement.ConfiguredKMSProviders["aws_kms.v1"] + assert.Equal(t, "us-east-1", awsProvider["region"]) + assert.Equal(t, "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012", awsProvider["key_id"]) + + // Check Azure Key Vault provider + azureProvider := cfg.SecretsManagement.ConfiguredKMSProviders["azure_kv.v1"] + assert.Equal(t, "https://myvault.vault.azure.net/", azureProvider["vault_url"]) + assert.Equal(t, "mykey", azureProvider["key_name"]) + assert.Equal(t, "12345678-1234-1234-1234-123456789012", azureProvider["tenant_id"]) + + // Check secret key provider + secretProvider := cfg.SecretsManagement.ConfiguredKMSProviders["secret_key.v1"] + assert.Equal(t, "my-secret-key", secretProvider["key"]) + }) + + t.Run("should default to misconfigured provider when no encryption_provider is set", func(t *testing.T) { + iniContent := ` +[secrets_manager] +# no encryption_provider setting +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, MisconfiguredProvider, cfg.SecretsManagement.CurrentEncryptionProvider) + }) + + t.Run("should handle empty sections gracefully", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = empty_provider + +[secrets_manager.encryption.empty_provider] +# empty section +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, "empty_provider", cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 1) + + emptyProvider := cfg.SecretsManagement.ConfiguredKMSProviders["empty_provider"] + assert.NotNil(t, emptyProvider) + assert.Empty(t, emptyProvider) + }) + + t.Run("should ignore sections that don't match provider prefix", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms.v1 + +[secrets_manager.encryption.valid_provider] +key = value + +[secrets_manager.other_section] +setting = value + +[completely_different_section] +some_setting = some_value +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 1) + + validProvider := cfg.SecretsManagement.ConfiguredKMSProviders["valid_provider"] + assert.Equal(t, "value", validProvider["key"]) + }) + + t.Run("should handle provider names with special characters", func(t *testing.T) { + iniContent := ` +[secrets_manager] +encryption_provider = aws_kms.v1 + +[secrets_manager.encryption.aws_kms.v1] +region = us-west-2 +key_id = test-key + +[secrets_manager.encryption.azure_kv.v1] +vault_url = https://test.vault.azure.net/ +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 2) + + awsProvider := cfg.SecretsManagement.ConfiguredKMSProviders["aws_kms.v1"] + assert.Equal(t, "us-west-2", awsProvider["region"]) + assert.Equal(t, "test-key", awsProvider["key_id"]) + + azureProvider := cfg.SecretsManagement.ConfiguredKMSProviders["azure_kv.v1"] + assert.Equal(t, "https://test.vault.azure.net/", azureProvider["vault_url"]) + }) + + t.Run("should handle configuration with no secrets_manager section", func(t *testing.T) { + iniContent := ` +[server] +domain = example.com +` + cfg, err := NewCfgFromBytes([]byte(iniContent)) + require.NoError(t, err) + + assert.Equal(t, MisconfiguredProvider, cfg.SecretsManagement.CurrentEncryptionProvider) + assert.Empty(t, cfg.SecretsManagement.ConfiguredKMSProviders) + }) +} diff --git a/pkg/setting/settings_zanzana.go b/pkg/setting/settings_zanzana.go index 3b4fff83b78..be7121f3b0d 100644 --- a/pkg/setting/settings_zanzana.go +++ b/pkg/setting/settings_zanzana.go @@ -33,10 +33,8 @@ type ZanzanaServerSettings struct { // OpenFGA http server address which allows to connect with fga cli. // Can only be used in dev mode. OpenFGAHttpAddr string - // Enable cache for Check() requests - CheckQueryCache bool - // TTL for cached requests. Default is 10 seconds. - CheckQueryCacheTTL time.Duration + // Cache settings + CacheSettings OpenFgaCacheSettings // Max number of results returned by ListObjects() query. Default is 1000. ListObjectsMaxResults uint32 // Deadline for the ListObjects() query. Default is 3 seconds. @@ -50,6 +48,54 @@ type ZanzanaServerSettings struct { AllowInsecure bool } +// Parameters to configure OpenFGA cache. +type OpenFgaCacheSettings struct { + // Number of items that will be kept in the in-memory cache used to resolve Check queries. + // If CheckQueryCacheEnabled or CheckIteratorCacheEnabled, this is the size limit of the cache + CheckCacheLimit uint32 + // When enabled, cache controller will verify whether check subproblem cache and check + // iterator cache needs to be invalidated when there is a check or list objects API request. + // The invalidation determination is based on whether there are recent write or deletes for + // the store. This feature allows a larger check-query-cache-ttl and check-iterator-cache-ttl + // at the expense of additional datastore queries for recent writes and deletes. + CacheControllerEnabled bool + // Specifies how frequently the cache controller checks for Writes occurring. While the + // cache controller result is cached, the server will not read the datastore to check + // whether subproblem cache and iterator cache needs to be invalidated. + CacheControllerTTL time.Duration + // Enables in-memory caching of Check subproblems. For example, if you have a relation + // `define viewer: owner or editor`, and the query is `Check(user:anne, viewer, doc:1)`, + // we'll evaluate the `owner` relation and the `editor` relation and cache both results: + // `(user:anne, viewer, doc:1) -> allowed=true` and `(user:anne, owner, doc:1) -> allowed=true`. + CheckQueryCacheEnabled bool + // Time that items will be kept in the cache of Check subproblems + CheckQueryCacheTTL time.Duration + // Enables in-memory caching of database iterators. Each iterator is the result of a database + // query, for example, usersets related to a specific object, or objects related to a specific + // user, up to a certain number of tuples per iterator + CheckIteratorCacheEnabled bool + // Number of tuples that will be stored for each database iterator + CheckIteratorCacheMaxResults uint32 + // Time that items will be kept in the cache of database iterators + CheckIteratorCacheTTL time.Duration + // Enables in-memory caching of database iterators for ListObjects. Each iterator is the result + // of a database query, for example, usersets related to a specific object, or objects related + // to a specific user, up to a certain number of tuples per iterator + ListObjectsIteratorCacheEnabled bool + // Number of tuples that will be stored for each ListObjects database iterator + ListObjectsIteratorCacheMaxResults uint32 + // Time that items will be kept in the cache of ListObjects database iterators + ListObjectsIteratorCacheTTL time.Duration + // Enables sharing of datastore iterators with different consumers. Each iterator is the result + // of a database query, for example usersets related to a specific object, or objects related + // to a specific user, up to a certain number of tuples per iterator + SharedIteratorEnabled bool + // Limit of the number of iterators that can be shared when shared iterator is enabled + SharedIteratorLimit uint32 + // Time that shared iterators will be kept in the cache + SharedIteratorTTL time.Duration +} + func (cfg *Cfg) readZanzanaSettings() { zc := ZanzanaClientSettings{} clientSec := cfg.SectionWithEnvOverrides("zanzana.client") @@ -73,13 +119,27 @@ func (cfg *Cfg) readZanzanaSettings() { serverSec := cfg.SectionWithEnvOverrides("zanzana.server") zs.OpenFGAHttpAddr = serverSec.Key("http_addr").MustString("127.0.0.1:8080") - zs.CheckQueryCache = serverSec.Key("check_query_cache").MustBool(true) - zs.CheckQueryCacheTTL = serverSec.Key("check_query_cache_ttl").MustDuration(10 * time.Second) zs.ListObjectsDeadline = serverSec.Key("list_objects_deadline").MustDuration(3 * time.Second) zs.ListObjectsMaxResults = uint32(serverSec.Key("list_objects_max_results").MustUint(1000)) zs.UseStreamedListObjects = serverSec.Key("use_streamed_list_objects").MustBool(false) zs.SigningKeysURL = serverSec.Key("signing_keys_url").MustString("") zs.AllowInsecure = serverSec.Key("allow_insecure").MustBool(false) + // Cache settings + zs.CacheSettings.CheckCacheLimit = uint32(serverSec.Key("check_cache_limit").MustUint(10000)) + zs.CacheSettings.CacheControllerEnabled = serverSec.Key("cache_controller_enabled").MustBool(false) + zs.CacheSettings.CacheControllerTTL = serverSec.Key("cache_controller_ttl").MustDuration(10 * time.Second) + zs.CacheSettings.CheckQueryCacheEnabled = serverSec.Key("check_query_cache_enabled").MustBool(true) + zs.CacheSettings.CheckQueryCacheTTL = serverSec.Key("check_query_cache_ttl").MustDuration(10 * time.Second) + zs.CacheSettings.CheckIteratorCacheEnabled = serverSec.Key("check_iterator_cache_enabled").MustBool(false) + zs.CacheSettings.CheckIteratorCacheMaxResults = uint32(serverSec.Key("check_iterator_cache_max_results").MustUint(1000)) + zs.CacheSettings.CheckIteratorCacheTTL = serverSec.Key("check_iterator_cache_ttl").MustDuration(10 * time.Second) + zs.CacheSettings.ListObjectsIteratorCacheEnabled = serverSec.Key("list_objects_iterator_cache_enabled").MustBool(false) + zs.CacheSettings.ListObjectsIteratorCacheMaxResults = uint32(serverSec.Key("list_objects_iterator_cache_max_results").MustUint(1000)) + zs.CacheSettings.ListObjectsIteratorCacheTTL = serverSec.Key("list_objects_iterator_cache_ttl").MustDuration(10 * time.Second) + zs.CacheSettings.SharedIteratorEnabled = serverSec.Key("shared_iterator_enabled").MustBool(false) + zs.CacheSettings.SharedIteratorLimit = uint32(serverSec.Key("shared_iterator_limit").MustUint(1000)) + zs.CacheSettings.SharedIteratorTTL = serverSec.Key("shared_iterator_ttl").MustDuration(10 * time.Second) + cfg.ZanzanaServer = zs } diff --git a/pkg/setting/settingtest/provider_mock.go b/pkg/setting/settingtest/provider_mock.go index 9cccb382d72..bf0bb232d86 100644 --- a/pkg/setting/settingtest/provider_mock.go +++ b/pkg/setting/settingtest/provider_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.3. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package settingtest @@ -12,7 +12,7 @@ type MockProvider struct { mock.Mock } -// Current provides a mock function with given fields: +// Current provides a mock function with no fields func (_m *MockProvider) Current() setting.SettingsBag { ret := _m.Called() @@ -32,7 +32,7 @@ func (_m *MockProvider) Current() setting.SettingsBag { return r0 } -// CurrentVerbose provides a mock function with given fields: +// CurrentVerbose provides a mock function with no fields func (_m *MockProvider) CurrentVerbose() setting.VerboseSettingsBag { ret := _m.Called() diff --git a/pkg/storage/legacysql/dualwrite/dualwriter.go b/pkg/storage/legacysql/dualwrite/dualwriter.go index b46472be075..0cb1091b13b 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter.go @@ -39,7 +39,16 @@ type dualWriter struct { func (d *dualWriter) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { // If we read from unified, we can just do that and return. if d.readUnified { - return d.unified.Get(ctx, name, options) + unifiedGet, unifiedErr := d.unified.Get(ctx, name, options) + if apierrors.IsNotFound(unifiedErr) { + // If resource is not found in unified storage, fallback to legacy. + // This fixes cases in where records (stored in multiple tables, including permissions) + // are inserted first in legacy and then on Unified. + log := logging.FromContext(ctx).With("method", "Get") + log.Error("resource not found in Unified Storage, trying to GET from legacy", "err", unifiedErr) + return d.legacy.Get(ctx, name, options) + } + return unifiedGet, unifiedErr } // If legacy is still our main store, lets first read from it. legacyGet, err := d.legacy.Get(ctx, name, options) diff --git a/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go b/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go index bd55753c949..00753d1d656 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter_mode3_test.go @@ -109,12 +109,31 @@ func TestMode3_Get(t *testing.T) { }, }, { - name: "should return an error when getting an object in the unified store fails, and should not go to legacy", + name: "should return an error when getting an object in the unified store fails", setupStorageFn: func(m *mock.Mock, name string) { m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) }, wantErr: true, }, + { + name: "should return an error when getting an object in the unified store fails with not found error, and legacy fails as well", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error")) + }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "dashboards.dashboard.grafana.app", Resource: "dashboard"}, "uid")) + }, + wantErr: true, + }, + { + name: "should succeed when getting an object in the UnifiedStorage fails with NotFound, but Legacy succeeds", + setupLegacyFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil) + }, + setupStorageFn: func(m *mock.Mock, name string) { + m.On("Get", mock.Anything, name, mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "dashboards.dashboard.grafana.app", Resource: "dashboard"}, "uid")) + }, + }, { name: "should succeed when getting an object in the LegacyStorage fails", setupLegacyFn: func(m *mock.Mock, name string) { diff --git a/pkg/storage/legacysql/dualwrite/service_mock.go b/pkg/storage/legacysql/dualwrite/service_mock.go index 264c19d7047..d7524082652 100644 --- a/pkg/storage/legacysql/dualwrite/service_mock.go +++ b/pkg/storage/legacysql/dualwrite/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package dualwrite diff --git a/pkg/storage/secret/encryption/data/encrypted_value_create.sql b/pkg/storage/secret/encryption/data/encrypted_value_create.sql index a3c4378959e..7bce6a19bd4 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_create.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_create.sql @@ -1,12 +1,14 @@ INSERT INTO {{ .Ident "secret_encrypted_value" }} ( - {{ .Ident "uid" }}, {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} ) VALUES ( - {{ .Arg .Row.UID }}, {{ .Arg .Row.Namespace }}, + {{ .Arg .Row.Name }}, + {{ .Arg .Row.Version }}, {{ .Arg .Row.EncryptedData }}, {{ .Arg .Row.Created }}, {{ .Arg .Row.Updated }} diff --git a/pkg/storage/secret/encryption/data/encrypted_value_delete.sql b/pkg/storage/secret/encryption/data/encrypted_value_delete.sql index 8c505c23af1..e1c78f97d39 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_delete.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_delete.sql @@ -1,4 +1,6 @@ DELETE FROM {{ .Ident "secret_encrypted_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "uid" }} = {{ .Arg .UID }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} ; diff --git a/pkg/storage/secret/encryption/data/encrypted_value_read.sql b/pkg/storage/secret/encryption/data/encrypted_value_read.sql index f1e5e771b40..6a672554efb 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_read.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_read.sql @@ -1,11 +1,14 @@ SELECT - {{ .Ident "uid" }}, {{ .Ident "namespace" }}, + {{ .Ident "name" }}, + {{ .Ident "version" }}, {{ .Ident "encrypted_data" }}, {{ .Ident "created" }}, {{ .Ident "updated" }} FROM {{ .Ident "secret_encrypted_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "uid" }} = {{ .Arg .UID }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} ; diff --git a/pkg/storage/secret/encryption/data/encrypted_value_update.sql b/pkg/storage/secret/encryption/data/encrypted_value_update.sql index a068d53455e..08e985297a1 100644 --- a/pkg/storage/secret/encryption/data/encrypted_value_update.sql +++ b/pkg/storage/secret/encryption/data/encrypted_value_update.sql @@ -3,6 +3,8 @@ UPDATE SET {{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }}, {{ .Ident "updated" }} = {{ .Arg .Updated }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "uid" }} = {{ .Arg .UID }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} ; diff --git a/pkg/storage/secret/encryption/data_key_store.go b/pkg/storage/secret/encryption/data_key_store.go index 78c2bf63910..aa94333b629 100644 --- a/pkg/storage/secret/encryption/data_key_store.go +++ b/pkg/storage/secret/encryption/data_key_store.go @@ -5,13 +5,12 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) // encryptionStoreImpl is the actual implementation of the data key storage. @@ -19,26 +18,18 @@ type encryptionStoreImpl struct { db contracts.Database dialect sqltemplate.Dialect tracer trace.Tracer - log log.Logger metrics *DataKeyMetrics } func ProvideDataKeyStorage( db contracts.Database, tracer trace.Tracer, - features featuremgmt.FeatureToggles, registerer prometheus.Registerer, ) (contracts.DataKeyStorage, error) { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || - !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { - return &encryptionStoreImpl{}, nil - } - store := &encryptionStoreImpl{ db: db, dialect: sqltemplate.DialectForDriver(db.DriverName()), tracer: tracer, - log: log.New("encryption.store"), metrics: NewDataKeyMetrics(registerer), } diff --git a/pkg/storage/secret/encryption/data_key_store_test.go b/pkg/storage/secret/encryption/data_key_store_test.go index 4f2d52d3387..134be881231 100644 --- a/pkg/storage/secret/encryption/data_key_store_test.go +++ b/pkg/storage/secret/encryption/data_key_store_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/migrator" @@ -30,8 +29,7 @@ func TestEncryptionStoreImpl_DataKeyLifecycle(t *testing.T) { // Initialize data key storage with a fake db testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - store, err := ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil) + store, err := ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, nil) require.NoError(t, err) ctx := context.Background() diff --git a/pkg/storage/secret/encryption/encrypted_value_model.go b/pkg/storage/secret/encryption/encrypted_value_model.go index 42e3fb324b6..eb66247c021 100644 --- a/pkg/storage/secret/encryption/encrypted_value_model.go +++ b/pkg/storage/secret/encryption/encrypted_value_model.go @@ -3,8 +3,9 @@ package encryption import "github.com/grafana/grafana/pkg/storage/secret/migrator" type EncryptedValue struct { - UID string Namespace string + Name string + Version int64 EncryptedData []byte Created int64 Updated int64 diff --git a/pkg/storage/secret/encryption/encrypted_value_store.go b/pkg/storage/secret/encryption/encrypted_value_store.go index a369a7a74f5..cd298591b31 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store.go +++ b/pkg/storage/secret/encryption/encrypted_value_store.go @@ -6,29 +6,24 @@ import ( "fmt" "time" - "github.com/google/uuid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) var ( - ErrEncryptedValueNotFound = errors.New("encrypted value not found") + ErrEncryptedValueNotFound = errors.New("encrypted value not found") + ErrEncryptedValueAlreadyExists = errors.New("encrypted value alredy exists") + ErrUnexpectedNumberOfRowsAffected = errors.New("unexpected number of rows modified by query") ) func ProvideEncryptedValueStorage( db contracts.Database, tracer trace.Tracer, - features featuremgmt.FeatureToggles, ) (contracts.EncryptedValueStorage, error) { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || - !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { - return &encryptedValStorage{}, nil - } - return &encryptedValStorage{ db: db, dialect: sqltemplate.DialectForDriver(db.DriverName()), @@ -42,7 +37,7 @@ type encryptedValStorage struct { tracer trace.Tracer } -func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encryptedData []byte) (ev *contracts.EncryptedValue, err error) { +func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes( attribute.String("namespace", namespace), )) @@ -50,14 +45,20 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encr defer func() { if ev != nil { - span.SetAttributes(attribute.String("uid", ev.UID)) + span.SetAttributes( + attribute.String("namespace", ev.Namespace), + attribute.String("name", ev.Name), + attribute.Int64("version", ev.Version), + ) } }() createdTime := time.Now().Unix() + encryptedValue := &EncryptedValue{ - UID: uuid.New().String(), Namespace: namespace, + Name: name, + Version: version, EncryptedData: encryptedData, Created: createdTime, Updated: createdTime, @@ -74,6 +75,9 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encr res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) if err != nil { + if sql.IsRowAlreadyExistsError(err) { + return nil, ErrEncryptedValueAlreadyExists + } return nil, fmt.Errorf("inserting row: %w", err) } @@ -84,25 +88,28 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encr } return &contracts.EncryptedValue{ - UID: encryptedValue.UID, Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, EncryptedData: encryptedValue.EncryptedData, Created: encryptedValue.Created, Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Update(ctx context.Context, namespace string, uid string, encryptedData []byte) error { +func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes( - attribute.String("uid", uid), attribute.String("namespace", namespace), + attribute.String("name", name), + attribute.Int64("version", version), )) defer span.End() req := updateEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace, - UID: uid, + Name: name, + Version: version, EncryptedData: encryptedData, Updated: time.Now().Unix(), } @@ -120,23 +127,25 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace string, uid if rowsAffected, err := res.RowsAffected(); err != nil { return fmt.Errorf("getting rows affected: %w", err) } else if rowsAffected != 1 { - return fmt.Errorf("expected 1 row affected, got %d on %s", rowsAffected, namespace) + return fmt.Errorf("expected 1 row affected, got %d on %s: %w", rowsAffected, namespace, ErrUnexpectedNumberOfRowsAffected) } return nil } -func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid string) (*contracts.EncryptedValue, error) { +func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes( - attribute.String("uid", uid), attribute.String("namespace", namespace), + attribute.String("name", name), + attribute.Int64("version", version), )) defer span.End() req := &readEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace, - UID: uid, + Name: name, + Version: version, } query, err := sqltemplate.Execute(sqlEncryptedValueRead, req) if err != nil { @@ -154,7 +163,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid str } var encryptedValue EncryptedValue - err = rows.Scan(&encryptedValue.UID, &encryptedValue.Namespace, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated) + err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated) if err != nil { return nil, fmt.Errorf("failed to scan encrypted value row: %w", err) } @@ -163,25 +172,28 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid str } return &contracts.EncryptedValue{ - UID: encryptedValue.UID, Namespace: encryptedValue.Namespace, + Name: encryptedValue.Name, + Version: encryptedValue.Version, EncryptedData: encryptedValue.EncryptedData, Created: encryptedValue.Created, Updated: encryptedValue.Updated, }, nil } -func (s *encryptedValStorage) Delete(ctx context.Context, namespace string, uid string) error { +func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error { ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes( - attribute.String("uid", uid), attribute.String("namespace", namespace), + attribute.String("name", name), + attribute.Int64("version", version), )) defer span.End() req := deleteEncryptedValue{ SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace, - UID: uid, + Name: name, + Version: version, } query, err := sqltemplate.Execute(sqlEncryptedValueDelete, req) if err != nil { diff --git a/pkg/storage/secret/encryption/encrypted_value_store_test.go b/pkg/storage/secret/encryption/encrypted_value_store_test.go index 92afe58e7e2..18b67a651fb 100644 --- a/pkg/storage/secret/encryption/encrypted_value_store_test.go +++ b/pkg/storage/secret/encryption/encrypted_value_store_test.go @@ -1,46 +1,47 @@ -package encryption +package encryption_test import ( - "context" + "errors" + "slices" "testing" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/secret/database" - "github.com/grafana/grafana/pkg/storage/secret/migrator" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/storage/secret/encryption" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" + "pgregory.net/rapid" ) func TestEncryptedValueStoreImpl(t *testing.T) { - // Initialize data key storage with a fake db - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - database := database.ProvideDatabase(testDB, tracer) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - ctx := context.Background() - - store, err := ProvideEncryptedValueStorage(database, tracer, features) - require.NoError(t, err) + t.Parallel() t.Run("creating an encrypted value returns it", func(t *testing.T) { - createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) + t.Parallel() + + sut := testutils.Setup(t) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) require.NoError(t, err) - require.NotEmpty(t, createdEV.UID) + require.NotEmpty(t, createdEV.Namespace) + require.NotEmpty(t, createdEV.Name) require.NotEmpty(t, createdEV.Created) require.NotEmpty(t, createdEV.Updated) require.NotEmpty(t, createdEV.EncryptedData) require.Equal(t, "test-namespace", createdEV.Namespace) + require.Equal(t, "test-name", createdEV.Name) }) t.Run("get an existent encrypted value returns it", func(t *testing.T) { - createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) + t.Parallel() + + sut := testutils.Setup(t) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) require.NoError(t, err) - obtainedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) require.NoError(t, err) - require.Equal(t, createdEV.UID, obtainedEV.UID) + require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) + require.Equal(t, createdEV.Name, obtainedEV.Name) require.Equal(t, createdEV.Created, obtainedEV.Created) require.Equal(t, createdEV.Updated, obtainedEV.Updated) require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData) @@ -48,10 +49,13 @@ func TestEncryptedValueStoreImpl(t *testing.T) { }) t.Run("get an existent encrypted value with a different namespace returns error", func(t *testing.T) { - createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) + t.Parallel() + + sut := testutils.Setup(t) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "ns1", "test-name", 1, []byte("test-data")) require.NoError(t, err) - obtainedEV, err := store.Get(ctx, "other-test-namespace", createdEV.UID) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "ns2", createdEV.Name, createdEV.Version) require.Error(t, err) require.Equal(t, "encrypted value not found", err.Error()) @@ -59,20 +63,26 @@ func TestEncryptedValueStoreImpl(t *testing.T) { }) t.Run("get a non existent encrypted value returns error", func(t *testing.T) { - obtainedEV, err := store.Get(ctx, "test-namespace", "test-uid") + t.Parallel() + + sut := testutils.Setup(t) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), "test-namespace", "test-name", 1) require.Error(t, err) require.Equal(t, "encrypted value not found", err.Error()) require.Nil(t, obtainedEV) }) t.Run("updating an existing encrypted value returns no error", func(t *testing.T) { - createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) + t.Parallel() + + sut := testutils.Setup(t) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("test-data")) require.NoError(t, err) - err = store.Update(ctx, "test-namespace", createdEV.UID, []byte("test-data-updated")) + err = sut.EncryptedValueStorage.Update(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version, []byte("test-data-updated")) require.NoError(t, err) - updatedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) + updatedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) require.NoError(t, err) require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData) @@ -81,27 +91,192 @@ func TestEncryptedValueStoreImpl(t *testing.T) { }) t.Run("updating a non existing encrypted value returns error", func(t *testing.T) { - err := store.Update(ctx, "test-namespace", "test-uid", []byte("test-data")) + t.Parallel() + + sut := testutils.Setup(t) + err := sut.EncryptedValueStorage.Update(t.Context(), "test-namespace", "test-uid", 1, []byte("test-data")) require.Error(t, err) }) t.Run("delete an existing encrypted value returns error", func(t *testing.T) { - createdEV, err := store.Create(ctx, "test-namespace", []byte("ttttest-data")) + t.Parallel() + + sut := testutils.Setup(t) + createdEV, err := sut.EncryptedValueStorage.Create(t.Context(), "test-namespace", "test-name", 1, []byte("ttttest-data")) require.NoError(t, err) - obtainedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) + _, err = sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) require.NoError(t, err) - err = store.Delete(ctx, "test-namespace", obtainedEV.UID) + err = sut.EncryptedValueStorage.Delete(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) require.NoError(t, err) - obtainedEV, err = store.Get(ctx, "test-namespace", createdEV.UID) + obtainedEV, err := sut.EncryptedValueStorage.Get(t.Context(), createdEV.Namespace, createdEV.Name, createdEV.Version) require.Error(t, err) require.Nil(t, obtainedEV) }) t.Run("delete a non existing encrypted value does not return error", func(t *testing.T) { - err := store.Delete(ctx, "test-namespace", "test-uid") + t.Parallel() + + sut := testutils.Setup(t) + err := sut.EncryptedValueStorage.Delete(t.Context(), "test-namespace", "test-name", 1) require.NoError(t, err) }) } + +func TestStateMachine(t *testing.T) { + t.Parallel() + + tt := t + + rapid.Check(t, func(t *rapid.T) { + sut := testutils.Setup(tt) + m := newModel() + + t.Repeat(map[string]func(*rapid.T){ + "create": func(t *rapid.T) { + ns := namespaceGen.Draw(t, "ns") + name := nameGen.Draw(t, "name") + version := versionGen.Draw(t, "version") + plaintext := rapid.String().Draw(t, "plaintext") + + _, modelErr := m.create(ns, name, version, []byte(plaintext)) + _, err := sut.EncryptedValueStorage.Create(t.Context(), ns, name, version, []byte(plaintext)) + if modelErr != nil || err != nil { + require.ErrorIs(t, err, modelErr) + return + } + }, + "update": func(t *rapid.T) { + ns := namespaceGen.Draw(t, "ns") + name := nameGen.Draw(t, "name") + version := versionGen.Draw(t, "version") + plaintext := rapid.String().Draw(t, "plaintext") + + modelErr := m.update(ns, name, version, []byte(plaintext)) + err := sut.EncryptedValueStorage.Update(t.Context(), ns, name, version, []byte(plaintext)) + if modelErr != nil || err != nil { + require.ErrorIs(t, err, modelErr) + return + } + }, + "get": func(t *rapid.T) { + ns := namespaceGen.Draw(t, "ns") + name := nameGen.Draw(t, "name") + version := versionGen.Draw(t, "version") + + modelValue, modelErr := m.get(ns, name, version) + value, err := sut.EncryptedValueStorage.Get(t.Context(), ns, name, version) + if modelErr != nil || err != nil { + require.ErrorIs(t, err, modelErr) + return + } + // Do not compare timestamps because the model doesn't model them. + require.Equal(t, modelValue.Namespace, value.Namespace) + require.Equal(t, modelValue.Name, value.Name) + require.Equal(t, modelValue.EncryptedData, value.EncryptedData) + require.Equal(t, modelValue.Version, value.Version) + }, + "delete": func(t *rapid.T) { + ns := namespaceGen.Draw(t, "ns") + name := nameGen.Draw(t, "name") + version := versionGen.Draw(t, "version") + + modelErr := m.delete(ns, name, version) + err := sut.EncryptedValueStorage.Delete(t.Context(), ns, name, version) + if modelErr != nil || err != nil { + require.ErrorIs(t, err, modelErr) + return + } + }, + }) + }) +} + +var ( + namespaceGen = rapid.Custom(func(t *rapid.T) string { + return rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}).Draw(t, "namespace") + }) + nameGen = rapid.Custom(func(t *rapid.T) string { + return rapid.SampledFrom([]string{"name1", "name2", "name3", "name4", "name5"}).Draw(t, "name") + }) + versionGen = rapid.Custom(func(t *rapid.T) int64 { + return rapid.Int64Range(1, 5).Draw(t, "version") + }) +) + +// A simplified model of the encrypted value storage +type model struct { + entries []*entry +} + +type entry struct { + namespace string + name string + version int64 + encryptedData []byte +} + +func newModel() *model { + return &model{} +} + +func (m *model) create(namespace, name string, version int64, encryptedData []byte) (*contracts.EncryptedValue, error) { + v, err := m.get(namespace, name, version) + if err != nil && !errors.Is(err, encryption.ErrEncryptedValueNotFound) { + return nil, err + } + // The entry being creted already exists + if v != nil { + return nil, encryption.ErrEncryptedValueAlreadyExists + } + + m.entries = append(m.entries, &entry{ + namespace: namespace, + name: name, + version: version, + encryptedData: encryptedData, + }) + return &contracts.EncryptedValue{ + Namespace: namespace, + Name: name, + Version: version, + EncryptedData: encryptedData, + Created: 1, + Updated: 1, + }, nil +} +func (m *model) update(namespace, name string, version int64, encryptedData []byte) error { + for _, v := range m.entries { + if v.namespace == namespace && v.name == name && v.version == version { + v.encryptedData = encryptedData + return nil + } + } + + return encryption.ErrUnexpectedNumberOfRowsAffected +} + +func (m *model) get(namespace, name string, version int64) (*contracts.EncryptedValue, error) { + for _, v := range m.entries { + if v.namespace == namespace && v.name == name && v.version == version { + return &contracts.EncryptedValue{ + Namespace: namespace, + Name: name, + Version: version, + EncryptedData: v.encryptedData, + Created: 1, + Updated: 1, + }, nil + } + } + + return nil, encryption.ErrEncryptedValueNotFound +} +func (m *model) delete(namespace, name string, version int64) error { + m.entries = slices.DeleteFunc(m.entries, func(v *entry) bool { + return v.namespace == namespace && v.name == name && v.version == version + }) + return nil +} diff --git a/pkg/storage/secret/encryption/query.go b/pkg/storage/secret/encryption/query.go index 1e6a8b13609..25f21cb77ba 100644 --- a/pkg/storage/secret/encryption/query.go +++ b/pkg/storage/secret/encryption/query.go @@ -55,7 +55,8 @@ func (r createEncryptedValue) Validate() error { type readEncryptedValue struct { sqltemplate.SQLTemplate Namespace string - UID string + Name string + Version int64 } // Validate is only used if we use `dbutil` from `unifiedstorage` @@ -67,7 +68,8 @@ func (r readEncryptedValue) Validate() error { type updateEncryptedValue struct { sqltemplate.SQLTemplate Namespace string - UID string + Name string + Version int64 EncryptedData []byte Updated int64 } @@ -81,7 +83,8 @@ func (r updateEncryptedValue) Validate() error { type deleteEncryptedValue struct { sqltemplate.SQLTemplate Namespace string - UID string + Name string + Version int64 } // Validate is only used if we use `dbutil` from `unifiedstorage` diff --git a/pkg/storage/secret/encryption/query_test.go b/pkg/storage/secret/encryption/query_test.go index 371d5bfc887..51e078d8e25 100644 --- a/pkg/storage/secret/encryption/query_test.go +++ b/pkg/storage/secret/encryption/query_test.go @@ -20,7 +20,8 @@ func TestEncryptedValueQueries(t *testing.T) { SQLTemplate: mocks.NewTestingSQLTemplate(), Row: &EncryptedValue{ Namespace: "ns", - UID: "abc123", + Name: "n1", + Version: 1, EncryptedData: []byte("secret"), Created: 1234, Updated: 5678, @@ -34,7 +35,8 @@ func TestEncryptedValueQueries(t *testing.T) { Data: &readEncryptedValue{ SQLTemplate: mocks.NewTestingSQLTemplate(), Namespace: "ns", - UID: "abc123", + Name: "n1", + Version: 1, }, }, }, @@ -44,7 +46,8 @@ func TestEncryptedValueQueries(t *testing.T) { Data: &updateEncryptedValue{ SQLTemplate: mocks.NewTestingSQLTemplate(), Namespace: "ns", - UID: "abc123", + Name: "n1", + Version: 1, EncryptedData: []byte("secret"), Updated: 5679, }, @@ -56,7 +59,8 @@ func TestEncryptedValueQueries(t *testing.T) { Data: &deleteEncryptedValue{ SQLTemplate: mocks.NewTestingSQLTemplate(), Namespace: "ns", - UID: "abc123", + Name: "n1", + Version: 1, }, }, }, diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql index 079496061ca..7a886b1c6ee 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_create-create.sql @@ -1,12 +1,14 @@ INSERT INTO `secret_encrypted_value` ( - `uid`, `namespace`, + `name`, + `version`, `encrypted_data`, `created`, `updated` ) VALUES ( - 'abc123', 'ns', + 'n1', + 1, '[115 101 99 114 101 116]', 1234, 5678 diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_delete-delete.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_delete-delete.sql index fb492bcd0c9..6c1dc38ac79 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_delete-delete.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_delete-delete.sql @@ -1,4 +1,6 @@ DELETE FROM `secret_encrypted_value` -WHERE `namespace` = 'ns' AND - `uid` = 'abc123' +WHERE + `namespace` = 'ns' AND + `name` = 'n1' AND + `version` = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql index 70ce871fe47..bb9faddb8ff 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_read-read.sql @@ -1,11 +1,14 @@ SELECT - `uid`, `namespace`, + `name`, + `version`, `encrypted_data`, `created`, `updated` FROM `secret_encrypted_value` -WHERE `namespace` = 'ns' AND - `uid` = 'abc123' +WHERE + `namespace` = 'ns' AND + `name` = 'n1' AND + `version` = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql index c54acd9a2e7..cf3d6897a64 100755 --- a/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/mysql--encrypted_value_update-update.sql @@ -3,6 +3,8 @@ UPDATE SET `encrypted_data` = '[115 101 99 114 101 116]', `updated` = 5679 -WHERE `namespace` = 'ns' AND - `uid` = 'abc123' +WHERE + `namespace` = 'ns' AND + `name` = 'n1' AND + `version` = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql index 1c8a8934e88..23d9a0f2331 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_create-create.sql @@ -1,12 +1,14 @@ INSERT INTO "secret_encrypted_value" ( - "uid", "namespace", + "name", + "version", "encrypted_data", "created", "updated" ) VALUES ( - 'abc123', 'ns', + 'n1', + 1, '[115 101 99 114 101 116]', 1234, 5678 diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_delete-delete.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_delete-delete.sql index 380e44b1d68..98a7a677f94 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_delete-delete.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_delete-delete.sql @@ -1,4 +1,6 @@ DELETE FROM "secret_encrypted_value" -WHERE "namespace" = 'ns' AND - "uid" = 'abc123' +WHERE + "namespace" = 'ns' AND + "name" = 'n1' AND + "version" = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql index 77d325ac85a..4bbe1000ce6 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_read-read.sql @@ -1,11 +1,14 @@ SELECT - "uid", "namespace", + "name", + "version", "encrypted_data", "created", "updated" FROM "secret_encrypted_value" -WHERE "namespace" = 'ns' AND - "uid" = 'abc123' +WHERE + "namespace" = 'ns' AND + "name" = 'n1' AND + "version" = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql index 8db8bb3aa6a..b2b5e30ecac 100755 --- a/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/postgres--encrypted_value_update-update.sql @@ -3,6 +3,8 @@ UPDATE SET "encrypted_data" = '[115 101 99 114 101 116]', "updated" = 5679 -WHERE "namespace" = 'ns' AND - "uid" = 'abc123' +WHERE + "namespace" = 'ns' AND + "name" = 'n1' AND + "version" = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql index 1c8a8934e88..23d9a0f2331 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_create-create.sql @@ -1,12 +1,14 @@ INSERT INTO "secret_encrypted_value" ( - "uid", "namespace", + "name", + "version", "encrypted_data", "created", "updated" ) VALUES ( - 'abc123', 'ns', + 'n1', + 1, '[115 101 99 114 101 116]', 1234, 5678 diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_delete-delete.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_delete-delete.sql index 380e44b1d68..98a7a677f94 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_delete-delete.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_delete-delete.sql @@ -1,4 +1,6 @@ DELETE FROM "secret_encrypted_value" -WHERE "namespace" = 'ns' AND - "uid" = 'abc123' +WHERE + "namespace" = 'ns' AND + "name" = 'n1' AND + "version" = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql index 77d325ac85a..4bbe1000ce6 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_read-read.sql @@ -1,11 +1,14 @@ SELECT - "uid", "namespace", + "name", + "version", "encrypted_data", "created", "updated" FROM "secret_encrypted_value" -WHERE "namespace" = 'ns' AND - "uid" = 'abc123' +WHERE + "namespace" = 'ns' AND + "name" = 'n1' AND + "version" = 1 ; diff --git a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql index 8db8bb3aa6a..b2b5e30ecac 100755 --- a/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql +++ b/pkg/storage/secret/encryption/testdata/sqlite--encrypted_value_update-update.sql @@ -3,6 +3,8 @@ UPDATE SET "encrypted_data" = '[115 101 99 114 101 116]', "updated" = 5679 -WHERE "namespace" = 'ns' AND - "uid" = 'abc123' +WHERE + "namespace" = 'ns' AND + "name" = 'n1' AND + "version" = 1 ; diff --git a/pkg/storage/secret/metadata/data/secure_value_create.sql b/pkg/storage/secret/metadata/data/secure_value_create.sql index ae03dd2405e..a120feaa5e5 100644 --- a/pkg/storage/secret/metadata/data/secure_value_create.sql +++ b/pkg/storage/secret/metadata/data/secure_value_create.sql @@ -8,10 +8,8 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ .Ident "created_by" }}, {{ .Ident "updated" }}, {{ .Ident "updated_by" }}, - {{ .Ident "status_phase" }}, - {{ if .Row.Message.Valid }} - {{ .Ident "status_message" }}, - {{ end }} + {{ .Ident "active" }}, + {{ .Ident "version" }}, {{ .Ident "description" }}, {{ if .Row.Keeper.Valid }} {{ .Ident "keeper" }}, @@ -33,10 +31,8 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ .Arg .Row.CreatedBy }}, {{ .Arg .Row.Updated }}, {{ .Arg .Row.UpdatedBy }}, - {{ .Arg .Row.Phase }}, - {{ if .Row.Message.Valid }} - {{ .Arg .Row.Message.String }}, - {{ end }} + {{ .Arg .Row.Active }}, + {{ .Arg .Row.Version }}, {{ .Arg .Row.Description }}, {{ if .Row.Keeper.Valid }} {{ .Arg .Row.Keeper.String }}, @@ -48,4 +44,4 @@ INSERT INTO {{ .Ident "secret_secure_value" }} ( {{ .Arg .Row.Ref.String }}, {{ end }} {{ .Arg .Row.ExternalID }} -); +); \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_delete.sql b/pkg/storage/secret/metadata/data/secure_value_delete.sql deleted file mode 100644 index 2714510c291..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql b/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql new file mode 100644 index 00000000000..90319d469eb --- /dev/null +++ b/pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql @@ -0,0 +1,10 @@ +SELECT + {{ .Ident "version" }} +FROM + {{ .Ident "secret_secure_value" }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} +ORDER BY {{ .Ident "version" }} DESC +LIMIT 1 +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_list.sql b/pkg/storage/secret/metadata/data/secure_value_list.sql index db19a729ebf..9c0c604b72d 100644 --- a/pkg/storage/secret/metadata/data/secure_value_list.sql +++ b/pkg/storage/secret/metadata/data/secure_value_list.sql @@ -8,15 +8,17 @@ SELECT {{ .Ident "created_by" }}, {{ .Ident "updated" }}, {{ .Ident "updated_by" }}, - {{ .Ident "status_phase" }}, - {{ .Ident "status_message" }}, {{ .Ident "description" }}, {{ .Ident "keeper" }}, {{ .Ident "decrypters" }}, {{ .Ident "ref" }}, - {{ .Ident "external_id" }} + {{ .Ident "external_id" }}, + {{ .Ident "version" }}, + {{ .Ident "active" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "active" }} = true ORDER BY {{ .Ident "updated" }} DESC -; +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_listByName.sql b/pkg/storage/secret/metadata/data/secure_value_listByName.sql index b8158d3c0ff..7a5945e4fde 100644 --- a/pkg/storage/secret/metadata/data/secure_value_listByName.sql +++ b/pkg/storage/secret/metadata/data/secure_value_listByName.sql @@ -5,7 +5,9 @@ SELECT {{ .Ident "keeper" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }}) +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }}) AND + {{ .Ident "active" }} = true {{ .SelectFor "UPDATE" }} ; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_append.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_append.sql deleted file mode 100644 index e541cec3140..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_append.sql +++ /dev/null @@ -1,33 +0,0 @@ -INSERT INTO {{ .Ident "secret_secure_value_outbox" }} ( - {{ .Ident "request_id" }}, - {{ .Ident "message_type" }}, - {{ .Ident "name" }}, - {{ .Ident "namespace" }}, -{{ if .Row.EncryptedSecret.Valid }} - {{ .Ident "encrypted_secret" }}, -{{ end }} -{{ if .Row.KeeperName.Valid }} - {{ .Ident "keeper_name" }}, -{{ end }} -{{ if .Row.ExternalID.Valid }} - {{ .Ident "external_id" }}, -{{ end }} - {{ .Ident "receive_count" }}, - {{ .Ident "created" }} -) VALUES ( - {{ .Arg .Row.RequestID }}, - {{ .Arg .Row.MessageType }}, - {{ .Arg .Row.Name }}, - {{ .Arg .Row.Namespace }}, -{{ if .Row.EncryptedSecret.Valid }} - {{ .Arg .Row.EncryptedSecret.String }}, -{{ end }} -{{ if .Row.KeeperName.Valid }} - {{ .Arg .Row.KeeperName.String }}, -{{ end }} -{{ if .Row.ExternalID.Valid }} - {{ .Arg .Row.ExternalID.String }}, -{{ end }} - {{ .Arg .Row.ReceiveCount }}, - {{ .Arg .Row.Created }} -); diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql deleted file mode 100644 index d23f3808f9c..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - {{ .Ident "secret_secure_value_outbox" }} -WHERE - {{ .Ident "id" }} = {{ .Arg .MessageID }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql deleted file mode 100644 index 2c53c2223fd..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - {{ .Ident "id" }} -FROM {{ .Ident "secret_secure_value_outbox" }} -ORDER BY id ASC -LIMIT {{ .Arg .ReceiveLimit }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql deleted file mode 100644 index 8e525ca9009..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - {{ .Ident "created" }}, - {{ .Ident "message_type" }} -FROM - {{ .Ident "secret_secure_value_outbox" }} -WHERE - {{ .Ident "id" }} = {{ .Arg .MessageID }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql deleted file mode 100644 index 254cb3d3f3d..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT - {{ .Ident "request_id" }}, - {{ .Ident "id" }}, - {{ .Ident "message_type" }}, - {{ .Ident "name" }}, - {{ .Ident "namespace" }}, - {{ .Ident "encrypted_secret" }}, - {{ .Ident "keeper_name" }}, - {{ .Ident "external_id" }}, - {{ .Ident "receive_count" }}, - {{ .Ident "created" }} -FROM - {{ .Ident "secret_secure_value_outbox" }} -WHERE - {{ .Ident "id" }} IN ({{ .ArgList .MessageIDs }}) -ORDER BY - {{ .Ident "id" }} ASC -{{ .SelectFor "UPDATE SKIP LOCKED" }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql b/pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql deleted file mode 100644 index 610f29695a8..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - {{ .Ident "secret_secure_value_outbox" }} -SET - {{ .Ident "receive_count" }} = {{ .Ident "receive_count" }} + 1 -WHERE - {{ .Ident "id" }} IN ({{ .ArgList .MessageIDs }}) -; diff --git a/pkg/storage/secret/metadata/data/secure_value_read.sql b/pkg/storage/secret/metadata/data/secure_value_read.sql index 3453abb08e4..b90d54b4a5f 100644 --- a/pkg/storage/secret/metadata/data/secure_value_read.sql +++ b/pkg/storage/secret/metadata/data/secure_value_read.sql @@ -8,18 +8,20 @@ SELECT {{ .Ident "created_by" }}, {{ .Ident "updated" }}, {{ .Ident "updated_by" }}, - {{ .Ident "status_phase" }}, - {{ .Ident "status_message" }}, {{ .Ident "description" }}, {{ .Ident "keeper" }}, {{ .Ident "decrypters" }}, {{ .Ident "ref" }}, - {{ .Ident "external_id" }} + {{ .Ident "external_id" }}, + {{ .Ident "active" }}, + {{ .Ident "version" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "active" }} = true {{ if .IsForUpdate }} {{ .SelectFor "UPDATE" }} {{ end }} -; +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql b/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql index 14080fa0507..975c00d9f3b 100644 --- a/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql +++ b/pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql @@ -2,9 +2,12 @@ SELECT {{ .Ident "keeper" }}, {{ .Ident "decrypters" }}, {{ .Ident "ref" }}, - {{ .Ident "external_id" }} + {{ .Ident "external_id" }}, + {{ .Ident "active" }} FROM {{ .Ident "secret_secure_value" }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "active" }} = true +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql b/pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql new file mode 100644 index 00000000000..bf069441ac3 --- /dev/null +++ b/pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql @@ -0,0 +1,8 @@ +UPDATE + {{ .Ident "secret_secure_value" }} +SET + {{ .Ident "active" }} = ({{ .Ident "version" }} = {{ .Arg .Version}}) +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql b/pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql new file mode 100644 index 00000000000..608bb813c4f --- /dev/null +++ b/pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql @@ -0,0 +1,9 @@ +UPDATE + {{ .Ident "secret_secure_value" }} +SET + {{ .Ident "active" }} = false +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_update.sql b/pkg/storage/secret/metadata/data/secure_value_update.sql deleted file mode 100644 index 90fc4946611..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_update.sql +++ /dev/null @@ -1,30 +0,0 @@ -UPDATE - {{ .Ident "secret_secure_value" }} -SET - {{ .Ident "guid" }} = {{ .Arg .Row.GUID }}, - {{ .Ident "name" }} = {{ .Arg .Row.Name }}, - {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }}, - {{ .Ident "annotations" }} = {{ .Arg .Row.Annotations }}, - {{ .Ident "labels" }} = {{ .Arg .Row.Labels }}, - {{ .Ident "created" }} = {{ .Arg .Row.Created }}, - {{ .Ident "created_by" }} = {{ .Arg .Row.CreatedBy }}, - {{ .Ident "updated" }} = {{ .Arg .Row.Updated }}, - {{ .Ident "updated_by" }} = {{ .Arg .Row.UpdatedBy }}, - {{ .Ident "status_phase" }} = {{ .Arg .Row.Phase }}, - {{ if .Row.Message.Valid }} - {{ .Ident "status_message" }} = {{ .Arg .Row.Message.String }}, - {{ end }} - {{ .Ident "description" }} = {{ .Arg .Row.Description }}, - {{ if .Row.Keeper.Valid }} - {{ .Ident "keeper" }} = {{ .Arg .Row.Keeper.String }}, - {{ end }} - {{ if .Row.Decrypters.Valid }} - {{ .Ident "decrypters" }} = {{ .Arg .Row.Decrypters.String }}, - {{ end }} - {{ if .Row.Ref.Valid }} - {{ .Ident "ref" }} = {{ .Arg .Row.Ref.String }}, - {{ end }} - {{ .Ident "external_id" }} = {{ .Arg .Row.ExternalID }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Row.Name }} -; diff --git a/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql b/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql index 9b2927802e6..fb891badaf5 100644 --- a/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql +++ b/pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE {{ .Ident "secret_secure_value" }} SET {{ .Ident "external_id" }} = {{ .Arg .ExternalID }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; +WHERE + {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND + {{ .Ident "name" }} = {{ .Arg .Name }} AND + {{ .Ident "version" }} = {{ .Arg .Version }} +; \ No newline at end of file diff --git a/pkg/storage/secret/metadata/data/secure_value_updateStatus.sql b/pkg/storage/secret/metadata/data/secure_value_updateStatus.sql deleted file mode 100644 index dfba1960ce0..00000000000 --- a/pkg/storage/secret/metadata/data/secure_value_updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - {{ .Ident "secret_secure_value" }} -SET - {{ .Ident "status_phase" }} = {{ .Arg .Phase }}, - {{ .Ident "status_message" }} = {{ .Arg .Message }} -WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND - {{ .Ident "name" }} = {{ .Arg .Name }} -; diff --git a/pkg/storage/secret/metadata/decrypt_store.go b/pkg/storage/secret/metadata/decrypt_store.go new file mode 100644 index 00000000000..c1b402b2224 --- /dev/null +++ b/pkg/storage/secret/metadata/decrypt_store.go @@ -0,0 +1,115 @@ +package metadata + +import ( + "context" + "fmt" + "strconv" + "time" + + claims "github.com/grafana/authlib/types" + "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-app-sdk/logging" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" +) + +func ProvideDecryptStorage( + tracer trace.Tracer, + keeperService contracts.KeeperService, + keeperMetadataStorage contracts.KeeperMetadataStorage, + secureValueMetadataStorage contracts.SecureValueMetadataStorage, + decryptAuthorizer contracts.DecryptAuthorizer, + reg prometheus.Registerer, +) (contracts.DecryptStorage, error) { + if decryptAuthorizer == nil { + return nil, fmt.Errorf("a decrypt authorizer is required") + } + + return &decryptStorage{ + tracer: tracer, + keeperMetadataStorage: keeperMetadataStorage, + keeperService: keeperService, + secureValueMetadataStorage: secureValueMetadataStorage, + decryptAuthorizer: decryptAuthorizer, + metrics: metrics.NewStorageMetrics(reg), + }, nil +} + +// decryptStorage is the actual implementation of the decrypt storage. +type decryptStorage struct { + tracer trace.Tracer + keeperMetadataStorage contracts.KeeperMetadataStorage + keeperService contracts.KeeperService + secureValueMetadataStorage contracts.SecureValueMetadataStorage + decryptAuthorizer contracts.DecryptAuthorizer + metrics *metrics.StorageMetrics +} + +// Decrypt decrypts a secure value from the keeper. +func (s *decryptStorage) Decrypt(ctx context.Context, namespace xkube.Namespace, name string) (_ secretv1beta1.ExposedSecureValue, decryptErr error) { + ctx, span := s.tracer.Start(ctx, "DecryptStorage.Decrypt", trace.WithAttributes( + attribute.String("namespace", namespace.String()), + attribute.String("name", name), + )) + defer span.End() + + var decrypterIdentity string + start := time.Now() + // TEMPORARY: While we evaluate all of our auditing needs, provide one for decrypt operations. + defer func() { + span.SetAttributes(attribute.String("decrypter.identity", decrypterIdentity)) + + if decryptErr == nil { + logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_success", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity) + } else { + span.SetStatus(codes.Error, "Decrypt failed") + span.RecordError(decryptErr) + + logging.FromContext(ctx).Info("Audit log:", "operation", "decrypt_secret_error", "namespace", namespace, "secret_name", name, "decrypter_identity", decrypterIdentity, "error", decryptErr) + } + success := decryptErr == nil + s.metrics.DecryptDuration.WithLabelValues(strconv.FormatBool(success)).Observe(time.Since(start).Seconds()) + s.metrics.DecryptRequestCount.WithLabelValues(strconv.FormatBool(success)).Inc() + }() + + // Basic authn check before reading a secure value metadata, it is here on purpose. + if _, ok := claims.AuthInfoFrom(ctx); !ok { + return "", contracts.ErrDecryptNotAuthorized + } + + // The auth token will not necessarily have the permission to read the secure value metadata, + // but we still need to do it to inspect the `decrypters` field, hence the actual `authorize` + // function call happens after this. + sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{}) + if err != nil { + return "", contracts.ErrDecryptNotFound + } + + decrypterIdentity, authorized := s.decryptAuthorizer.Authorize(ctx, name, sv.Spec.Decrypters) + if !authorized { + return "", contracts.ErrDecryptNotAuthorized + } + + keeperConfig, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, namespace.String(), sv.Spec.Keeper, contracts.ReadOpts{}) + if err != nil { + return "", contracts.ErrDecryptFailed + } + + keeper, err := s.keeperService.KeeperForConfig(keeperConfig) + if err != nil { + return "", contracts.ErrDecryptFailed + } + + exposedValue, err := keeper.Expose(ctx, keeperConfig, namespace.String(), name, sv.Status.Version) + if err != nil { + return "", contracts.ErrDecryptFailed + } + + return exposedValue, nil +} diff --git a/pkg/storage/secret/metadata/decrypt_store_test.go b/pkg/storage/secret/metadata/decrypt_store_test.go new file mode 100644 index 00000000000..08e22e6bc6e --- /dev/null +++ b/pkg/storage/secret/metadata/decrypt_store_test.go @@ -0,0 +1,271 @@ +package metadata_test + +import ( + "context" + "testing" + + "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/types" + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" +) + +func TestIntegrationDecrypt(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + t.Parallel() + + t.Run("when no auth info is present, it returns an error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + sut := testutils.Setup(t) + + exposed, err := sut.DecryptStorage.Decrypt(ctx, "default", "name") + require.Error(t, err) + require.Empty(t, exposed) + }) + + t.Run("when secure value cannot be found, it returns an error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + // Create auth context with proper permissions + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/group1:decrypt"}, "svc", types.TypeUser) + + sut := testutils.Setup(t) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "non-existent-value") + require.ErrorIs(t, err, contracts.ErrDecryptNotFound) + require.Empty(t, exposed) + }) + + t.Run("when happy path with valid auth and permissions, it returns decrypted value", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + svcIdentity := "svc" + + // Create auth context with proper permissions that match the decrypters + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues:decrypt"}, svcIdentity, types.TypeUser) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{svcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = "sv-test" + sv.Namespace = "default" + + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test") + require.NoError(t, err) + require.NotEmpty(t, exposed) + require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue()) + }) + + t.Run("with permissions for a specific secure value but trying to decrypt another one, it returns unauthorized error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + svName := "sv-test" + svcIdentity := "svc" + + // Create auth context with proper permissions that match the decrypters + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/sv-test2:decrypt"}, svcIdentity, types.TypeUser) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{svcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = svName + sv.Namespace = "default" + + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) + require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) + require.Empty(t, exposed) + }) + + t.Run("when permission format is malformed (no verb), it returns unauthorized error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + svcIdentity := "svc" + + // Create auth context with malformed permission (no verb) + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues"}, svcIdentity, types.TypeUser) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{svcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = "sv-test" + sv.Namespace = "default" + + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test") + require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) + require.Empty(t, exposed) + }) + + t.Run("when permission verb is not 'decrypt', it returns unauthorized error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + svName := "sv-test" + svcIdentity := "svc" + + // Create auth context with wrong verb + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/" + svName + ":read"}, svcIdentity, types.TypeUser) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{svcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = svName + sv.Namespace = "default" + + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) + require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) + require.Empty(t, exposed) + }) + + t.Run("when permission has incorrect number of parts, it returns unauthorized error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + svcIdentity := "svc" + + // Create auth context with incorrect number of parts + authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/:decrypt"}, svcIdentity, types.TypeUser) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{svcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = "sv-test" + sv.Namespace = "default" + + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test") + require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) + require.Empty(t, exposed) + }) + + t.Run("when permission has incorrect group or resource, it returns unauthorized error", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + svName := "sv-test" + svcIdentity := "svc" + + // Create auth context with incorrect group + authCtx := createAuthContext(ctx, "default", []string{"wrong.group/securevalues/" + svName + ":decrypt"}, svcIdentity, types.TypeUser) + + // Setup service + sut := testutils.Setup(t) + + // Create a secure value + spec := secretv1beta1.SecureValueSpec{ + Description: "description", + Decrypters: []string{svcIdentity}, + Value: ptr.To(secretv1beta1.NewExposedSecureValue("value")), + } + sv := &secretv1beta1.SecureValue{Spec: spec} + sv.Name = svName + sv.Namespace = "default" + + _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv)) + require.NoError(t, err) + + exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName) + require.Error(t, err) + require.Equal(t, err.Error(), "not authorized") + require.Empty(t, exposed) + }) + + // TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though. +} + +func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context { + requester := &identity.StaticRequester{ + Type: identityType, + Namespace: namespace, + AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: permissions, + ServiceIdentity: svc, + }, + }, + } + + if identityType == types.TypeUser { + requester.UserID = 1 + } + + return types.WithAuthInfo(ctx, requester) +} diff --git a/pkg/storage/secret/metadata/keeper_model.go b/pkg/storage/secret/metadata/keeper_model.go index ae8ef7bb908..8fd43dc35e0 100644 --- a/pkg/storage/secret/metadata/keeper_model.go +++ b/pkg/storage/secret/metadata/keeper_model.go @@ -6,8 +6,8 @@ import ( "time" "github.com/google/uuid" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/migrator" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,7 +37,7 @@ func (*keeperDB) TableName() string { } // toKubernetes maps a DB row into a Kubernetes resource (metadata + spec). -func (kp *keeperDB) toKubernetes() (*secretv0alpha1.Keeper, error) { +func (kp *keeperDB) toKubernetes() (*secretv1beta1.Keeper, error) { annotations := make(map[string]string, 0) if kp.Annotations != "" { if err := json.Unmarshal([]byte(kp.Annotations), &annotations); err != nil { @@ -52,23 +52,23 @@ func (kp *keeperDB) toKubernetes() (*secretv0alpha1.Keeper, error) { } } - resource := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + resource := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: kp.Description, }, } // Obtain provider configs - provider := toProvider(secretv0alpha1.KeeperType(kp.Type), kp.Payload) + provider := toProvider(secretv1beta1.KeeperType(kp.Type), kp.Payload) switch v := provider.(type) { - case *secretv0alpha1.AWSKeeperConfig: - resource.Spec.AWS = v - case *secretv0alpha1.AzureKeeperConfig: + case *secretv1beta1.KeeperAWSConfig: + resource.Spec.Aws = v + case *secretv1beta1.KeeperAzureConfig: resource.Spec.Azure = v - case *secretv0alpha1.GCPKeeperConfig: - resource.Spec.GCP = v - case *secretv0alpha1.HashiCorpKeeperConfig: - resource.Spec.HashiCorp = v + case *secretv1beta1.KeeperGCPConfig: + resource.Spec.Gcp = v + case *secretv1beta1.KeeperHashiCorpConfig: + resource.Spec.HashiCorpVault = v } // Set all meta fields here for consistency. @@ -94,7 +94,7 @@ func (kp *keeperDB) toKubernetes() (*secretv0alpha1.Keeper, error) { } // toKeeperCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted. -func toKeeperCreateRow(kp *secretv0alpha1.Keeper, actorUID string) (*keeperDB, error) { +func toKeeperCreateRow(kp *secretv1beta1.Keeper, actorUID string) (*keeperDB, error) { row, err := toKeeperRow(kp) if err != nil { return nil, fmt.Errorf("failed to map to row: %w", err) @@ -112,7 +112,7 @@ func toKeeperCreateRow(kp *secretv0alpha1.Keeper, actorUID string) (*keeperDB, e } // toKeeperUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated. -func toKeeperUpdateRow(currentRow *keeperDB, newKeeper *secretv0alpha1.Keeper, actorUID string) (*keeperDB, error) { +func toKeeperUpdateRow(currentRow *keeperDB, newKeeper *secretv1beta1.Keeper, actorUID string) (*keeperDB, error) { row, err := toKeeperRow(newKeeper) if err != nil { return nil, fmt.Errorf("failed to map to row: %w", err) @@ -130,7 +130,7 @@ func toKeeperUpdateRow(currentRow *keeperDB, newKeeper *secretv0alpha1.Keeper, a } // toKeeperRow maps a Kubernetes Keeper resource into a Keeper DB row. -func toKeeperRow(kp *secretv0alpha1.Keeper) (*keeperDB, error) { +func toKeeperRow(kp *secretv1beta1.Keeper) (*keeperDB, error) { var annotations string if len(kp.Annotations) > 0 { cleanedAnnotations := xkube.CleanAnnotations(kp.Annotations) @@ -196,19 +196,19 @@ func toKeeperRow(kp *secretv0alpha1.Keeper) (*keeperDB, error) { // toTypeAndPayload obtain keeper type and payload from a Kubernetes Keeper resource. // TODO: Move as method of KeeperSpec -func toTypeAndPayload(kp *secretv0alpha1.Keeper) (secretv0alpha1.KeeperType, string, error) { - if kp.Spec.AWS != nil { - payload, err := json.Marshal(kp.Spec.AWS.AWSCredentials) - return secretv0alpha1.AWSKeeperType, string(payload), err +func toTypeAndPayload(kp *secretv1beta1.Keeper) (secretv1beta1.KeeperType, string, error) { + if kp.Spec.Aws != nil { + payload, err := json.Marshal(kp.Spec.Aws) + return secretv1beta1.AWSKeeperType, string(payload), err } else if kp.Spec.Azure != nil { payload, err := json.Marshal(kp.Spec.Azure) - return secretv0alpha1.AzureKeeperType, string(payload), err - } else if kp.Spec.GCP != nil { - payload, err := json.Marshal(kp.Spec.GCP) - return secretv0alpha1.GCPKeeperType, string(payload), err - } else if kp.Spec.HashiCorp != nil { - payload, err := json.Marshal(kp.Spec.HashiCorp) - return secretv0alpha1.HashiCorpKeeperType, string(payload), err + return secretv1beta1.AzureKeeperType, string(payload), err + } else if kp.Spec.Gcp != nil { + payload, err := json.Marshal(kp.Spec.Gcp) + return secretv1beta1.GCPKeeperType, string(payload), err + } else if kp.Spec.HashiCorpVault != nil { + payload, err := json.Marshal(kp.Spec.HashiCorpVault) + return secretv1beta1.HashiCorpKeeperType, string(payload), err } return "", "", fmt.Errorf("no keeper type found") @@ -216,28 +216,28 @@ func toTypeAndPayload(kp *secretv0alpha1.Keeper) (secretv0alpha1.KeeperType, str // toProvider maps a KeeperType and payload into a provider config struct. // TODO: Move as method of KeeperType -func toProvider(keeperType secretv0alpha1.KeeperType, payload string) secretv0alpha1.KeeperConfig { +func toProvider(keeperType secretv1beta1.KeeperType, payload string) secretv1beta1.KeeperConfig { switch keeperType { - case secretv0alpha1.AWSKeeperType: - aws := &secretv0alpha1.AWSKeeperConfig{} + case secretv1beta1.AWSKeeperType: + aws := &secretv1beta1.KeeperAWSConfig{} if err := json.Unmarshal([]byte(payload), aws); err != nil { return nil } return aws - case secretv0alpha1.AzureKeeperType: - azure := &secretv0alpha1.AzureKeeperConfig{} + case secretv1beta1.AzureKeeperType: + azure := &secretv1beta1.KeeperAzureConfig{} if err := json.Unmarshal([]byte(payload), azure); err != nil { return nil } return azure - case secretv0alpha1.GCPKeeperType: - gcp := &secretv0alpha1.GCPKeeperConfig{} + case secretv1beta1.GCPKeeperType: + gcp := &secretv1beta1.KeeperGCPConfig{} if err := json.Unmarshal([]byte(payload), gcp); err != nil { return nil } return gcp - case secretv0alpha1.HashiCorpKeeperType: - hashicorp := &secretv0alpha1.HashiCorpKeeperConfig{} + case secretv1beta1.HashiCorpKeeperType: + hashicorp := &secretv1beta1.KeeperHashiCorpConfig{} if err := json.Unmarshal([]byte(payload), hashicorp); err != nil { return nil } @@ -248,17 +248,17 @@ func toProvider(keeperType secretv0alpha1.KeeperType, payload string) secretv0al } // extractSecureValues extracts unique securevalues referenced by the keeper, if any. -func extractSecureValues(kp *secretv0alpha1.Keeper) map[string]struct{} { +func extractSecureValues(kp *secretv1beta1.Keeper) map[string]struct{} { switch { - case kp.Spec.AWS != nil: + case kp.Spec.Aws != nil: secureValues := make(map[string]struct{}, 0) - if kp.Spec.AWS.AccessKeyID.SecureValueName != "" { - secureValues[kp.Spec.AWS.AccessKeyID.SecureValueName] = struct{}{} + if kp.Spec.Aws.AccessKeyID.SecureValueName != "" { + secureValues[kp.Spec.Aws.AccessKeyID.SecureValueName] = struct{}{} } - if kp.Spec.AWS.SecretAccessKey.SecureValueName != "" { - secureValues[kp.Spec.AWS.SecretAccessKey.SecureValueName] = struct{}{} + if kp.Spec.Aws.SecretAccessKey.SecureValueName != "" { + secureValues[kp.Spec.Aws.SecretAccessKey.SecureValueName] = struct{}{} } return secureValues @@ -269,12 +269,12 @@ func extractSecureValues(kp *secretv0alpha1.Keeper) map[string]struct{} { } // GCP does not reference secureValues. - case kp.Spec.GCP != nil: + case kp.Spec.Gcp != nil: return nil - case kp.Spec.HashiCorp != nil: - if kp.Spec.HashiCorp.Token.SecureValueName != "" { - return map[string]struct{}{kp.Spec.HashiCorp.Token.SecureValueName: {}} + case kp.Spec.HashiCorpVault != nil: + if kp.Spec.HashiCorpVault.Token.SecureValueName != "" { + return map[string]struct{}{kp.Spec.HashiCorpVault.Token.SecureValueName: {}} } } diff --git a/pkg/storage/secret/metadata/keeper_store.go b/pkg/storage/secret/metadata/keeper_store.go index defc9974125..92707351ba7 100644 --- a/pkg/storage/secret/metadata/keeper_store.go +++ b/pkg/storage/secret/metadata/keeper_store.go @@ -5,16 +5,16 @@ import ( "fmt" "time" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) // keeperMetadataStorage is the actual implementation of the keeper metadata storage. @@ -30,14 +30,8 @@ var _ contracts.KeeperMetadataStorage = (*keeperMetadataStorage)(nil) func ProvideKeeperMetadataStorage( db contracts.Database, tracer trace.Tracer, - features featuremgmt.FeatureToggles, reg prometheus.Registerer, ) (contracts.KeeperMetadataStorage, error) { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || - !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { - return &keeperMetadataStorage{}, nil - } - return &keeperMetadataStorage{ db: db, dialect: sqltemplate.DialectForDriver(db.DriverName()), @@ -46,7 +40,7 @@ func ProvideKeeperMetadataStorage( }, nil } -func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) { +func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv1beta1.Keeper, actorUID string) (*secretv1beta1.Keeper, error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.Create", trace.WithAttributes( attribute.String("name", keeper.GetName()), @@ -111,7 +105,7 @@ func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alph return createdKeeper, nil } -func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.Keeper, error) { +func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv1beta1.Keeper, error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.Read", trace.WithAttributes( attribute.String("name", name), @@ -174,7 +168,7 @@ func (s *keeperMetadataStorage) read(ctx context.Context, namespace, name string return &keeper, nil } -func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) { +func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv1beta1.Keeper, actorUID string) (*secretv1beta1.Keeper, error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.Update", trace.WithAttributes( attribute.String("name", newKeeper.GetName()), @@ -291,7 +285,7 @@ func (s *keeperMetadataStorage) Delete(ctx context.Context, namespace xkube.Name return nil } -func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (keeperList []secretv0alpha1.Keeper, err error) { +func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (keeperList []secretv1beta1.Keeper, err error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.List", trace.WithAttributes( attribute.String("namespace", namespace.String()), @@ -318,7 +312,7 @@ func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namesp } defer func() { _ = rows.Close() }() - keepers := make([]secretv0alpha1.Keeper, 0) + keepers := make([]secretv1beta1.Keeper, 0) for rows.Next() { var row keeperDB @@ -350,7 +344,7 @@ func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namesp // validateSecureValueReferences checks that all secure values referenced by the keeper exist and are not referenced by other third-party keepers. // It is used by other methods inside a transaction. -func (s *keeperMetadataStorage) validateSecureValueReferences(ctx context.Context, keeper *secretv0alpha1.Keeper) (err error) { +func (s *keeperMetadataStorage) validateSecureValueReferences(ctx context.Context, keeper *secretv1beta1.Keeper) (err error) { ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.ValidateSecureValueReferences", trace.WithAttributes( attribute.String("name", keeper.GetName()), attribute.String("namespace", keeper.GetNamespace()), @@ -497,7 +491,7 @@ func (s *keeperMetadataStorage) validateSecureValueReferences(ctx context.Contex return nil } -func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name *string, opts contracts.ReadOpts) (secretv0alpha1.KeeperConfig, error) { +func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name *string, opts contracts.ReadOpts) (secretv1beta1.KeeperConfig, error) { ctx, span := s.tracer.Start(ctx, "KeeperMetadataStorage.GetKeeperConfig", trace.WithAttributes( attribute.String("namespace", namespace), attribute.Bool("isForUpdate", opts.ForUpdate), @@ -506,7 +500,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s // Check if keeper is the systemwide one. if name == nil { - return &secretv0alpha1.SystemKeeperConfig{}, nil + return &secretv1beta1.SystemKeeperConfig{}, nil } start := time.Now() @@ -518,7 +512,7 @@ func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace s return nil, err } - keeperConfig := toProvider(secretv0alpha1.KeeperType(kp.Type), kp.Payload) + keeperConfig := toProvider(secretv1beta1.KeeperType(kp.Type), kp.Payload) s.metrics.KeeperMetadataGetKeeperConfigDuration.Observe(time.Since(start).Seconds()) diff --git a/pkg/storage/secret/metadata/keeper_store_test.go b/pkg/storage/secret/metadata/keeper_store_test.go index 644b3ccf714..f21fbdaa575 100644 --- a/pkg/storage/secret/metadata/keeper_store_test.go +++ b/pkg/storage/secret/metadata/keeper_store_test.go @@ -1,18 +1,20 @@ -package metadata +package metadata_test import ( "context" "testing" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/secret/database" - "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" + "k8s.io/utils/ptr" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/storage/secret/database" + "github.com/grafana/grafana/pkg/storage/secret/metadata" + "github.com/grafana/grafana/pkg/storage/secret/migrator" ) func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { @@ -21,10 +23,10 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { defaultKeeperName := "kp-test" defaultKeeperNS := "default" - testKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + testKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "description", - AWS: &secretv0alpha1.AWSKeeperConfig{}, + Aws: &secretv1beta1.KeeperAWSConfig{}, }, } @@ -40,7 +42,7 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { // get system keeper config keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, nil, contracts.ReadOpts{}) require.NoError(t, err) - require.IsType(t, &secretv0alpha1.SystemKeeperConfig{}, keeperConfig) + require.IsType(t, &secretv1beta1.SystemKeeperConfig{}, keeperConfig) }) t.Run("get test keeper config", func(t *testing.T) { @@ -89,10 +91,10 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { keeperTest := "kp-test2" keeperNamespaceTest := "ns" - testKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + testKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "another description", - AWS: &secretv0alpha1.AWSKeeperConfig{}, + Aws: &secretv1beta1.KeeperAWSConfig{}, }, } testKeeper.Name = keeperTest @@ -127,10 +129,10 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { keeperNamespaceTest := "ns" // Create initial keeper - initialKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + initialKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "initial description", - AWS: &secretv0alpha1.AWSKeeperConfig{}, + Aws: &secretv1beta1.KeeperAWSConfig{}, }, } initialKeeper.Name = keeperTest @@ -146,10 +148,10 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { require.Equal(t, "initial description", keeper.Spec.Description) // Update the keeper with new values - updatedKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + updatedKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "updated description", - AWS: &secretv0alpha1.AWSKeeperConfig{}, + Aws: &secretv1beta1.KeeperAWSConfig{}, }, } updatedKeeper.Name = keeperTest @@ -181,19 +183,17 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { keeperNamespaceTest := "ns" // Create initial keeper with first AWS config - initialKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + initialKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "initial description", - AWS: &secretv0alpha1.AWSKeeperConfig{ - AWSCredentials: secretv0alpha1.AWSCredentials{ - AccessKeyID: secretv0alpha1.CredentialValue{ - ValueFromEnv: "AWS_ACCESS_KEY_ID_1", - }, - SecretAccessKey: secretv0alpha1.CredentialValue{ - ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1", - }, - KMSKeyID: "kms-key-id-1", + Aws: &secretv1beta1.KeeperAWSConfig{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID_1", }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1", + }, + KmsKeyID: ptr.To("kms-key-id-1"), }, }, } @@ -207,24 +207,22 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { // Verify initial AWS config keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) require.NoError(t, err) - require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.AWS.AccessKeyID.ValueFromEnv) - require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.AWS.SecretAccessKey.ValueFromEnv) - require.Equal(t, "kms-key-id-1", keeper.Spec.AWS.KMSKeyID) + require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.Aws.AccessKeyID.ValueFromEnv) + require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.Aws.SecretAccessKey.ValueFromEnv) + require.Equal(t, "kms-key-id-1", *keeper.Spec.Aws.KmsKeyID) // Update with new AWS config - updatedKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + updatedKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "updated description", - AWS: &secretv0alpha1.AWSKeeperConfig{ - AWSCredentials: secretv0alpha1.AWSCredentials{ - AccessKeyID: secretv0alpha1.CredentialValue{ - ValueFromEnv: "AWS_ACCESS_KEY_ID_2", - }, - SecretAccessKey: secretv0alpha1.CredentialValue{ - ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2", - }, - KMSKeyID: "kms-key-id-2", + Aws: &secretv1beta1.KeeperAWSConfig{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID_2", }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2", + }, + KmsKeyID: ptr.To("kms-key-id-2"), }, }, } @@ -238,9 +236,9 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { // Verify updated AWS config updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{}) require.NoError(t, err) - require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.AWS.AccessKeyID.ValueFromEnv) - require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.AWS.SecretAccessKey.ValueFromEnv) - require.Equal(t, "kms-key-id-2", updatedKeeper.Spec.AWS.KMSKeyID) + require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.Aws.AccessKeyID.ValueFromEnv) + require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.Aws.SecretAccessKey.ValueFromEnv) + require.Equal(t, "kms-key-id-2", *updatedKeeper.Spec.Aws.KmsKeyID) }) t.Run("list keepers in empty namespace", func(t *testing.T) { @@ -275,17 +273,15 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { keeperNamespaceTest := "ns1" // Create initial keeper - initialKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + initialKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "initial description", - AWS: &secretv0alpha1.AWSKeeperConfig{ - AWSCredentials: secretv0alpha1.AWSCredentials{ - AccessKeyID: secretv0alpha1.CredentialValue{ - ValueFromEnv: "AWS_ACCESS_KEY_ID", - }, - SecretAccessKey: secretv0alpha1.CredentialValue{ - ValueFromEnv: "AWS_SECRET_ACCESS_KEY", - }, + Aws: &secretv1beta1.KeeperAWSConfig{ + AccessKeyID: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_ACCESS_KEY_ID", + }, + SecretAccessKey: secretv1beta1.KeeperCredentialValue{ + ValueFromEnv: "AWS_SECRET_ACCESS_KEY", }, }, }, @@ -319,10 +315,10 @@ func Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) { ctx := context.Background() keeperMetadataStorage := initStorage(t) - nonExistentKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + nonExistentKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "some description", - AWS: &secretv0alpha1.AWSKeeperConfig{}, + Aws: &secretv1beta1.KeeperAWSConfig{}, }, } nonExistentKeeper.Name = "non-existent" @@ -337,10 +333,9 @@ func initStorage(t *testing.T) contracts.KeeperMetadataStorage { testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) tracer := noop.NewTracerProvider().Tracer("test") db := database.ProvideDatabase(testDB, tracer) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) // Initialize the keeper storage - keeperMetadataStorage, err := ProvideKeeperMetadataStorage(db, tracer, features, nil) + keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(db, tracer, nil) require.NoError(t, err) return keeperMetadataStorage } diff --git a/pkg/storage/secret/metadata/metrics/metrics.go b/pkg/storage/secret/metadata/metrics/metrics.go index bffbaebc604..bedaa03b4f8 100644 --- a/pkg/storage/secret/metadata/metrics/metrics.go +++ b/pkg/storage/secret/metadata/metrics/metrics.go @@ -13,16 +13,6 @@ const ( // StorageMetrics is a struct that contains all the metrics for all operations of secrets storage. type StorageMetrics struct { - OutboxAppendDuration *prometheus.HistogramVec - OutboxReceiveDuration prometheus.Histogram - OutboxAppendCount *prometheus.CounterVec - OutboxReceiveCount prometheus.Counter - OutboxDeleteDuration prometheus.Histogram - OutboxDeleteCount prometheus.Counter - OutboxIncrementReceiveCountDuration prometheus.Histogram - OutboxIncrementReceiveCountCount prometheus.Counter - OutboxTotalMessageLifetimeDuration *prometheus.HistogramVec - KeeperMetadataCreateDuration *prometheus.HistogramVec KeeperMetadataCreateCount *prometheus.CounterVec KeeperMetadataUpdateDuration *prometheus.HistogramVec @@ -55,67 +45,6 @@ type StorageMetrics struct { func newStorageMetrics() *StorageMetrics { return &StorageMetrics{ - // Outbox metrics - OutboxAppendDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_append_duration_seconds", - Help: "Duration of outbox message append operations", - Buckets: prometheus.DefBuckets, - }, []string{"message_type"}), - OutboxAppendCount: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_append_count", - Help: "Count of outbox message append operations", - }, []string{"message_type"}), - OutboxReceiveDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_receive_duration_seconds", - Help: "Duration of outbox message receive operations", - Buckets: prometheus.DefBuckets, - }), - OutboxReceiveCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_receive_count", - Help: "Count of outbox message receive operations", - }), - OutboxDeleteDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_delete_duration_seconds", - Help: "Duration of outbox message delete operations", - Buckets: prometheus.DefBuckets, - }), - OutboxDeleteCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_delete_count", - Help: "Count of outbox message delete operations", - }), - OutboxIncrementReceiveCountDuration: prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_increment_receive_count_duration_seconds", - Help: "Duration of outbox message increment receive count operations", - Buckets: prometheus.DefBuckets, - }), - OutboxIncrementReceiveCountCount: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_increment_receive_count_count", - Help: "Count of outbox message increment receive count operations", - }), - OutboxTotalMessageLifetimeDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "outbox_total_message_lifetime_duration_seconds", - Help: "Total duration of outbox message lifetime", - Buckets: prometheus.DefBuckets, - }, []string{"message_type"}), - // Keeper metrics KeeperMetadataCreateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: namespace, @@ -307,15 +236,6 @@ func NewStorageMetrics(reg prometheus.Registerer) *StorageMetrics { if reg != nil { reg.MustRegister( - m.OutboxAppendDuration, - m.OutboxAppendCount, - m.OutboxReceiveDuration, - m.OutboxReceiveCount, - m.OutboxDeleteDuration, - m.OutboxDeleteCount, - m.OutboxIncrementReceiveCountDuration, - m.OutboxIncrementReceiveCountCount, - m.OutboxTotalMessageLifetimeDuration, m.KeeperMetadataCreateDuration, m.KeeperMetadataCreateCount, m.KeeperMetadataUpdateDuration, diff --git a/pkg/storage/secret/metadata/outbox_store.go b/pkg/storage/secret/metadata/outbox_store.go deleted file mode 100644 index 1f183b7882b..00000000000 --- a/pkg/storage/secret/metadata/outbox_store.go +++ /dev/null @@ -1,397 +0,0 @@ -package metadata - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" - unifiedsql "github.com/grafana/grafana/pkg/storage/unified/sql" - "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/registry/apis/secret/assert" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" -) - -type outboxStore struct { - db contracts.Database - dialect sqltemplate.Dialect - tracer trace.Tracer - metrics *metrics.StorageMetrics -} - -func ProvideOutboxQueue( - db contracts.Database, - tracer trace.Tracer, - reg prometheus.Registerer, -) contracts.OutboxQueue { - return &outboxStore{ - db: db, - dialect: sqltemplate.DialectForDriver(db.DriverName()), - metrics: metrics.NewStorageMetrics(reg), - tracer: tracer, - } -} - -type outboxMessageDB struct { - RequestID string - MessageID int64 - MessageType contracts.OutboxMessageType - Name string - Namespace string - EncryptedSecret sql.NullString - KeeperName sql.NullString - ExternalID sql.NullString - ReceiveCount int - Created int64 -} - -func (s *outboxStore) Append(ctx context.Context, input contracts.AppendOutboxMessage) (messageID int64, err error) { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes( - attribute.String("name", input.Name), - attribute.String("namespace", input.Namespace), - attribute.String("type", string(input.Type)), - attribute.String("requestID", input.RequestID), - )) - defer span.End() - - defer func() { - if err != nil { - span.SetStatus(codes.Error, "failed to append outbox message") - span.RecordError(err) - } - - if messageID != 0 { - span.SetAttributes(attribute.Int64("messageID", messageID)) - } - }() - - assert.True(input.Type != "", "outboxStore.Append: outbox message type is required") - - messageID, err = s.insertMessage(ctx, input) - if err != nil { - return messageID, fmt.Errorf("inserting message into outbox table: %+w", err) - } - - s.metrics.OutboxAppendDuration.WithLabelValues(string(input.Type)).Observe(time.Since(start).Seconds()) - s.metrics.OutboxAppendCount.WithLabelValues(string(input.Type)).Inc() - - return messageID, nil -} - -func (s *outboxStore) insertMessage(ctx context.Context, input contracts.AppendOutboxMessage) (int64, error) { - keeperName := sql.NullString{} - if input.KeeperName != nil { - keeperName = sql.NullString{ - Valid: true, - String: *input.KeeperName, - } - } - - externalID := sql.NullString{} - if input.ExternalID != nil { - externalID = sql.NullString{ - Valid: true, - String: *input.ExternalID, - } - } - - encryptedSecret := sql.NullString{} - if input.Type == contracts.CreateSecretOutboxMessage || input.Type == contracts.UpdateSecretOutboxMessage { - encryptedSecret = sql.NullString{ - Valid: true, - String: input.EncryptedSecret, - } - } - - req := appendSecureValueOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - Row: &outboxMessageDB{ - RequestID: input.RequestID, - MessageType: input.Type, - Name: input.Name, - Namespace: input.Namespace, - EncryptedSecret: encryptedSecret, - KeeperName: keeperName, - ExternalID: externalID, - ReceiveCount: 0, - Created: time.Now().UTC().UnixMilli(), - }, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxAppend, req) - if err != nil { - return 0, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxAppend.Name(), err) - } - - result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - if unifiedsql.IsRowAlreadyExistsError(err) { - return 0, contracts.ErrSecureValueOperationInProgress - } - return 0, fmt.Errorf("inserting message into secure value outbox table: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return 0, fmt.Errorf("get rows affected: %w", err) - } - - if rowsAffected != 1 { - return 0, fmt.Errorf("expected to affect 1 row, but affected %d", rowsAffected) - } - - id, err := result.LastInsertId() - if err != nil { - return id, fmt.Errorf("fetching last inserted id: %w", err) - } - - return id, nil -} - -func (s *outboxStore) ReceiveN(ctx context.Context, limit uint) ([]contracts.OutboxMessage, error) { - start := time.Now() - messageIDs, err := s.fetchMessageIdsInQueue(ctx, limit) - if err != nil { - return nil, fmt.Errorf("fetching message ids from queue: %w", err) - } - // If queue is empty - if len(messageIDs) == 0 { - return nil, nil - } - req := receiveNSecureValueOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageIDs: messageIDs, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxReceiveN, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxReceiveN.Name(), err) - } - - rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return nil, fmt.Errorf("fetching rows from secure value outbox table: %w", err) - } - defer func() { _ = rows.Close() }() - - messages := make([]contracts.OutboxMessage, 0) - - for rows.Next() { - var row outboxMessageDB - if err := rows.Scan( - &row.RequestID, - &row.MessageID, - &row.MessageType, - &row.Name, - &row.Namespace, - &row.EncryptedSecret, - &row.KeeperName, - &row.ExternalID, - &row.ReceiveCount, - &row.Created, - ); err != nil { - return nil, fmt.Errorf("scanning row from secure value outbox table: %w", err) - } - - var keeperName *string - if row.KeeperName.Valid { - keeperName = &row.KeeperName.String - } - - var externalID *string - if row.ExternalID.Valid { - externalID = &row.ExternalID.String - } - - msg := contracts.OutboxMessage{ - RequestID: row.RequestID, - Type: row.MessageType, - MessageID: row.MessageID, - Name: row.Name, - Namespace: row.Namespace, - KeeperName: keeperName, - ExternalID: externalID, - ReceiveCount: row.ReceiveCount, - Created: row.Created, - } - - if row.MessageType != contracts.DeleteSecretOutboxMessage && row.EncryptedSecret.Valid { - msg.EncryptedSecret = row.EncryptedSecret.String - } - - messages = append(messages, msg) - } - - if err := rows.Err(); err != nil { - return messages, fmt.Errorf("reading rows: %w", err) - } - - s.metrics.OutboxReceiveDuration.Observe(time.Since(start).Seconds()) - s.metrics.OutboxReceiveCount.Add(float64(len(messages))) - - return messages, nil -} - -func (s *outboxStore) fetchMessageIdsInQueue(ctx context.Context, limit uint) ([]int64, error) { - req := fetchMessageIDsOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - ReceiveLimit: limit, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxFetchMessageIDs, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxFetchMessageIDs.Name(), err) - } - - rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return nil, fmt.Errorf("fetching rows from secure value outbox table: %w", err) - } - defer func() { _ = rows.Close() }() - - messageIDs := make([]int64, 0, limit) - - for rows.Next() { - var id int64 - if err := rows.Scan(&id); err != nil { - return nil, fmt.Errorf("scanning row; %w", err) - } - messageIDs = append(messageIDs, id) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("reading rows: %w", err) - } - - return messageIDs, nil -} - -func (s *outboxStore) Delete(ctx context.Context, messageID int64) (err error) { - ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes( - attribute.Int64("messageID", messageID), - )) - defer span.End() - - defer func() { - if err != nil { - span.SetStatus(codes.Error, "failed to delete message from outbox") - span.RecordError(err) - } - }() - - assert.True(messageID != 0, "outboxStore.Delete: messageID is required") - - start := time.Now() - if err := s.deleteMessage(ctx, messageID); err != nil { - return fmt.Errorf("deleting message from outbox table %+w", err) - } - - s.metrics.OutboxDeleteDuration.Observe(time.Since(start).Seconds()) - s.metrics.OutboxDeleteCount.Inc() - - return nil -} - -func (s *outboxStore) deleteMessage(ctx context.Context, messageID int64) error { - tsReq := getOutboxMessageTimestamp{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageID: messageID, - } - - // First query the object so we can get the timestamp and calculate the total lifetime - timestampQuery, err := sqltemplate.Execute(sqlSecureValueOutboxQueryTimestamp, tsReq) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxQueryTimestamp.Name(), err) - } - - rows, err := s.db.QueryContext(ctx, timestampQuery, tsReq.GetArgs()...) - if err != nil { - return fmt.Errorf("querying timestamp from secure value outbox table: %w", err) - } - - if !rows.Next() { - _ = rows.Close() - return fmt.Errorf("no row found for message id=%v", messageID) - } - - var timestamp int64 - var messageType string - if err := rows.Scan(×tamp, &messageType); err != nil { - _ = rows.Close() - return fmt.Errorf("scanning timestamp: %w", err) - } - - // Explicitly close rows and check for errors before proceeding - if err := rows.Close(); err != nil { - return fmt.Errorf("closing rows: %w", err) - } - - if err := rows.Err(); err != nil { - return fmt.Errorf("rows error: %w", err) - } - - totalLifetime := time.Since(time.UnixMilli(timestamp)) - s.metrics.OutboxTotalMessageLifetimeDuration.WithLabelValues(messageType).Observe(totalLifetime.Seconds()) - - // Then delete the object - delReq := deleteSecureValueOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageID: messageID, - } - - query, err := sqltemplate.Execute(sqlSecureValueOutboxDelete, delReq) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxDelete.Name(), err) - } - - result, err := s.db.ExecContext(ctx, query, delReq.GetArgs()...) - if err != nil { - return fmt.Errorf("deleting message id=%v from secure value outbox table: %w", messageID, err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("get rows affected: %w", err) - } - - // TODO: Presumably it's a bug if we delete 0 rows? - if rowsAffected > 1 { - return fmt.Errorf("bug: deleted more than one row from the outbox table, should delete only one at a time: deleted=%v", rowsAffected) - } - - return nil -} - -func (s *outboxStore) IncrementReceiveCount(ctx context.Context, messageIDs []int64) error { - if len(messageIDs) == 0 { - return nil - } - - req := incrementReceiveCountOutbox{ - SQLTemplate: sqltemplate.New(s.dialect), - MessageIDs: messageIDs, - } - - start := time.Now() - query, err := sqltemplate.Execute(sqlSecureValueOutboxUpdateReceiveCount, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxUpdateReceiveCount.Name(), err) - } - - _, err = s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("updating outbox messages receive count: %w", err) - } - - s.metrics.OutboxIncrementReceiveCountDuration.Observe(time.Since(start).Seconds()) - s.metrics.OutboxIncrementReceiveCountCount.Add(float64(len(messageIDs))) - - return nil -} diff --git a/pkg/storage/secret/metadata/outbox_store_test.go b/pkg/storage/secret/metadata/outbox_store_test.go deleted file mode 100644 index f757e8b6b38..00000000000 --- a/pkg/storage/secret/metadata/outbox_store_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package metadata - -import ( - "context" - "fmt" - "math/rand" - "slices" - "testing" - "time" - - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/secret/database" - "github.com/grafana/grafana/pkg/storage/secret/migrator" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace/noop" -) - -type outboxStoreModel struct { - rows []contracts.OutboxMessage -} - -func newOutboxStoreModel() *outboxStoreModel { - return &outboxStoreModel{} -} - -func (model *outboxStoreModel) Append(messageID int64, message contracts.AppendOutboxMessage) { - model.rows = append(model.rows, contracts.OutboxMessage{ - Type: message.Type, - MessageID: messageID, - Name: message.Name, - Namespace: message.Namespace, - EncryptedSecret: message.EncryptedSecret, - KeeperName: message.KeeperName, - ExternalID: message.ExternalID, - }) -} - -func (model *outboxStoreModel) ReceiveN(n uint) []contracts.OutboxMessage { - maxMessages := min(len(model.rows), int(n)) - if maxMessages == 0 { - return nil - } - return model.rows[:maxMessages] -} - -func (model *outboxStoreModel) Delete(messageID int64) { - oldLen := len(model.rows) - model.rows = slices.DeleteFunc(model.rows, func(m contracts.OutboxMessage) bool { - return m.MessageID == messageID - }) - if len(model.rows) != oldLen-1 { - panic("Delete: deleted more than one message") - } -} - -func TestOutboxStoreModel(t *testing.T) { - t.Parallel() - - model := newOutboxStoreModel() - - require.Empty(t, model.ReceiveN(10)) - - appendOutboxMessage := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - - outboxMessage1 := contracts.OutboxMessage{ - MessageID: 1, - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - - outboxMessage2 := contracts.OutboxMessage{ - MessageID: 2, - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - - model.Append(1, appendOutboxMessage) - - require.Equal(t, []contracts.OutboxMessage{outboxMessage1}, model.ReceiveN(10)) - - model.Append(2, appendOutboxMessage) - - require.Equal(t, []contracts.OutboxMessage{outboxMessage1, outboxMessage2}, model.ReceiveN(10)) - - model.Delete(outboxMessage1.MessageID) - - require.Equal(t, []contracts.OutboxMessage{outboxMessage2}, model.ReceiveN(5)) - - model.Delete(outboxMessage2.MessageID) - - require.Empty(t, model.ReceiveN(1)) -} - -func TestOutboxStoreSecureValueOperationInProgress(t *testing.T) { - t.Parallel() - - t.Run("Append returns error when the queue already contains an operation for the secure value", func(t *testing.T) { - t.Parallel() - - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - - ctx := context.Background() - - outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil) - - _, err := outbox.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: "1", - Type: contracts.CreateSecretOutboxMessage, - Name: "name1", - Namespace: "ns1", - EncryptedSecret: "v1", - KeeperName: nil, - ExternalID: nil, - }) - require.NoError(t, err) - - _, err = outbox.Append(ctx, contracts.AppendOutboxMessage{ - RequestID: "1", - Type: contracts.UpdateSecretOutboxMessage, - Name: "name1", - Namespace: "ns1", - EncryptedSecret: "v1", - KeeperName: nil, - ExternalID: nil, - }) - - require.ErrorIs(t, err, contracts.ErrSecureValueOperationInProgress) - }) -} - -func TestOutboxStore(t *testing.T) { - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - - ctx := context.Background() - - outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil) - - m1 := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-1", - EncryptedSecret: "value", - ExternalID: nil, - } - m2 := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: "s-1", - Namespace: "n-2", - EncryptedSecret: "value", - ExternalID: nil, - } - - messages, err := outbox.ReceiveN(ctx, 10) - require.NoError(t, err) - require.Empty(t, messages) - - messageID1, err := outbox.Append(ctx, m1) - require.NoError(t, err) - - for range 2 { - messages, err = outbox.ReceiveN(ctx, 10) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - require.Equal(t, messageID1, messages[0].MessageID) - } - - messageID2, err := outbox.Append(ctx, m2) - require.NoError(t, err) - - messages, err = outbox.ReceiveN(ctx, 3) - require.NoError(t, err) - require.Equal(t, 2, len(messages)) - require.Equal(t, messageID1, messages[0].MessageID) - require.Equal(t, messageID2, messages[1].MessageID) - - require.NoError(t, outbox.Delete(ctx, messageID1)) - messages, err = outbox.ReceiveN(ctx, 10) - require.NoError(t, err) - require.Equal(t, 1, len(messages)) - require.Equal(t, messageID2, messages[0].MessageID) - - require.NoError(t, outbox.Delete(ctx, messageID2)) - - messages, err = outbox.ReceiveN(ctx, 100) - require.NoError(t, err) - require.Empty(t, messages) -} - -func TestOutboxStoreProperty(t *testing.T) { - seed := time.Now().UnixMicro() - rng := rand.New(rand.NewSource(seed)) - - defer func() { - if t.Failed() { - fmt.Printf("TestOutboxStoreProperty: SEED=%+v\n\n", seed) - } - }() - - // The number of iterations was decided arbitrarily based on the time the test takes to run - for range 10 { - testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) - tracer := noop.NewTracerProvider().Tracer("test") - - outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil) - - model := newOutboxStoreModel() - - ctx := context.Background() - - for i := range 100 { - n := rng.Intn(3) - switch n { - case 0: - time.Sleep(1 * time.Microsecond) - message := contracts.AppendOutboxMessage{ - Type: contracts.CreateSecretOutboxMessage, - Name: fmt.Sprintf("s-%d", i), - Namespace: fmt.Sprintf("n-%d", i), - EncryptedSecret: "value", - ExternalID: nil, - } - messageID, err := outbox.Append(ctx, message) - require.NoError(t, err) - - model.Append(messageID, message) - - case 1: - n := uint(rng.Intn(10)) - messages, err := outbox.ReceiveN(ctx, n) - require.NoError(t, err) - - modelMessages := model.ReceiveN(n) - - require.Equal(t, len(modelMessages), len(messages)) - for i := range len(modelMessages) { - require.Equal(t, modelMessages[i].MessageID, messages[i].MessageID) - } - - case 2: - if len(model.rows) == 0 { - continue - } - - message := model.rows[rng.Intn(len(model.rows))] - - model.Delete(message.MessageID) - require.NoError(t, outbox.Delete(ctx, message.MessageID)) - - default: - panic(fmt.Sprintf("unhandled action: %+v", n)) - } - } - } -} diff --git a/pkg/storage/secret/metadata/query.go b/pkg/storage/secret/metadata/query.go index 907a4d6cbea..2c85d762b87 100644 --- a/pkg/storage/secret/metadata/query.go +++ b/pkg/storage/secret/metadata/query.go @@ -27,18 +27,12 @@ var ( sqlSecureValueRead = mustTemplate("secure_value_read.sql") sqlSecureValueList = mustTemplate("secure_value_list.sql") sqlSecureValueCreate = mustTemplate("secure_value_create.sql") - sqlSecureValueDelete = mustTemplate("secure_value_delete.sql") - sqlSecureValueUpdate = mustTemplate("secure_value_update.sql") sqlSecureValueUpdateExternalId = mustTemplate("secure_value_updateExternalId.sql") - sqlSecureValueUpdateStatus = mustTemplate("secure_value_updateStatus.sql") sqlSecureValueReadForDecrypt = mustTemplate("secure_value_read_for_decrypt.sql") - sqlSecureValueOutboxAppend = mustTemplate("secure_value_outbox_append.sql") - sqlSecureValueOutboxFetchMessageIDs = mustTemplate("secure_value_outbox_fetch_message_ids.sql") - sqlSecureValueOutboxReceiveN = mustTemplate("secure_value_outbox_receiveN.sql") - sqlSecureValueOutboxDelete = mustTemplate("secure_value_outbox_delete.sql") - sqlSecureValueOutboxUpdateReceiveCount = mustTemplate("secure_value_outbox_update_receive_count.sql") - sqlSecureValueOutboxQueryTimestamp = mustTemplate("secure_value_outbox_query_timestamp.sql") + sqlGetLatestSecureValueVersion = mustTemplate("secure_value_get_latest_version.sql") + sqlSecureValueSetVersionToActive = mustTemplate("secure_value_set_version_to_active.sql") + sqlSecureValueSetVersionToInactive = mustTemplate("secure_value_set_version_to_inactive.sql") ) func mustTemplate(filename string) *template.Template { @@ -150,6 +144,38 @@ func (r readSecureValue) Validate() error { return nil // TODO } +type getLatestSecureValueVersion struct { + sqltemplate.SQLTemplate + Namespace string + Name string +} + +func (r getLatestSecureValueVersion) Validate() error { + return nil +} + +type secureValueSetVersionToActive struct { + sqltemplate.SQLTemplate + Namespace string + Name string + Version int64 +} + +func (r secureValueSetVersionToActive) Validate() error { + return nil +} + +type secureValueSetVersionToInactive struct { + sqltemplate.SQLTemplate + Namespace string + Name string + Version int64 +} + +func (r secureValueSetVersionToInactive) Validate() error { + return nil +} + type listSecureValue struct { sqltemplate.SQLTemplate Namespace string @@ -170,23 +196,12 @@ func (r createSecureValue) Validate() error { return nil // TODO } -// Delete -type deleteSecureValue struct { - sqltemplate.SQLTemplate - Namespace string - Name string -} - -// Validate is only used if we use `dbutil` from `unifiedstorage` -func (r deleteSecureValue) Validate() error { - return nil // TODO -} - // Update externalId type updateExternalIdSecureValue struct { sqltemplate.SQLTemplate Namespace string Name string + Version int64 ExternalID string } @@ -195,33 +210,6 @@ func (r updateExternalIdSecureValue) Validate() error { return nil // TODO } -// Update secure value -type updateSecureValue struct { - sqltemplate.SQLTemplate - Namespace string - Name string - Row *secureValueDB -} - -// Validate is only used if we use `dbutil` from `unifiedstorage` -func (r updateSecureValue) Validate() error { - return nil // TODO -} - -// update status message -type updateStatusSecureValue struct { - sqltemplate.SQLTemplate - Namespace string - Name string - Phase string - Message string -} - -// Validate is only used if we use `dbutil` from `unifiedstorage` -func (r updateStatusSecureValue) Validate() error { - return nil // TODO -} - type readSecureValueForDecrypt struct { sqltemplate.SQLTemplate Namespace string @@ -229,48 +217,3 @@ type readSecureValueForDecrypt struct { } func (r readSecureValueForDecrypt) Validate() error { return nil } - -/*************************************/ -/**-- Secure Value Outbox Queries --**/ -/*************************************/ -type appendSecureValueOutbox struct { - sqltemplate.SQLTemplate - Row *outboxMessageDB -} - -func (appendSecureValueOutbox) Validate() error { return nil } - -type receiveNSecureValueOutbox struct { - sqltemplate.SQLTemplate - MessageIDs []int64 -} - -func (receiveNSecureValueOutbox) Validate() error { return nil } - -type fetchMessageIDsOutbox struct { - sqltemplate.SQLTemplate - ReceiveLimit uint -} - -func (fetchMessageIDsOutbox) Validate() error { return nil } - -type deleteSecureValueOutbox struct { - sqltemplate.SQLTemplate - MessageID int64 -} - -func (deleteSecureValueOutbox) Validate() error { return nil } - -type getOutboxMessageTimestamp struct { - sqltemplate.SQLTemplate - MessageID int64 -} - -func (getOutboxMessageTimestamp) Validate() error { return nil } - -type incrementReceiveCountOutbox struct { - sqltemplate.SQLTemplate - MessageIDs []int64 -} - -func (incrementReceiveCountOutbox) Validate() error { return nil } diff --git a/pkg/storage/secret/metadata/query_test.go b/pkg/storage/secret/metadata/query_test.go index 8a5ff0e6a9a..dce469d6d87 100644 --- a/pkg/storage/secret/metadata/query_test.go +++ b/pkg/storage/secret/metadata/query_test.go @@ -1,7 +1,6 @@ package metadata import ( - "database/sql" "testing" "text/template" @@ -123,6 +122,27 @@ func TestSecureValueQueries(t *testing.T) { mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ RootDir: "testdata", Templates: map[*template.Template][]mocks.TemplateTestCase{ + sqlGetLatestSecureValueVersion: { + { + Name: "get latest secure value version", + Data: &getLatestSecureValueVersion{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + }, + }, + }, + sqlSecureValueSetVersionToActive: { + { + Name: "set secure value version to active", + Data: &secureValueSetVersionToActive{ + SQLTemplate: mocks.NewTestingSQLTemplate(), + Name: "name", + Namespace: "ns", + Version: 1, + }, + }, + }, sqlSecureValueRead: { { Name: "read", @@ -166,8 +186,7 @@ func TestSecureValueQueries(t *testing.T) { CreatedBy: "user:ryan", Updated: 5678, UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(nil), + Version: 1, Description: "description", Keeper: toNullString(nil), Decrypters: toNullString(nil), @@ -190,72 +209,7 @@ func TestSecureValueQueries(t *testing.T) { CreatedBy: "user:ryan", Updated: 5678, UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(ptr.To("message_test")), - Description: "description", - Keeper: toNullString(ptr.To("keeper_test")), - Decrypters: toNullString(ptr.To("decrypters_test")), - Ref: toNullString(ptr.To("ref_test")), - ExternalID: "extId", - }, - }, - }, - }, - sqlSecureValueDelete: { - { - Name: "delete", - Data: &deleteSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - }, - }, - }, - sqlSecureValueUpdate: { - { - Name: "update-null", - Data: &updateSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - Row: &secureValueDB{ - GUID: "abc", - Name: "name", - Namespace: "ns", - Annotations: `{"x":"XXXX"}`, - Labels: `{"a":"AAA", "b", "BBBB"}`, - Created: 1234, - CreatedBy: "user:ryan", - Updated: 5678, - UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(nil), - Description: "description", - Keeper: toNullString(nil), - Decrypters: toNullString(nil), - Ref: toNullString(nil), - ExternalID: "extId", - }, - }, - }, - { - Name: "update-not-null", - Data: &updateSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - Row: &secureValueDB{ - GUID: "abc", - Name: "name", - Namespace: "ns", - Annotations: `{"x":"XXXX"}`, - Labels: `{"a":"AAA", "b", "BBBB"}`, - Created: 1234, - CreatedBy: "user:ryan", - Updated: 5678, - UpdatedBy: "user:cameron", - Phase: "creating", - Message: toNullString(ptr.To("message_test")), + Version: 1, Description: "description", Keeper: toNullString(ptr.To("keeper_test")), Decrypters: toNullString(ptr.To("decrypters_test")), @@ -276,18 +230,6 @@ func TestSecureValueQueries(t *testing.T) { }, }, }, - sqlSecureValueUpdateStatus: { - { - Name: "updateStatus", - Data: &updateStatusSecureValue{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Name: "name", - Namespace: "ns", - Phase: "Succeeded", - Message: "message-1", - }, - }, - }, sqlSecureValueReadForDecrypt: { { Name: "read-for-decrypt", @@ -301,111 +243,3 @@ func TestSecureValueQueries(t *testing.T) { }, }) } - -func TestSecureValueOutboxQueries(t *testing.T) { - mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ - RootDir: "testdata", - Templates: map[*template.Template][]mocks.TemplateTestCase{ - sqlSecureValueOutboxUpdateReceiveCount: { - { - - Name: "update-receive-count", - Data: &incrementReceiveCountOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - MessageIDs: []int64{1, 2, 3}, - }, - }, - }, - sqlSecureValueOutboxAppend: { - { - Name: "no-encrypted-secret", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - ExternalID: sql.NullString{Valid: true, String: "external-id"}, - KeeperName: sql.NullString{Valid: true, String: "keeper"}, - Created: 1234, - }, - }, - }, - { - Name: "no-external-id", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"}, - KeeperName: sql.NullString{Valid: true, String: "keeper"}, - Created: 1234, - }, - }, - }, - { - Name: "no-keeper-name", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"}, - ExternalID: sql.NullString{Valid: true, String: "external-id"}, - Created: 1234, - }, - }, - }, - { - Name: "all-fields-present", - Data: &appendSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - Row: &outboxMessageDB{ - MessageID: 1, - MessageType: "some-type", - Name: "name", - Namespace: "namespace", - EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"}, - ExternalID: sql.NullString{Valid: true, String: ""}, // can be empty string - KeeperName: sql.NullString{Valid: true, String: "keeper"}, - Created: 1234, - }, - }, - }, - }, - sqlSecureValueOutboxFetchMessageIDs: { - { - Name: "basic", - Data: &fetchMessageIDsOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - ReceiveLimit: 10, - }, - }, - }, - sqlSecureValueOutboxReceiveN: { - { - Name: "basic", - Data: &receiveNSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - MessageIDs: []int64{1, 2, 3}, - }, - }, - }, - sqlSecureValueOutboxDelete: { - { - Name: "basic", - Data: &deleteSecureValueOutbox{ - SQLTemplate: mocks.NewTestingSQLTemplate(), - MessageID: 1, - }, - }, - }, - }, - }) -} diff --git a/pkg/storage/secret/metadata/secure_value_model.go b/pkg/storage/secret/metadata/secure_value_model.go index d90beaa6782..c2f59a6e4c1 100644 --- a/pkg/storage/secret/metadata/secure_value_model.go +++ b/pkg/storage/secret/metadata/secure_value_model.go @@ -7,8 +7,8 @@ import ( "time" "github.com/google/uuid" + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/storage/secret/migrator" @@ -29,8 +29,8 @@ type secureValueDB struct { UpdatedBy string // Kubernetes Status - Phase string - Message sql.NullString + Active bool + Version int64 // Spec Description string @@ -45,7 +45,7 @@ func (*secureValueDB) TableName() string { } // toKubernetes maps a DB row into a Kubernetes resource (metadata + spec). -func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) { +func (sv *secureValueDB) toKubernetes() (*secretv1beta1.SecureValue, error) { annotations := make(map[string]string, 0) if sv.Annotations != "" { if err := json.Unmarshal([]byte(sv.Annotations), &annotations); err != nil { @@ -68,14 +68,14 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) { } } - resource := &secretv0alpha1.SecureValue{ - Spec: secretv0alpha1.SecureValueSpec{ + resource := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ Description: sv.Description, Decrypters: decrypters, }, - Status: secretv0alpha1.SecureValueStatus{ - Phase: secretv0alpha1.SecureValuePhase(sv.Phase), + Status: secretv1beta1.SecureValueStatus{ ExternalID: sv.ExternalID, + Version: sv.Version, }, } @@ -85,10 +85,7 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) { if sv.Ref.Valid { resource.Spec.Ref = &sv.Ref.String } - if sv.Message.Valid { - resource.Status.Message = sv.Message.String - } - resource.Status.Phase = secretv0alpha1.SecureValuePhase(sv.Phase) + resource.Status.ExternalID = sv.ExternalID // Set all meta fields here for consistency. @@ -114,7 +111,7 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) { } // toCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted. -func toCreateRow(sv *secretv0alpha1.SecureValue, actorUID string) (*secureValueDB, error) { +func toCreateRow(sv *secretv1beta1.SecureValue, actorUID string) (*secureValueDB, error) { row, err := toRow(sv, "") if err != nil { return nil, fmt.Errorf("failed to convert SecureValue to secureValueDB: %w", err) @@ -131,26 +128,8 @@ func toCreateRow(sv *secretv0alpha1.SecureValue, actorUID string) (*secureValueD return row, nil } -// toUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated. -func toUpdateRow(currentRow *secureValueDB, newSecureValue *secretv0alpha1.SecureValue, actorUID, externalID string) (*secureValueDB, error) { - row, err := toRow(newSecureValue, externalID) - if err != nil { - return nil, fmt.Errorf("failed to create: %w", err) - } - - now := time.Now().UTC().Unix() - - row.GUID = currentRow.GUID - row.Created = currentRow.Created - row.CreatedBy = currentRow.CreatedBy - row.Updated = now - row.UpdatedBy = actorUID - - return row, nil -} - // toRow maps a Kubernetes resource into a DB row. -func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, error) { +func toRow(sv *secretv1beta1.SecureValue, externalID string) (*secureValueDB, error) { var annotations string if len(sv.Annotations) > 0 { cleanedAnnotations := xkube.CleanAnnotations(sv.Annotations) @@ -201,11 +180,6 @@ func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, e return nil, fmt.Errorf("failed to get resource version: %w", err) } - var statusMessage *string - if sv.Status.Message != "" { - statusMessage = &sv.Status.Message - } - return &secureValueDB{ GUID: string(sv.UID), Name: sv.Name, @@ -217,8 +191,7 @@ func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, e Updated: updatedTimestamp, UpdatedBy: meta.GetUpdatedBy(), - Phase: string(sv.Status.Phase), - Message: toNullString(statusMessage), + Version: sv.Status.Version, Description: sv.Spec.Description, Keeper: toNullString(sv.Spec.Keeper), @@ -233,6 +206,7 @@ type secureValueForDecrypt struct { Keeper sql.NullString Decrypters sql.NullString Ref sql.NullString + Active bool ExternalID string } diff --git a/pkg/storage/secret/metadata/secure_value_store.go b/pkg/storage/secret/metadata/secure_value_store.go index 6f79dfbd353..e2a7a6c0408 100644 --- a/pkg/storage/secret/metadata/secure_value_store.go +++ b/pkg/storage/secret/metadata/secure_value_store.go @@ -5,16 +5,16 @@ import ( "fmt" "time" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" - "github.com/grafana/grafana/pkg/storage/unified/sql" - "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/storage/secret/metadata/metrics" + "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) var _ contracts.SecureValueMetadataStorage = (*secureValueMetadataStorage)(nil) @@ -22,14 +22,8 @@ var _ contracts.SecureValueMetadataStorage = (*secureValueMetadataStorage)(nil) func ProvideSecureValueMetadataStorage( db contracts.Database, tracer trace.Tracer, - features featuremgmt.FeatureToggles, reg prometheus.Registerer, ) (contracts.SecureValueMetadataStorage, error) { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || - !features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { - return &secureValueMetadataStorage{}, nil - } - return &secureValueMetadataStorage{ db: db, dialect: sqltemplate.DialectForDriver(db.DriverName()), @@ -46,7 +40,7 @@ type secureValueMetadataStorage struct { tracer trace.Tracer } -func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { +func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Create", trace.WithAttributes( attribute.String("name", sv.GetName()), @@ -55,33 +49,18 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp )) defer span.End() - sv.Status.Phase = secretv0alpha1.SecureValuePhasePending - sv.Status.Message = "Creating secure value" + // Set inside of the transaction callback + var row *secureValueDB - row, err := toCreateRow(sv, actorUID) - if err != nil { - return nil, fmt.Errorf("to create row: %w", err) - } - - req := createSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Row: row, - } - - query, err := sqltemplate.Execute(sqlSecureValueCreate, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err) - } - - err = s.db.Transaction(ctx, func(ctx context.Context) error { - if row.Keeper.Valid { + err := s.db.Transaction(ctx, func(ctx context.Context) error { + if sv.Spec.Keeper != nil { // Validate before inserting that the chosen `keeper` exists. // -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage req := &readKeeper{ SQLTemplate: sqltemplate.New(s.dialect), - Namespace: row.Namespace, - Name: row.Keeper.String, + Namespace: sv.Namespace, + Name: *sv.Spec.Keeper, IsForUpdate: true, } @@ -101,23 +80,61 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp } } - res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + latestVersion, err := s.getLatestVersion(ctx, xkube.Namespace(sv.Namespace), sv.Name) if err != nil { - if sql.IsRowAlreadyExistsError(err) { - return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists) + return fmt.Errorf("fetching latest secure value version: %w", err) + } + + version := int64(1) + if latestVersion != nil { + version = *latestVersion + 1 + } + + // Some other concurrent request may have created the version we're trying to create, + // if that's the case, we'll retry with a new version up to max attempts. + maxAttempts := 3 + attempts := 0 + for { + sv.Status.Version = version + + row, err = toCreateRow(sv, actorUID) + if err != nil { + return fmt.Errorf("to create row: %w", err) } - return fmt.Errorf("inserting row: %w", err) - } - rowsAffected, err := res.RowsAffected() - if err != nil { - return fmt.Errorf("getting rows affected: %w", err) - } + req := createSecureValue{ + SQLTemplate: sqltemplate.New(s.dialect), + Row: row, + } - if rowsAffected != 1 { - return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace) + query, err := sqltemplate.Execute(sqlSecureValueCreate, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err) + } + + res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) + if err != nil { + if sql.IsRowAlreadyExistsError(err) { + if attempts < maxAttempts { + attempts += 1 + version += 1 + continue + } + return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists) + } + return fmt.Errorf("inserting row: %w", err) + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("getting rows affected: %w", err) + } + + if rowsAffected != 1 { + return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace) + } + return nil } - return nil }) if err != nil { return nil, fmt.Errorf("db failure: %w", err) @@ -134,302 +151,47 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp return createdSecureValue, nil } -func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.SecureValue, error) { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Read", trace.WithAttributes( - attribute.String("name", name), - attribute.String("namespace", namespace.String()), - attribute.Bool("isForUpdate", opts.ForUpdate), - )) - defer span.End() - - secureValue, err := s.read(ctx, namespace, name, opts) - if err != nil { - return nil, err - } - - secureValueKub, err := secureValue.toKubernetes() - if err != nil { - return nil, fmt.Errorf("convert to kubernetes object: %w", err) - } - - s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataGetCount.Inc() - - return secureValueKub, nil -} - -func (s *secureValueMetadataStorage) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Update", trace.WithAttributes( - attribute.String("name", newSecureValue.GetName()), - attribute.String("namespace", newSecureValue.GetNamespace()), - attribute.String("actorUID", actorUID), - )) - defer span.End() - - var newRow *secureValueDB - - err := s.db.Transaction(ctx, func(ctx context.Context) error { - read, err := s.read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true}) - if err != nil { - return fmt.Errorf("reading secure value: %w", err) - } - - // TODO: Confirm the ExternalID should come from the read model. - var updateErr error - newRow, updateErr = toUpdateRow(&read, newSecureValue, actorUID, read.ExternalID) - if updateErr != nil { - return fmt.Errorf("model to update row: %w", updateErr) - } - - if newRow.Keeper.Valid { - // Validate before updating that the new `keeper` exists. - - // -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage - req := &readKeeper{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: newRow.Namespace, - Name: newRow.Keeper.String, - IsForUpdate: true, - } - - query, err := sqltemplate.Execute(sqlKeeperRead, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err) - } - - res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("getting row: %w", err) - } - defer func() { _ = res.Close() }() - - if !res.Next() { - return contracts.ErrKeeperNotFound - } - } - - req := &updateSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: newRow.Namespace, - Name: newRow.Name, - Row: newRow, - } - - query, err := sqltemplate.Execute(sqlSecureValueUpdate, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdate.Name(), err) - } - - result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("updating row: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("getting rows affected: %w", err) - } - - if rowsAffected != 1 { - return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, newRow.Name, newRow.Namespace) - } - - return nil - }) - if err != nil { - return nil, fmt.Errorf("db failure: %w", err) - } - - secureValue, err := newRow.toKubernetes() - if err != nil { - return nil, fmt.Errorf("convert to kubernetes object: %w", err) - } - - s.metrics.SecureValueMetadataUpdateDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataUpdateCount.Inc() - - return secureValue, nil -} - -func (s *secureValueMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Delete", trace.WithAttributes( +func (s *secureValueMetadataStorage) getLatestVersion(ctx context.Context, namespace xkube.Namespace, name string) (*int64, error) { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersion", trace.WithAttributes( attribute.String("name", name), attribute.String("namespace", namespace.String()), )) defer span.End() - req := deleteSecureValue{ + req := getLatestSecureValueVersion{ SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace.String(), Name: name, } - query, err := sqltemplate.Execute(sqlSecureValueDelete, req) + q, err := sqltemplate.Execute(sqlGetLatestSecureValueVersion, req) if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueDelete.Name(), err) - } - - res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) - if err != nil { - return fmt.Errorf("deleting secure value row: %w", err) - } - - if rowsAffected, err := res.RowsAffected(); err != nil || rowsAffected != 1 { - return fmt.Errorf("deleting secure value rowsAffected=%d error=%w", rowsAffected, err) - } - - s.metrics.SecureValueMetadataDeleteDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataDeleteCount.Inc() - - return nil -} - -func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (svList []secretv0alpha1.SecureValue, error error) { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.List", trace.WithAttributes( - attribute.String("namespace", namespace.String()), - )) - defer span.End() - - defer func() { - span.SetAttributes(attribute.Int("returnedList.count", len(svList))) - }() - - req := listSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - } - - q, err := sqltemplate.Execute(sqlSecureValueList, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueList.Name(), err) + return nil, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersion.Name(), err) } rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...) if err != nil { - return nil, fmt.Errorf("listing secure values: %w", err) + return nil, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err) } defer func() { _ = rows.Close() }() - secureValues := make([]secretv0alpha1.SecureValue, 0) - for rows.Next() { - row := secureValueDB{} - - err = rows.Scan(&row.GUID, - &row.Name, &row.Namespace, &row.Annotations, - &row.Labels, - &row.Created, &row.CreatedBy, - &row.Updated, &row.UpdatedBy, - &row.Phase, &row.Message, - &row.Description, &row.Keeper, &row.Decrypters, - &row.Ref, &row.ExternalID, - ) - - if err != nil { - return nil, fmt.Errorf("error reading secure value row: %w", err) - } - - secureValue, err := row.toKubernetes() - if err != nil { - return nil, fmt.Errorf("convert to kubernetes object: %w", err) - } - - secureValues = append(secureValues, *secureValue) - } if err := rows.Err(); err != nil { - return nil, fmt.Errorf("read rows error: %w", err) + return nil, fmt.Errorf("error executing query: %w", err) } - s.metrics.SecureValueMetadataListDuration.Observe(time.Since(start).Seconds()) - s.metrics.SecureValueMetadataListCount.Inc() + if !rows.Next() { + return nil, nil + } - return secureValues, nil -} - -func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID contracts.ExternalID) error { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( - attribute.String("name", name), - attribute.String("namespace", namespace.String()), - attribute.String("externalID", externalID.String()), - )) - defer span.End() - - req := updateExternalIdSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - Name: name, - ExternalID: externalID.String(), - } - - q, err := sqltemplate.Execute(sqlSecureValueUpdateExternalId, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateExternalId.Name(), err) - } - - res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) - if err != nil { - return fmt.Errorf("setting secure value external id: namespace=%+v name=%+v externalID=%+v %w", namespace, name, externalID, err) - } - - // validate modified cound - modifiedCount, err := res.RowsAffected() - if err != nil { - return fmt.Errorf("getting updated rows update external id secure value: %w", err) - } - if modifiedCount > 1 { - return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount) - } - s.metrics.SecureValueSetExternalIDDuration.Observe(time.Since(start).Seconds()) - - return nil -} - -func (s *secureValueMetadataStorage) SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error { - start := time.Now() - ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetStatus", trace.WithAttributes( - attribute.String("name", name), - attribute.String("namespace", namespace.String()), - attribute.String("status.phase", string(status.Phase)), - attribute.String("status.message", status.Message), - attribute.String("status.externalID", status.ExternalID), - )) - defer span.End() - - req := updateStatusSecureValue{ - SQLTemplate: sqltemplate.New(s.dialect), - Namespace: namespace.String(), - Name: name, - Phase: string(status.Phase), - Message: status.Message, - } - - q, err := sqltemplate.Execute(sqlSecureValueUpdateStatus, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateStatus.Name(), err) - } - - res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) - if err != nil { - return fmt.Errorf("setting secure value status to Succeeded id: namespace=%+v name=%+v %w", namespace, name, err) - } - - // validate modified cound - modifiedCount, err := res.RowsAffected() - if err != nil { - return fmt.Errorf("getting updated rows update status secure value: %w", err) - } - if modifiedCount > 1 { - return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount) + var version int64 + if err := rows.Scan(&version); err != nil { + return nil, fmt.Errorf("scanning version from returned rows: %w", err) } - s.metrics.SecureValueSetStatusDuration.Observe(time.Since(start).Seconds()) - return nil + return &version, nil } +// TODO: can this method + queries be removed? func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) { start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes( @@ -461,7 +223,7 @@ func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespa } if err := res.Scan( &row.Keeper, &row.Decrypters, - &row.Ref, &row.ExternalID); err != nil { + &row.Ref, &row.ExternalID, &row.Active); err != nil { return nil, fmt.Errorf("failed to scan secure value row: %w", err) } @@ -469,6 +231,10 @@ func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespa return nil, fmt.Errorf("read rows error: %w", err) } + if !row.Active { + return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row) + } + secureValue, err := row.toDecrypt() if err != nil { return nil, fmt.Errorf("convert to kubernetes object: %w", err) @@ -479,7 +245,7 @@ func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespa return secureValue, nil } -func (s *secureValueMetadataStorage) read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) { +func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) { req := readSecureValue{ SQLTemplate: sqltemplate.New(s.dialect), Namespace: namespace.String(), @@ -508,8 +274,7 @@ func (s *secureValueMetadataStorage) read(ctx context.Context, namespace xkube.N &secureValue.Annotations, &secureValue.Labels, &secureValue.Created, &secureValue.CreatedBy, &secureValue.Updated, &secureValue.UpdatedBy, - &secureValue.Phase, &secureValue.Message, - &secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID); err != nil { + &secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID, &secureValue.Active, &secureValue.Version); err != nil { return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err) } @@ -518,3 +283,207 @@ func (s *secureValueMetadataStorage) read(ctx context.Context, namespace xkube.N } return secureValue, nil } + +func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv1beta1.SecureValue, error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Read", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + attribute.Bool("isForUpdate", opts.ForUpdate), + )) + defer span.End() + + secureValue, err := s.readActiveVersion(ctx, namespace, name, opts) + if err != nil { + return nil, err + } + + secureValueKub, err := secureValue.toKubernetes() + if err != nil { + return nil, fmt.Errorf("convert to kubernetes object: %w", err) + } + + s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds()) + s.metrics.SecureValueMetadataGetCount.Inc() + + return secureValueKub, nil +} + +func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (svList []secretv1beta1.SecureValue, error error) { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.List", trace.WithAttributes( + attribute.String("namespace", namespace.String()), + )) + defer span.End() + + defer func() { + span.SetAttributes(attribute.Int("returnedList.count", len(svList))) + }() + + req := listSecureValue{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + } + + q, err := sqltemplate.Execute(sqlSecureValueList, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueList.Name(), err) + } + + rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...) + if err != nil { + return nil, fmt.Errorf("listing secure values: %w", err) + } + defer func() { _ = rows.Close() }() + + secureValues := make([]secretv1beta1.SecureValue, 0) + for rows.Next() { + row := secureValueDB{} + + err = rows.Scan(&row.GUID, + &row.Name, &row.Namespace, &row.Annotations, + &row.Labels, + &row.Created, &row.CreatedBy, + &row.Updated, &row.UpdatedBy, + &row.Description, &row.Keeper, &row.Decrypters, + &row.Ref, &row.ExternalID, &row.Version, &row.Active, + ) + + if err != nil { + return nil, fmt.Errorf("error reading secure value row: %w", err) + } + + if !row.Active { + return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row) + } + + secureValue, err := row.toKubernetes() + if err != nil { + return nil, fmt.Errorf("convert to kubernetes object: %w", err) + } + + secureValues = append(secureValues, *secureValue) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("read rows error: %w", err) + } + + s.metrics.SecureValueMetadataListDuration.Observe(time.Since(start).Seconds()) + s.metrics.SecureValueMetadataListCount.Inc() + + return secureValues, nil +} + +func (s *secureValueMetadataStorage) SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + attribute.Int64("version", version), + )) + defer span.End() + + req := secureValueSetVersionToActive{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + Version: version, + } + + q, err := sqltemplate.Execute(sqlSecureValueSetVersionToActive, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueSetVersionToActive.Name(), err) + } + + res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) + if err != nil { + return fmt.Errorf("setting secure value version to active: namespace=%+v name=%+v version=%+v %w", namespace, name, version, err) + } + + // validate modified cound + modifiedCount, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("fetching number of modified rows: %w", err) + } + if modifiedCount == 0 { + return fmt.Errorf("expected to modify at least one row but modified 0: modifiedCount=%d", modifiedCount) + } + + return nil +} + +func (s *secureValueMetadataStorage) SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error { + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + attribute.Int64("version", version), + )) + defer span.End() + + req := secureValueSetVersionToInactive{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + Version: version, + } + + q, err := sqltemplate.Execute(sqlSecureValueSetVersionToInactive, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueSetVersionToInactive.Name(), err) + } + + res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) + if err != nil { + return fmt.Errorf("setting secure value version to active: namespace=%+v name=%+v version=%+v %w", namespace, name, version, err) + } + + modifiedCount, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("fetching number of modified rows: %w", err) + } + if modifiedCount > 1 { + return fmt.Errorf("expected to modify at at most one row but modified more: modifiedCount=%d", modifiedCount) + } + + return nil +} + +func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID contracts.ExternalID) error { + start := time.Now() + ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( + attribute.String("name", name), + attribute.String("namespace", namespace.String()), + attribute.String("externalID", externalID.String()), + attribute.Int64("version", version), + )) + defer span.End() + + req := updateExternalIdSecureValue{ + SQLTemplate: sqltemplate.New(s.dialect), + Namespace: namespace.String(), + Name: name, + Version: version, + ExternalID: externalID.String(), + } + + q, err := sqltemplate.Execute(sqlSecureValueUpdateExternalId, req) + if err != nil { + return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateExternalId.Name(), err) + } + + res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) + if err != nil { + return fmt.Errorf("setting secure value external id: namespace=%+v name=%+v externalID=%+v %w", namespace, name, externalID, err) + } + + // validate modified cound + modifiedCount, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("getting updated rows update external id secure value: %w", err) + } + if modifiedCount > 1 { + return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount) + } + s.metrics.SecureValueSetExternalIDDuration.Observe(time.Since(start).Seconds()) + + return nil +} diff --git a/pkg/storage/secret/metadata/secure_value_store_test.go b/pkg/storage/secret/metadata/secure_value_store_test.go index b9ec0023430..e0f77074e87 100644 --- a/pkg/storage/secret/metadata/secure_value_store_test.go +++ b/pkg/storage/secret/metadata/secure_value_store_test.go @@ -1,27 +1,29 @@ -package metadata +package metadata_test import ( "context" "testing" - secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/secret/database" - "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" + "k8s.io/utils/ptr" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/storage/secret/database" + "github.com/grafana/grafana/pkg/storage/secret/metadata" + "github.com/grafana/grafana/pkg/storage/secret/migrator" ) func createTestKeeper(t *testing.T, ctx context.Context, keeperStorage contracts.KeeperMetadataStorage, name, namespace string) string { t.Helper() - testKeeper := &secretv0alpha1.Keeper{ - Spec: secretv0alpha1.KeeperSpec{ + testKeeper := &secretv1beta1.Keeper{ + Spec: secretv1beta1.KeeperSpec{ Description: "test keeper description", - AWS: &secretv0alpha1.AWSKeeperConfig{}, + Aws: &secretv1beta1.KeeperAWSConfig{}, }, } testKeeper.Name = name @@ -40,14 +42,12 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { tracer := noop.NewTracerProvider().Tracer("test") db := database.ProvideDatabase(testDB, tracer) - features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) - // Initialize the secure value storage - secureValueStorage, err := ProvideSecureValueMetadataStorage(db, tracer, features, nil) + secureValueStorage, err := metadata.ProvideSecureValueMetadataStorage(db, tracer, nil) require.NoError(t, err) // Initialize the keeper storage - keeperStorage, err := ProvideKeeperMetadataStorage(db, tracer, features, nil) + keeperStorage, err := metadata.ProvideKeeperMetadataStorage(db, tracer, nil) require.NoError(t, err) t.Run("create and read a secure value", func(t *testing.T) { @@ -55,10 +55,10 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { keeperName := createTestKeeper(t, ctx, keeperStorage, "test-keeper", "default") // Create a test secure value - testSecureValue := &secretv0alpha1.SecureValue{ - Spec: secretv0alpha1.SecureValueSpec{ + testSecureValue := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ Description: "test description", - Value: "test-value", + Value: ptr.To(secretv1beta1.NewExposedSecureValue("test-value")), Keeper: &keeperName, }, } @@ -73,7 +73,8 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "default", createdSecureValue.Namespace) require.Equal(t, "test description", createdSecureValue.Spec.Description) require.Equal(t, keeperName, *createdSecureValue.Spec.Keeper) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, createdSecureValue.Status.Phase) + + require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version)) // Read the secure value back readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test", contracts.ReadOpts{}) @@ -83,7 +84,6 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "default", readSecureValue.Namespace) require.Equal(t, "test description", readSecureValue.Spec.Description) require.Equal(t, keeperName, *readSecureValue.Spec.Keeper) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, readSecureValue.Status.Phase) // List secure values and verify our value is in the list secureValues, err := secureValueStorage.List(ctx, xkube.Namespace("default")) @@ -98,7 +98,6 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "default", sv.Namespace) require.Equal(t, "test description", sv.Spec.Description) require.Equal(t, keeperName, *sv.Spec.Keeper) - require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase) break } } @@ -110,10 +109,10 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { keeperName := createTestKeeper(t, ctx, keeperStorage, "test-keeper-2", "default") // Create a test secure value - testSecureValue := &secretv0alpha1.SecureValue{ - Spec: secretv0alpha1.SecureValueSpec{ + testSecureValue := &secretv1beta1.SecureValue{ + Spec: secretv1beta1.SecureValueSpec{ Description: "test description 2", - Value: "test-value-2", + Value: ptr.To(secretv1beta1.NewExposedSecureValue("test-value-2")), Keeper: &keeperName, }, } @@ -125,6 +124,8 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.NoError(t, err) require.NotNil(t, createdSecureValue) + require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version)) + // Read the secure value to verify it exists readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test-2", contracts.ReadOpts{}) require.NoError(t, err) @@ -132,7 +133,7 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) { require.Equal(t, "sv-test-2", readSecureValue.Name) // Delete the secure value - err = secureValueStorage.Delete(ctx, xkube.Namespace("default"), "sv-test-2") + err = secureValueStorage.SetVersionToInactive(ctx, xkube.Namespace("default"), "sv-test-2", readSecureValue.Status.Version) require.NoError(t, err) // Try to read the deleted secure value - should return error diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go new file mode 100644 index 00000000000..856c1dfc021 --- /dev/null +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -0,0 +1,458 @@ +package metadata_test + +import ( + "fmt" + "slices" + "testing" + + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/mitchellh/copystructure" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "pgregory.net/rapid" +) + +type modelSecureValue struct { + *secretv1beta1.SecureValue + active bool +} + +// A simplified model of the grafana secrets manager +type model struct { + secureValues []*modelSecureValue +} + +func newModel() *model { + return &model{} +} + +func (m *model) getNewVersionNumber(namespace, name string) int64 { + latestVersion := int64(0) + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name { + latestVersion = max(latestVersion, sv.Status.Version) + } + } + return latestVersion + 1 +} + +func (m *model) setVersionToActive(namespace, name string, version int64) { + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name { + sv.active = sv.Status.Version == version + } + } +} + +func (m *model) setVersionToInactive(namespace, name string, version int64) { + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name && sv.Status.Version == version { + sv.active = false + return + } + } +} + +func (m *model) readActiveVersion(namespace, name string) *modelSecureValue { + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name && sv.active { + return sv + } + } + + return nil +} + +func (m *model) create(sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) { + modelSv := &modelSecureValue{sv, false} + modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) + modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) + m.secureValues = append(m.secureValues, modelSv) + m.setVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *model) update(newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) { + // If the payload doesn't contain a value, get the value from current version + if newSecureValue.Spec.Value == nil { + sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name) + if sv == nil { + return nil, false, contracts.ErrSecureValueNotFound + } + newSecureValue.Spec.Value = sv.Spec.Value + } + createdSv, err := m.create(newSecureValue, actorUID) + return createdSv, true, err +} + +func (m *model) delete(namespace, name string) (*secretv1beta1.SecureValue, error) { + modelSv := m.readActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + m.setVersionToInactive(namespace, name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *model) list(namespace string) (*secretv1beta1.SecureValueList, error) { + out := make([]secretv1beta1.SecureValue, 0) + + for _, v := range m.secureValues { + if v.Namespace == namespace && v.active { + out = append(out, *v.SecureValue) + } + } + + return &secretv1beta1.SecureValueList{Items: out}, nil +} + +func (m *model) decrypt(decrypter, namespace, name string) (map[string]service.DecryptResult, error) { + for _, v := range m.secureValues { + if v.Namespace == namespace && + v.Name == name && + v.active { + if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) { + return map[string]service.DecryptResult{ + name: service.NewDecryptResultValue(deepCopy(v).Spec.Value), + }, nil + } + + return map[string]service.DecryptResult{ + name: service.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), + }, nil + } + } + return map[string]service.DecryptResult{ + name: service.NewDecryptResultErr(contracts.ErrDecryptNotFound), + }, nil +} + +func (m *model) read(namespace, name string) (*secretv1beta1.SecureValue, error) { + modelSv := m.readActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + return modelSv.SecureValue, nil +} + +var ( + decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) + nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) + namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) + anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { + return &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: nameGen.Draw(t, "name"), + Namespace: namespaceGen.Draw(t, "ns"), + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), + Value: ptr.To(secretv1beta1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value"))), + Decrypters: rapid.SliceOfDistinct(decryptersGen, func(v string) string { return v }).Draw(t, "decrypters"), + }, + Status: secretv1beta1.SecureValueStatus{}, + } + }) + updateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv1beta1.SecureValue { + sv := anySecureValueGen.Draw(t, "sv") + // Maybe update the secret value, maybe not + if !rapid.Bool().Draw(t, "should_update_value") { + sv.Spec.Value = nil + } + return sv + }) + // Any secure value will do + deleteSecureValueGen = anySecureValueGen + decryptGen = rapid.Custom(func(t *rapid.T) decryptInput { + return decryptInput{ + namespace: namespaceGen.Draw(t, "ns"), + name: nameGen.Draw(t, "name"), + decrypter: decryptersGen.Draw(t, "decrypter"), + } + }) +) + +type decryptInput struct { + namespace string + name string + decrypter string +} + +func TestModel(t *testing.T) { + t.Parallel() + + sv := &secretv1beta1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv1beta1.SecureValueSpec{ + Description: "desc1", + Value: ptr.To(secretv1beta1.NewExposedSecureValue("v1")), + Decrypters: []string{"decrypter1"}, + }, + Status: secretv1beta1.SecureValueStatus{}, + } + + t.Run("creating secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // Create a secure value + sv1, err := m.create(deepCopy(sv), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv1.Namespace) + require.Equal(t, sv.Name, sv1.Name) + require.EqualValues(t, 1, sv1.Status.Version) + + // Create a new version of a secure value + sv2, err := m.create(deepCopy(sv), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv2.Namespace) + require.Equal(t, sv.Name, sv2.Name) + require.EqualValues(t, 2, sv2.Status.Version) + }) + + t.Run("updating secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + sv1, err := m.create(deepCopy(sv), "actor-uid") + require.NoError(t, err) + + // Create a new version of a secure value by updating it + sv2, _, err := m.update(deepCopy(sv1), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv2.Namespace) + require.Equal(t, sv.Name, sv2.Name) + require.EqualValues(t, 2, sv2.Status.Version) + + // Try updating a secure value that doesn't exist without specifying a value for it + sv3 := deepCopy(sv2) + sv3.Name = "i_dont_exist" + sv3.Spec.Value = nil + _, _, err = m.update(sv3, "actor-uid") + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + + // Updating a value that doesn't exist creates a new version + sv4 := deepCopy(sv3) + sv4.Name = "i_dont_exist" + sv4.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("sv4")) + sv4, _, err = m.update(sv4, "actor-uid") + require.NoError(t, err) + require.EqualValues(t, 1, sv4.Status.Version) + }) + + t.Run("deleting a secure value", func(t *testing.T) { + t.Parallel() + + m := newModel() + + sv1, err := m.create(deepCopy(sv), "actor-uid") + require.NoError(t, err) + + // Deleting a secure value + deletedSv, err := m.delete(sv1.Namespace, sv1.Name) + require.NoError(t, err) + require.Equal(t, sv1.Namespace, deletedSv.Namespace) + require.Equal(t, sv1.Name, deletedSv.Name) + require.EqualValues(t, sv1.Status.Version, deletedSv.Status.Version) + + // Deleting a secure value that doesn't exist results in an error + _, err = m.delete(sv1.Namespace, sv1.Name) + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + }) + + t.Run("listing secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // No secure values exist yet + list, err := m.list(sv.Namespace) + require.NoError(t, err) + require.Equal(t, 0, len(list.Items)) + + // Create a secure value + sv1, err := m.create(deepCopy(sv), "actor-uid") + require.NoError(t, err) + + // 1 secure value exists and it should be returned + list, err = m.list(sv.Namespace) + require.NoError(t, err) + require.Equal(t, 1, len(list.Items)) + require.Equal(t, sv1.Namespace, list.Items[0].Namespace) + require.Equal(t, sv1.Name, list.Items[0].Name) + require.EqualValues(t, sv1.Status.Version, list.Items[0].Status.Version) + }) + + t.Run("decrypting secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // Decrypting a secure value that does not exist + result, err := m.decrypt("decrypter", "namespace", "name") + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Nil(t, result["name"].Value()) + require.ErrorIs(t, result["name"].Error(), contracts.ErrDecryptNotFound) + + // Create a secure value + secret := "v1" + sv1, err := m.create(deepCopy(sv), "actor-uid") + require.NoError(t, err) + + // Decrypt the just created secure value + result, err = m.decrypt(sv1.Spec.Decrypters[0], sv1.Namespace, sv1.Name) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Nil(t, result[sv1.Name].Error()) + require.Equal(t, secret, result[sv1.Name].Value().DangerouslyExposeAndConsumeValue()) + }) +} + +func TestStateMachine(t *testing.T) { + t.Parallel() + + tt := t + + rapid.Check(t, func(t *rapid.T) { + sut := testutils.Setup(tt) + model := newModel() + + t.Repeat(map[string]func(*rapid.T){ + "create": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + + modelCreatedSv, modelErr := model.create(deepCopy(sv), "actor-uid") + + createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(deepCopy(sv))) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace) + require.Equal(t, modelCreatedSv.Name, createdSv.Name) + require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) + }, + "update": func(t *rapid.T) { + sv := updateSecureValueGen.Draw(t, "sv") + modelCreatedSv, _, modelErr := model.update(deepCopy(sv), "actor-uid") + createdSv, err := sut.UpdateSv(t.Context(), deepCopy(sv)) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace) + require.Equal(t, modelCreatedSv.Name, createdSv.Name) + require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) + }, + "delete": func(t *rapid.T) { + sv := deleteSecureValueGen.Draw(t, "sv") + modelSv, modelErr := model.delete(sv.Namespace, sv.Name) + deletedSv, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, deletedSv.Namespace) + require.Equal(t, modelSv.Name, deletedSv.Name) + require.Equal(t, modelSv.Status.Version, deletedSv.Status.Version) + }, + "list": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + authCtx := testutils.CreateUserAuthContext(t.Context(), sv.Namespace, map[string][]string{ + "securevalues:read": {"securevalues:uid:*"}, + }) + modelList, modelErr := model.list(sv.Namespace) + list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(sv.Namespace)) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + + require.Equal(t, len(modelList.Items), len(list.Items)) + + // PERFORMANCE: The lists are always small + for _, v1 := range modelList.Items { + if !slices.ContainsFunc(list.Items, func(v2 secretv1beta1.SecureValue) bool { + return v2.Namespace == v1.Namespace && v2.Name == v1.Name && v2.Status.Version == v1.Status.Version + }) { + t.Fatalf("expected sut to return secure value ns=%+v name=%+v version=%+v in the result", v1.Namespace, v1.Name, v1.Status.Version) + } + } + }, + "get": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + modelSv, modelErr := model.read(sv.Namespace, sv.Name) + readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv.Namespace), sv.Name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, readSv.Namespace) + require.Equal(t, modelSv.Name, readSv.Name) + require.Equal(t, modelSv.Status.Version, readSv.Status.Version) + }, + "decrypt": func(t *rapid.T) { + input := decryptGen.Draw(t, "decryptInput") + authCtx := testutils.CreateServiceAuthContext(t.Context(), input.decrypter, []string{fmt.Sprintf("secret.grafana.app/securevalues/%+v:decrypt", input.name)}) + modelResult, modelErr := model.decrypt(input.decrypter, input.namespace, input.name) + result, err := sut.DecryptService.Decrypt(authCtx, input.namespace, input.name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + + require.Equal(t, len(modelResult), len(result)) + for name := range modelResult { + require.Equal(t, modelResult[name].Value(), result[name].Value()) + require.Equal(t, modelResult[name].Error(), result[name].Error()) + } + }, + }) + }) +} + +func TestSecureValueServiceExampleBased(t *testing.T) { + t.Parallel() + + t.Run("shouldn't be able to decrypt using deleted secure value", func(t *testing.T) { + t.Parallel() + + sut := testutils.Setup(t) + + sv, err := sut.CreateSv(t.Context()) + require.NoError(t, err) + + readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv.Namespace), sv.Name) + require.NoError(t, err) + require.Equal(t, sv.Status.Version, readSv.Status.Version) + + deletedSv, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name) + require.NoError(t, err) + require.Equal(t, sv.Status.Version, deletedSv.Status.Version) + + authCtx := testutils.CreateServiceAuthContext(t.Context(), sv.Spec.Decrypters[0], []string{fmt.Sprintf("secret.grafana.app/securevalues/%+v:decrypt", sv.Name)}) + result, err := sut.DecryptService.Decrypt(authCtx, sv.Namespace, sv.Name) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.ErrorIs(t, result[sv.Name].Error(), contracts.ErrDecryptNotFound) + }) +} + +func deepCopy[T any](sv T) T { + copied, err := copystructure.Copy(sv) + if err != nil { + panic(fmt.Sprintf("failed to copy secure value: %v", err)) + } + return copied.(T) +} diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql index 8dea7db9fa8..aadea1d47a9 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql @@ -8,8 +8,8 @@ INSERT INTO `secret_secure_value` ( `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, + `active`, + `version`, `description`, `keeper`, `decrypters`, @@ -25,8 +25,8 @@ INSERT INTO `secret_secure_value` ( 'user:ryan', 5678, 'user:cameron', - 'creating', - 'message_test', + FALSE, + 1, 'description', 'keeper_test', 'decrypters_test', diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql index fcaa4271d49..ff0875fc55c 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql @@ -8,7 +8,8 @@ INSERT INTO `secret_secure_value` ( `created_by`, `updated`, `updated_by`, - `status_phase`, + `active`, + `version`, `description`, `external_id` ) VALUES ( @@ -21,7 +22,8 @@ INSERT INTO `secret_secure_value` ( 'user:ryan', 5678, 'user:cameron', - 'creating', + FALSE, + 1, 'description', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql deleted file mode 100755 index e8597c9fc8b..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql new file mode 100755 index 00000000000..8118fc81489 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql @@ -0,0 +1,10 @@ +SELECT + `version` +FROM + `secret_secure_value` +WHERE + `namespace` = 'ns' AND + `name` = 'name' +ORDER BY `version` DESC +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql index c47a02c16b2..d73828bfae8 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql @@ -8,15 +8,17 @@ SELECT `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, `description`, `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `version`, + `active` FROM `secret_secure_value` -WHERE `namespace` = 'ns' +WHERE + `namespace` = 'ns' AND + `active` = true ORDER BY `updated` DESC ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql index 91ac79af6ae..91850979fdb 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql @@ -3,7 +3,9 @@ SELECT `keeper` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` IN ('a', 'b') +WHERE + `namespace` = 'ns' AND + `name` IN ('a', 'b') AND + `active` = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql deleted file mode 100755 index a1cff11dd5d..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql +++ /dev/null @@ -1,21 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `keeper_name`, - `external_id`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - '', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql deleted file mode 100755 index 139d262d49a..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `keeper_name`, - `external_id`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'keeper', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql deleted file mode 100755 index 62af967a284..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `keeper_name`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql deleted file mode 100755 index 924b7d6277f..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO `secret_secure_value_outbox` ( - `request_id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `external_id`, - `receive_count`, - `created` -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql deleted file mode 100755 index 2a6fc52ea18..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - `secret_secure_value_outbox` -WHERE - `id` = 1 -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql deleted file mode 100755 index 47b006df9e3..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - `id` -FROM `secret_secure_value_outbox` -ORDER BY id ASC -LIMIT 10 -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql deleted file mode 100755 index 710f96bf187..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT - `request_id`, - `id`, - `message_type`, - `name`, - `namespace`, - `encrypted_secret`, - `keeper_name`, - `external_id`, - `receive_count`, - `created` -FROM - `secret_secure_value_outbox` -WHERE - `id` IN (1, 2, 3) -ORDER BY - `id` ASC -FOR UPDATE SKIP LOCKED -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql deleted file mode 100755 index e9df2c03014..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - `secret_secure_value_outbox` -SET - `receive_count` = `receive_count` + 1 -WHERE - `id` IN (1, 2, 3) -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql index 8f433497d22..4d8525dd0a0 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql @@ -8,16 +8,18 @@ SELECT `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, `description`, `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `active`, + `version` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `active` = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql index b45713eb594..c42fd0037c5 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql @@ -8,15 +8,17 @@ SELECT `created_by`, `updated`, `updated_by`, - `status_phase`, - `status_message`, `description`, `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `active`, + `version` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `active` = true ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql index 2694f0376d8..9d9095cf1e7 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql @@ -2,9 +2,12 @@ SELECT `keeper`, `decrypters`, `ref`, - `external_id` + `external_id`, + `active` FROM `secret_secure_value` -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `active` = true ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql new file mode 100755 index 00000000000..ce98578b8b5 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql @@ -0,0 +1,8 @@ +UPDATE + `secret_secure_value` +SET + `active` = (`version` = 1) +WHERE + `namespace` = 'ns' AND + `name` = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql deleted file mode 100755 index 6b39e09d23f..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE - `secret_secure_value` -SET - `guid` = 'abc', - `name` = 'name', - `namespace` = 'ns', - `annotations` = '{"x":"XXXX"}', - `labels` = '{"a":"AAA", "b", "BBBB"}', - `created` = 1234, - `created_by` = 'user:ryan', - `updated` = 5678, - `updated_by` = 'user:cameron', - `status_phase` = 'creating', - `status_message` = 'message_test', - `description` = 'description', - `keeper` = 'keeper_test', - `decrypters` = 'decrypters_test', - `ref` = 'ref_test', - `external_id` = 'extId' -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql deleted file mode 100755 index 378a2934ec3..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE - `secret_secure_value` -SET - `guid` = 'abc', - `name` = 'name', - `namespace` = 'ns', - `annotations` = '{"x":"XXXX"}', - `labels` = '{"a":"AAA", "b", "BBBB"}', - `created` = 1234, - `created_by` = 'user:ryan', - `updated` = 5678, - `updated_by` = 'user:cameron', - `status_phase` = 'creating', - `description` = 'description', - `external_id` = 'extId' -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql index d6d7db45247..eb862fcb072 100755 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql +++ b/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE `secret_secure_value` SET `external_id` = 'extId' -WHERE `namespace` = 'ns' AND - `name` = 'name' +WHERE + `namespace` = 'ns' AND + `name` = 'name' AND + `version` = 0 ; diff --git a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql b/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql deleted file mode 100755 index 25815855c68..00000000000 --- a/pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - `secret_secure_value` -SET - `status_phase` = 'Succeeded', - `status_message` = 'message-1' -WHERE `namespace` = 'ns' AND - `name` = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql index d9f191a9c63..cf43e0c130c 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql @@ -8,8 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", - "status_message", + "active", + "version", "description", "keeper", "decrypters", @@ -25,8 +25,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', - 'message_test', + FALSE, + 1, 'description', 'keeper_test', 'decrypters_test', diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql index 69dd74be229..564c5728f14 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql @@ -8,7 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", + "active", + "version", "description", "external_id" ) VALUES ( @@ -21,7 +22,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', + FALSE, + 1, 'description', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql deleted file mode 100755 index f2fbdc59f70..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql new file mode 100755 index 00000000000..1549114582d --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql @@ -0,0 +1,10 @@ +SELECT + "version" +FROM + "secret_secure_value" +WHERE + "namespace" = 'ns' AND + "name" = 'name' +ORDER BY "version" DESC +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql index 681ee5708cd..40ee42ebcf7 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "version", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' +WHERE + "namespace" = 'ns' AND + "active" = true ORDER BY "updated" DESC ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql index 93a449fa17a..d476e297129 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql @@ -3,7 +3,9 @@ SELECT "keeper" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" IN ('a', 'b') +WHERE + "namespace" = 'ns' AND + "name" IN ('a', 'b') AND + "active" = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql deleted file mode 100755 index 3c2b2588c08..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql +++ /dev/null @@ -1,21 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - '', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql deleted file mode 100755 index bcecf30a749..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'keeper', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql deleted file mode 100755 index 81eb522e3a8..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql deleted file mode 100755 index 3c0e3abf5e8..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql deleted file mode 100755 index f0c8984c8a0..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - "secret_secure_value_outbox" -WHERE - "id" = 1 -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql deleted file mode 100755 index 4c590706948..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - "id" -FROM "secret_secure_value_outbox" -ORDER BY id ASC -LIMIT 10 -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql deleted file mode 100755 index f4602caba07..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT - "request_id", - "id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -FROM - "secret_secure_value_outbox" -WHERE - "id" IN (1, 2, 3) -ORDER BY - "id" ASC -FOR UPDATE SKIP LOCKED -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql deleted file mode 100755 index d9404fd8212..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - "secret_secure_value_outbox" -SET - "receive_count" = "receive_count" + 1 -WHERE - "id" IN (1, 2, 3) -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql index 76ca4a8c155..cce38e6db7a 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql @@ -8,16 +8,18 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true FOR UPDATE ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql index d7fbc77a21b..2d16c211a85 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql index 999e79c2fa1..2a764e2677a 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql @@ -2,9 +2,12 @@ SELECT "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql new file mode 100755 index 00000000000..d659f944431 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql @@ -0,0 +1,8 @@ +UPDATE + "secret_secure_value" +SET + "active" = ("version" = 1) +WHERE + "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql deleted file mode 100755 index 7371f13f6e5..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "status_message" = 'message_test', - "description" = 'description', - "keeper" = 'keeper_test', - "decrypters" = 'decrypters_test', - "ref" = 'ref_test', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql deleted file mode 100755 index 0524e525096..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "description" = 'description', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql index 3d9f474776e..29bedaa7afe 100755 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql +++ b/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE "secret_secure_value" SET "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "version" = 0 ; diff --git a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql b/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql deleted file mode 100755 index c5cf29a5e7a..00000000000 --- a/pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "status_phase" = 'Succeeded', - "status_message" = 'message-1' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql index d9f191a9c63..cf43e0c130c 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql @@ -8,8 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", - "status_message", + "active", + "version", "description", "keeper", "decrypters", @@ -25,8 +25,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', - 'message_test', + FALSE, + 1, 'description', 'keeper_test', 'decrypters_test', diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql index 69dd74be229..564c5728f14 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql @@ -8,7 +8,8 @@ INSERT INTO "secret_secure_value" ( "created_by", "updated", "updated_by", - "status_phase", + "active", + "version", "description", "external_id" ) VALUES ( @@ -21,7 +22,8 @@ INSERT INTO "secret_secure_value" ( 'user:ryan', 5678, 'user:cameron', - 'creating', + FALSE, + 1, 'description', 'extId' ); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql deleted file mode 100755 index f2fbdc59f70..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql new file mode 100755 index 00000000000..1549114582d --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql @@ -0,0 +1,10 @@ +SELECT + "version" +FROM + "secret_secure_value" +WHERE + "namespace" = 'ns' AND + "name" = 'name' +ORDER BY "version" DESC +LIMIT 1 +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql index 681ee5708cd..40ee42ebcf7 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "version", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' +WHERE + "namespace" = 'ns' AND + "active" = true ORDER BY "updated" DESC ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql index 48ae01800ae..22d2006de82 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql @@ -3,6 +3,8 @@ SELECT "keeper" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" IN ('a', 'b') +WHERE + "namespace" = 'ns' AND + "name" IN ('a', 'b') AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql deleted file mode 100755 index 3c2b2588c08..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql +++ /dev/null @@ -1,21 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - '', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql deleted file mode 100755 index bcecf30a749..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "keeper_name", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'keeper', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql deleted file mode 100755 index 81eb522e3a8..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'keeper', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql deleted file mode 100755 index 3c0e3abf5e8..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO "secret_secure_value_outbox" ( - "request_id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "external_id", - "receive_count", - "created" -) VALUES ( - '', - 'some-type', - 'name', - 'namespace', - 'encrypted', - 'external-id', - 0, - 1234 -); diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql deleted file mode 100755 index f0c8984c8a0..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql +++ /dev/null @@ -1,5 +0,0 @@ -DELETE FROM - "secret_secure_value_outbox" -WHERE - "id" = 1 -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_fetch_message_ids-basic.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_fetch_message_ids-basic.sql deleted file mode 100755 index 4c590706948..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_fetch_message_ids-basic.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT - "id" -FROM "secret_secure_value_outbox" -ORDER BY id ASC -LIMIT 10 -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_receiveN-basic.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_receiveN-basic.sql deleted file mode 100755 index 62b16f507ba..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_receiveN-basic.sql +++ /dev/null @@ -1,18 +0,0 @@ -SELECT - "request_id", - "id", - "message_type", - "name", - "namespace", - "encrypted_secret", - "keeper_name", - "external_id", - "receive_count", - "created" -FROM - "secret_secure_value_outbox" -WHERE - "id" IN (1, 2, 3) -ORDER BY - "id" ASC -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_update_receive_count-update-receive-count.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_update_receive_count-update-receive-count.sql deleted file mode 100755 index d9404fd8212..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_update_receive_count-update-receive-count.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE - "secret_secure_value_outbox" -SET - "receive_count" = "receive_count" + 1 -WHERE - "id" IN (1, 2, 3) -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql index d7fbc77a21b..2d16c211a85 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read-for-update.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql index d7fbc77a21b..2d16c211a85 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read-read.sql @@ -8,15 +8,17 @@ SELECT "created_by", "updated", "updated_by", - "status_phase", - "status_message", "description", "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active", + "version" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql index 999e79c2fa1..2a764e2677a 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_read_for_decrypt-read-for-decrypt.sql @@ -2,9 +2,12 @@ SELECT "keeper", "decrypters", "ref", - "external_id" + "external_id", + "active" FROM "secret_secure_value" -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "active" = true ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_set_version_to_active-set secure value version to active.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_set_version_to_active-set secure value version to active.sql new file mode 100755 index 00000000000..d659f944431 --- /dev/null +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_set_version_to_active-set secure value version to active.sql @@ -0,0 +1,8 @@ +UPDATE + "secret_secure_value" +SET + "active" = ("version" = 1) +WHERE + "namespace" = 'ns' AND + "name" = 'name' +; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-not-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-not-null.sql deleted file mode 100755 index 7371f13f6e5..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-not-null.sql +++ /dev/null @@ -1,22 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "status_message" = 'message_test', - "description" = 'description', - "keeper" = 'keeper_test', - "decrypters" = 'decrypters_test', - "ref" = 'ref_test', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-null.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-null.sql deleted file mode 100755 index 0524e525096..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_update-update-null.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "guid" = 'abc', - "name" = 'name', - "namespace" = 'ns', - "annotations" = '{"x":"XXXX"}', - "labels" = '{"a":"AAA", "b", "BBBB"}', - "created" = 1234, - "created_by" = 'user:ryan', - "updated" = 5678, - "updated_by" = 'user:cameron', - "status_phase" = 'creating', - "description" = 'description', - "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql index 3d9f474776e..29bedaa7afe 100755 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql +++ b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateExternalId-updateExternalId.sql @@ -2,6 +2,8 @@ UPDATE "secret_secure_value" SET "external_id" = 'extId' -WHERE "namespace" = 'ns' AND - "name" = 'name' +WHERE + "namespace" = 'ns' AND + "name" = 'name' AND + "version" = 0 ; diff --git a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateStatus-updateStatus.sql b/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateStatus-updateStatus.sql deleted file mode 100755 index c5cf29a5e7a..00000000000 --- a/pkg/storage/secret/metadata/testdata/sqlite--secure_value_updateStatus-updateStatus.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE - "secret_secure_value" -SET - "status_phase" = 'Succeeded', - "status_message" = 'message-1' -WHERE "namespace" = 'ns' AND - "name" = 'name' -; diff --git a/pkg/storage/secret/migrator/migrator.go b/pkg/storage/secret/migrator/migrator.go index f453d6dd3af..ee2a5a1c975 100644 --- a/pkg/storage/secret/migrator/migrator.go +++ b/pkg/storage/secret/migrator/migrator.go @@ -12,11 +12,10 @@ import ( ) const ( - TableNameKeeper = "secret_keeper" - TableNameSecureValue = "secret_secure_value" - TableNameSecureValueOutbox = "secret_secure_value_outbox" - TableNameDataKey = "secret_data_key" - TableNameEncryptedValue = "secret_encrypted_value" + TableNameKeeper = "secret_keeper" + TableNameSecureValue = "secret_secure_value" + TableNameDataKey = "secret_data_key" + TableNameEncryptedValue = "secret_encrypted_value" ) type SecretDB struct { @@ -61,18 +60,19 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { {Name: "updated_by", Type: migrator.DB_Text, Nullable: false}, // Kubernetes Status - {Name: "status_phase", Type: migrator.DB_Text, Nullable: false}, - {Name: "status_message", Type: migrator.DB_Text, Nullable: true}, + {Name: "external_id", Type: migrator.DB_Text, Nullable: false}, + {Name: "active", Type: migrator.DB_Bool, Nullable: false}, + {Name: "version", Type: migrator.DB_BigInt, Nullable: false}, // Spec {Name: "description", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Chosen arbitrarily, but should be enough. {Name: "keeper", Type: migrator.DB_NVarchar, Length: 253, Nullable: true}, // Keeper name, if not set, use default keeper. {Name: "decrypters", Type: migrator.DB_Text, Nullable: true}, {Name: "ref", Type: migrator.DB_NVarchar, Length: 1024, Nullable: true}, // Reference to third-party storage secret path.Chosen arbitrarily, but should be enough. - {Name: "external_id", Type: migrator.DB_Text, Nullable: false}, }, Indices: []*migrator.Index{ - {Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex}, + {Cols: []string{"namespace", "name", "version", "active"}, Type: migrator.UniqueIndex}, + {Cols: []string{"namespace", "name", "version"}, Type: migrator.UniqueIndex}, }, }) @@ -118,38 +118,21 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) { Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make. }) - tables = append(tables, migrator.Table{ + encryptedValueTable := migrator.Table{ Name: TableNameEncryptedValue, Columns: []*migrator.Column{ {Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s. - {Name: "uid", Type: migrator.DB_NVarchar, Length: 36, IsPrimaryKey: true}, // Fixed size of a UUID. + {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, + {Name: "version", Type: migrator.DB_BigInt, Nullable: false}, {Name: "encrypted_data", Type: migrator.DB_Blob, Nullable: false}, {Name: "created", Type: migrator.DB_BigInt, Nullable: false}, {Name: "updated", Type: migrator.DB_BigInt, Nullable: false}, }, - Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make. - }) - - tables = append(tables, migrator.Table{ - Name: TableNameSecureValueOutbox, - Columns: []*migrator.Column{ - {Name: "request_id", Type: migrator.DB_NVarchar, Length: 1024, Nullable: false}, // Safer upper limit because we hex-encode traceparent+tracestate to form the request_id. - {Name: "id", Type: migrator.DB_BigInt, Length: 36, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "message_type", Type: migrator.DB_NVarchar, Length: 16, Nullable: false}, - {Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s. - {Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s. - {Name: "encrypted_secret", Type: migrator.DB_Blob, Nullable: true}, - {Name: "keeper_name", Type: migrator.DB_NVarchar, Length: 253, Nullable: true}, // Keeper name, if not set, use default keeper. - {Name: "external_id", Type: migrator.DB_NVarchar, Length: 36, Nullable: true}, // Fixed size of a UUID. - {Name: "receive_count", Type: migrator.DB_SmallInt, Nullable: false}, - {Name: "created", Type: migrator.DB_BigInt, Nullable: false}, - }, Indices: []*migrator.Index{ - // There's only one operation per secret in the queue at all times, - // meaning the namespace + name combination should be unique - {Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex}, + {Cols: []string{"namespace", "name", "version"}, Type: migrator.UniqueIndex}, }, - }) + } + tables = append(tables, encryptedValueTable) // Initialize all tables for t := range tables { diff --git a/pkg/storage/unified/apistore/watcher_test.go b/pkg/storage/unified/apistore/watcher_test.go index c68415bca97..91a251b04d0 100644 --- a/pkg/storage/unified/apistore/watcher_test.go +++ b/pkg/storage/unified/apistore/watcher_test.go @@ -40,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + "github.com/grafana/grafana/pkg/tests" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -134,9 +135,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte _, err = server.IsHealthy(ctx, &resourcepb.HealthCheckRequest{}) require.NoError(t, err) case StorageTypeUnified: - if testing.Short() { - t.Skip("skipping integration test") - } + tests.SkipIntegrationTestInShortMode(t) dbstore := infraDB.InitTestDB(t) cfg := setting.NewCfg() @@ -190,7 +189,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte return ctx, store, destroyFunc, nil } -func TestWatch(t *testing.T) { +func TestIntegrationWatch(t *testing.T) { for _, s := range []StorageType{StorageTypeFile, StorageTypeUnified} { t.Run(string(s), func(t *testing.T) { ctx, store, destroyFunc, err := testSetup(t, withStorageType(s)) diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go index 4a2ea89d7a0..8a71049a741 100644 --- a/pkg/storage/unified/resource/access.go +++ b/pkg/storage/unified/resource/access.go @@ -137,8 +137,8 @@ func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req c if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) { span.SetAttributes(attribute.Bool("allowed", false)) span.SetStatus(codes.Error, "Namespace mismatch") - span.RecordError(claims.ErrNamespaceMissmatch) - return claims.CheckResponse{Allowed: false}, claims.ErrNamespaceMissmatch + span.RecordError(claims.ErrNamespaceMismatch) + return claims.CheckResponse{Allowed: false}, claims.ErrNamespaceMismatch } if !c.IsCompatibleWithRBAC(req.Group, req.Resource) { @@ -186,8 +186,8 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) { span.SetAttributes(attribute.Bool("allowed", false)) span.SetStatus(codes.Error, "Namespace mismatch") - span.RecordError(claims.ErrNamespaceMissmatch) - return nil, claims.ErrNamespaceMissmatch + span.RecordError(claims.ErrNamespaceMismatch) + return nil, claims.ErrNamespaceMismatch } if !c.IsCompatibleWithRBAC(req.Group, req.Resource) { diff --git a/pkg/storage/unified/resource/access_test.go b/pkg/storage/unified/resource/access_test.go index 5477c438f6e..2ff885deab2 100644 --- a/pkg/storage/unified/resource/access_test.go +++ b/pkg/storage/unified/resource/access_test.go @@ -148,8 +148,8 @@ func TestNamespaceMatching(t *testing.T) { if tt.expectError { require.Error(t, checkErr, "Check should return error") require.Error(t, compileErr, "Compile should return error") - assert.ErrorIs(t, checkErr, authlib.ErrNamespaceMissmatch, "Check should return namespace mismatch error") - assert.ErrorIs(t, compileErr, authlib.ErrNamespaceMissmatch, "Compile should return namespace mismatch error") + assert.ErrorIs(t, checkErr, authlib.ErrNamespaceMismatch, "Check should return namespace mismatch error") + assert.ErrorIs(t, compileErr, authlib.ErrNamespaceMismatch, "Compile should return namespace mismatch error") } else { assert.NoError(t, checkErr, "Check should not return error when namespaces match") assert.NoError(t, compileErr, "Compile should not return error when namespaces match") diff --git a/pkg/storage/unified/resource/datastore.go b/pkg/storage/unified/resource/datastore.go index 836008117ba..a3301062048 100644 --- a/pkg/storage/unified/resource/datastore.go +++ b/pkg/storage/unified/resource/datastore.go @@ -239,11 +239,7 @@ func (d *dataStore) Get(ctx context.Context, key DataKey) (io.ReadCloser, error) return nil, fmt.Errorf("invalid data key: %w", err) } - obj, err := d.kv.Get(ctx, dataSection, key.String()) - if err != nil { - return nil, err - } - return obj.Value, nil + return d.kv.Get(ctx, dataSection, key.String()) } func (d *dataStore) Save(ctx context.Context, key DataKey, value io.Reader) error { @@ -251,7 +247,17 @@ func (d *dataStore) Save(ctx context.Context, key DataKey, value io.Reader) erro return fmt.Errorf("invalid data key: %w", err) } - return d.kv.Save(ctx, dataSection, key.String(), value) + writer, err := d.kv.Save(ctx, dataSection, key.String()) + if err != nil { + return err + } + _, err = io.Copy(writer, value) + if err != nil { + _ = writer.Close() + return err + } + + return writer.Close() } func (d *dataStore) Delete(ctx context.Context, key DataKey) error { diff --git a/pkg/storage/unified/resource/eventstore.go b/pkg/storage/unified/resource/eventstore.go index 6070d1a6eeb..93f1922c5cc 100644 --- a/pkg/storage/unified/resource/eventstore.go +++ b/pkg/storage/unified/resource/eventstore.go @@ -1,7 +1,6 @@ package resource import ( - "bytes" "context" "encoding/json" "fmt" @@ -134,12 +133,17 @@ func (n *eventStore) Save(ctx context.Context, event Event) error { return fmt.Errorf("invalid event key: %w", err) } - var buf bytes.Buffer - encoder := json.NewEncoder(&buf) - if err := encoder.Encode(event); err != nil { + writer, err := n.kv.Save(ctx, eventsSection, eventKey.String()) + if err != nil { return err } - return n.kv.Save(ctx, eventsSection, eventKey.String(), &buf) + encoder := json.NewEncoder(writer) + if err := encoder.Encode(event); err != nil { + _ = writer.Close() + return err + } + + return writer.Close() } func (n *eventStore) Get(ctx context.Context, key EventKey) (Event, error) { @@ -147,16 +151,15 @@ func (n *eventStore) Get(ctx context.Context, key EventKey) (Event, error) { return Event{}, fmt.Errorf("invalid event key: %w", err) } - obj, err := n.kv.Get(ctx, eventsSection, key.String()) + reader, err := n.kv.Get(ctx, eventsSection, key.String()) if err != nil { return Event{}, err } + defer func() { _ = reader.Close() }() var event Event - if err = json.NewDecoder(obj.Value).Decode(&event); err != nil { - _ = obj.Value.Close() + if err = json.NewDecoder(reader).Decode(&event); err != nil { return Event{}, err } - defer func() { _ = obj.Value.Close() }() return event, nil } @@ -173,14 +176,16 @@ func (n *eventStore) ListSince(ctx context.Context, sinceRV int64) iter.Seq2[Eve if err != nil { return } - obj, err := n.kv.Get(ctx, eventsSection, key) + reader, err := n.kv.Get(ctx, eventsSection, key) if err != nil { return } var event Event - if err := json.NewDecoder(obj.Value).Decode(&event); err != nil { + if err := json.NewDecoder(reader).Decode(&event); err != nil { + _ = reader.Close() return } + _ = reader.Close() if !yield(event, nil) { return } diff --git a/pkg/storage/unified/resource/kv.go b/pkg/storage/unified/resource/kv.go index fe3b6cedede..613841c5994 100644 --- a/pkg/storage/unified/resource/kv.go +++ b/pkg/storage/unified/resource/kv.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "iter" + "regexp" "time" badger "github.com/dgraph-io/badger/v4" @@ -28,21 +29,15 @@ type ListOptions struct { Limit int64 // maximum number of results to return. 0 means no limit. } -// KVObject represents a key-value object -type KVObject struct { - Key string // the key of the object within the section - Value io.ReadCloser // the value of the object -} - type KV interface { // Keys returns all the keys in the store Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] - // Get retrieves a key-value pair from the store - Get(ctx context.Context, section string, key string) (KVObject, error) + // Get retrieves the value for a key from the store + Get(ctx context.Context, section string, key string) (io.ReadCloser, error) - // Save a new value - Save(ctx context.Context, section string, key string, value io.Reader) error + // Save a new value - returns a WriteCloser to write the value to + Save(ctx context.Context, section string, key string) (io.WriteCloser, error) // Delete a value Delete(ctx context.Context, section string, key string) error @@ -66,12 +61,16 @@ func NewBadgerKV(db *badger.DB) *badgerKV { } } -func (k *badgerKV) Get(ctx context.Context, section string, key string) (KVObject, error) { +func (k *badgerKV) Get(ctx context.Context, section string, key string) (io.ReadCloser, error) { + if k.db.IsClosed() { + return nil, fmt.Errorf("database is closed") + } + txn := k.db.NewTransaction(false) defer txn.Discard() if section == "" { - return KVObject{}, fmt.Errorf("section is required") + return nil, fmt.Errorf("section is required") } key = section + "/" + key @@ -79,49 +78,85 @@ func (k *badgerKV) Get(ctx context.Context, section string, key string) (KVObjec item, err := txn.Get([]byte(key)) if err != nil { if errors.Is(err, badger.ErrKeyNotFound) { - return KVObject{}, ErrNotFound + return nil, ErrNotFound } - return KVObject{}, err - } - - out := KVObject{ - Key: string(item.Key())[len(section)+1:], + return nil, err } // Get the value and create a reader from it value, err := item.ValueCopy(nil) if err != nil { - return KVObject{}, err + return nil, err } - out.Value = io.NopCloser(bytes.NewReader(value)) - - return out, nil + return io.NopCloser(bytes.NewReader(value)), nil } -func (k *badgerKV) Save(ctx context.Context, section string, key string, value io.Reader) error { - if section == "" { - return fmt.Errorf("section is required") +// badgerWriteCloser implements io.WriteCloser for badgerKV +type badgerWriteCloser struct { + db *badger.DB + keyWithSection string + buf *bytes.Buffer + closed bool +} + +// Write implements io.Writer +func (w *badgerWriteCloser) Write(p []byte) (int, error) { + if w.closed { + return 0, fmt.Errorf("write to closed writer") + } + return w.buf.Write(p) +} + +// Close implements io.Closer - stores the buffered data in BadgerDB +func (w *badgerWriteCloser) Close() error { + if w.closed { + return nil + } + w.closed = true + + if w.db.IsClosed() { + return fmt.Errorf("database is closed") } - key = section + "/" + key + data := w.buf.Bytes() - data, err := io.ReadAll(value) - if err != nil { - return fmt.Errorf("failed to read value: %w", err) - } - - txn := k.db.NewTransaction(true) + txn := w.db.NewTransaction(true) defer txn.Discard() - err = txn.Set([]byte(key), data) + err := txn.Set([]byte(w.keyWithSection), data) if err != nil { return err } return txn.Commit() } +func (k *badgerKV) Save(ctx context.Context, section string, key string) (io.WriteCloser, error) { + if k.db.IsClosed() { + return nil, fmt.Errorf("database is closed") + } + + if section == "" { + return nil, fmt.Errorf("section is required") + } + + if key == "" { + return nil, fmt.Errorf("key is required") + } + + return &badgerWriteCloser{ + db: k.db, + keyWithSection: section + "/" + key, + buf: &bytes.Buffer{}, + closed: false, + }, nil +} + func (k *badgerKV) Delete(ctx context.Context, section string, key string) error { + if k.db.IsClosed() { + return fmt.Errorf("database is closed") + } + if section == "" { return fmt.Errorf("section is required") } @@ -148,6 +183,11 @@ func (k *badgerKV) Delete(ctx context.Context, section string, key string) error } func (k *badgerKV) Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] { + if k.db.IsClosed() { + return func(yield func(string, error) bool) { + yield("", fmt.Errorf("database is closed")) + } + } if section == "" { return func(yield func(string, error) bool) { yield("", fmt.Errorf("section is required")) @@ -216,3 +256,17 @@ func PrefixRangeEnd(prefix string) string { } return string(end) } + +var ( + // validKeyRegex validates keys used in the unified storage + // Keys can contain lowercase alphanumeric characters, '-', '.', '/', and '~' + // Any combination of these characters is allowed as long as the key is not empty + validKeyRegex = regexp.MustCompile(`^[a-z0-9./~-]+$`) +) + +func IsValidKey(key string) bool { + if key == "" { + return false + } + return validKeyRegex.MatchString(key) +} diff --git a/pkg/storage/unified/resource/kv_test.go b/pkg/storage/unified/resource/kv_test.go index 8ecf6a54751..27f24e19122 100644 --- a/pkg/storage/unified/resource/kv_test.go +++ b/pkg/storage/unified/resource/kv_test.go @@ -64,11 +64,10 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { expectedInternalKey := section + "/" + key // Save through KV interface - err := kv.Save(ctx, section, key, strings.NewReader(value)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value)) // Verify the raw key exists in badger with correct format - err = db.View(func(txn *badger.Txn) error { + err := db.View(func(txn *badger.Txn) error { item, err := txn.Get([]byte(expectedInternalKey)) require.NoError(t, err) @@ -90,13 +89,11 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { value2 := "value-from-section2" // Save same key in different sections - err := kv.Save(ctx, section1, key, strings.NewReader(value1)) - require.NoError(t, err) - err = kv.Save(ctx, section2, key, strings.NewReader(value2)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section1, key, strings.NewReader(value1)) + saveKVHelper(t, kv, ctx, section2, key, strings.NewReader(value2)) // Verify both keys exist in badger with different internal keys - err = db.View(func(txn *badger.Txn) error { + err := db.View(func(txn *badger.Txn) error { // Check section1 key item1, err := txn.Get([]byte(section1 + "/" + key)) require.NoError(t, err) @@ -116,20 +113,20 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { require.NoError(t, err) // Verify KV interface returns correct values for each section - obj1, err := kv.Get(ctx, section1, key) + reader1, err := kv.Get(ctx, section1, key) require.NoError(t, err) - val1, err := io.ReadAll(obj1.Value) + val1, err := io.ReadAll(reader1) require.NoError(t, err) require.Equal(t, value1, string(val1)) - err = obj1.Value.Close() + err = reader1.Close() require.NoError(t, err) - obj2, err := kv.Get(ctx, section2, key) + reader2, err := kv.Get(ctx, section2, key) require.NoError(t, err) - val2, err := io.ReadAll(obj2.Value) + val2, err := io.ReadAll(reader2) require.NoError(t, err) require.Equal(t, value2, string(val2)) - err = obj2.Value.Close() + err = reader2.Close() require.NoError(t, err) }) @@ -140,11 +137,10 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { internalKey := section + "/" + key // Save and verify it exists - err := kv.Save(ctx, section, key, strings.NewReader(value)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, key, strings.NewReader(value)) // Verify it exists in badger - err = db.View(func(txn *badger.Txn) error { + err := db.View(func(txn *badger.Txn) error { _, err := txn.Get([]byte(internalKey)) return err }) @@ -172,12 +168,10 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { keys2 := []string{"b1", "b2", "b3"} for _, k := range keys1 { - err := kv.Save(ctx, section1, k, strings.NewReader("value"+k)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section1, k, strings.NewReader("value"+k)) } for _, k := range keys2 { - err := kv.Save(ctx, section2, k, strings.NewReader("value"+k)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section2, k, strings.NewReader("value"+k)) } // List keys from section1 only @@ -222,3 +216,54 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { } }) } + +func TestIsValidKey(t *testing.T) { + tests := []struct { + name string + key string + expected bool + }{ + // Valid keys + {"simple key", "a", true}, + {"key with numbers", "a123", true}, + {"key with hyphens", "a-b-c", true}, + {"key with dots", "a.b.c", true}, + {"key with mixed", "a1-b2.c3", true}, + {"composite key with slash", "ns/group", true}, + {"composite key with tilde", "ns~action", true}, + {"complex composite key", "ns/group/resource/name", true}, + {"data key format", "ns/group/resource/name/123~created", true}, + {"metadata key format", "group/resource/ns/name/123~created~folder", true}, + {"metadata key format ending with a ~", "group/resource/ns/name/123~created~", true}, + + // invalid keys + {"empty key", "", false}, + {"uppercase letters", "Invalid", false}, + {"special characters", "a@b", false}, + {"spaces", "a b", false}, + {"leading space", " key", false}, + {"trailing space", "key ", false}, + {"tab character", "a\tb", false}, + {"newline character", "a\nb", false}, + {"underscores", "a_b", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidKey(tt.key) + require.Equal(t, tt.expected, result, + "IsValidKey(%q) = %v, expected %v", tt.key, result, tt.expected) + }) + } +} + +// saveKVHelper is a helper function to save data to KV store using the new WriteCloser interface +func saveKVHelper(t *testing.T, kv KV, ctx context.Context, section, key string, value io.Reader) { + t.Helper() + writer, err := kv.Save(ctx, section, key) + require.NoError(t, err) + _, err = io.Copy(writer, value) + require.NoError(t, err) + err = writer.Close() + require.NoError(t, err) +} diff --git a/pkg/storage/unified/resource/metadata.go b/pkg/storage/unified/resource/metadata.go index 0ed7d4ec493..83240f97727 100644 --- a/pkg/storage/unified/resource/metadata.go +++ b/pkg/storage/unified/resource/metadata.go @@ -1,7 +1,6 @@ package resource import ( - "bytes" "context" "encoding/json" "fmt" @@ -206,15 +205,15 @@ func (d *metadataStore) Get(ctx context.Context, key MetaDataKey) (MetaData, err return MetaData{}, fmt.Errorf("invalid metadata key: %w", err) } - obj, err := d.kv.Get(ctx, metaSection, key.String()) + reader, err := d.kv.Get(ctx, metaSection, key.String()) if err != nil { return MetaData{}, err } defer func() { - _ = obj.Value.Close() + _ = reader.Close() }() var meta MetaData - err = json.NewDecoder(obj.Value).Decode(&meta) + err = json.NewDecoder(reader).Decode(&meta) return meta, err } @@ -340,12 +339,18 @@ func (d *metadataStore) Save(ctx context.Context, obj MetaDataObj) error { if err := obj.Key.Validate(); err != nil { return fmt.Errorf("invalid metadata key: %w", err) } - var buf bytes.Buffer - encoder := json.NewEncoder(&buf) - if err := encoder.Encode(obj.Value); err != nil { + + writer, err := d.kv.Save(ctx, metaSection, obj.Key.String()) + if err != nil { return err } - return d.kv.Save(ctx, metaSection, obj.Key.String(), &buf) + encoder := json.NewEncoder(writer) + if err := encoder.Encode(obj.Value); err != nil { + _ = writer.Close() + return err + } + + return writer.Close() } // parseMetaDataKey parses a string key into a MetaDataKey struct diff --git a/pkg/storage/unified/resource/metadata_test.go b/pkg/storage/unified/resource/metadata_test.go index 70c3ea26a32..2e4816a3c32 100644 --- a/pkg/storage/unified/resource/metadata_test.go +++ b/pkg/storage/unified/resource/metadata_test.go @@ -110,11 +110,12 @@ func TestMetadataStore_Save(t *testing.T) { }) require.NoError(t, err) // Verify in the kv store that the metadata is saved - retrievedObj, err := store.kv.Get(ctx, metaSection, key.String()) + reader, err := store.kv.Get(ctx, metaSection, key.String()) require.NoError(t, err) - assert.Equal(t, key.String(), retrievedObj.Key) var retrivedMeta MetaData - actualData, err := io.ReadAll(retrievedObj.Value) + actualData, err := io.ReadAll(reader) + require.NoError(t, err) + err = reader.Close() require.NoError(t, err) err = json.Unmarshal(actualData, &retrivedMeta) require.NoError(t, err) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 2cc2b6f8b46..0a88d9a035a 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -14,6 +14,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -80,28 +81,16 @@ type ResourceIndex interface { // SearchBackend contains the technology specific logic to support search type SearchBackend interface { - // This will return nil if the key does not exist + // GetIndex returns existing index, or nil. GetIndex(ctx context.Context, key NamespacedResource) (ResourceIndex, error) - // Build an index from scratch - BuildIndex(ctx context.Context, - key NamespacedResource, + // BuildIndex builds an index from scratch. + // Depending on the size, the backend may choose different options (eg: memory vs disk). + // The last known resource version can be used to detect that nothing has changed, and existing on-disk index can be reused. + // The builder will write all documents before returning. + BuildIndex(ctx context.Context, key NamespacedResource, size int64, resourceVersion int64, nonStandardFields SearchableDocumentFields, builder func(index ResourceIndex) (int64, error)) (ResourceIndex, error) - // When the size is known, it will be passed along here - // Depending on the size, the backend may choose different options (eg: memory vs disk) - size int64, - - // The last known resource version (can be used to know that nothing has changed) - resourceVersion int64, - - // The non-standard index fields - fields SearchableDocumentFields, - - // The builder will write all documents before returning - builder func(index ResourceIndex) (int64, error), - ) (ResourceIndex, error) - - // Gets the total number of documents across all indexes + // TotalDocs returns the total number of documents across all indexes. TotalDocs() int64 } @@ -120,6 +109,8 @@ type searchSupport struct { initMinSize int initMaxSize int + buildIndex singleflight.Group + // Index queue processors indexQueueProcessorsMutex sync.Mutex indexQueueProcessors map[string]*indexQueueProcessor @@ -608,24 +599,53 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"GetOrCreateIndex") defer span.End() - // TODO??? - // We want to block while building the index and return the same index for the key - // simple mutex not great... we don't want to block while anything in building, just the same key idx, err := s.search.GetIndex(ctx, key) if err != nil { return nil, err } - if idx == nil { - idx, _, err = s.build(ctx, key, 10, 0) // unknown size and RV + if idx != nil { + return idx, nil + } + + idxInt, err, _ := s.buildIndex.Do(key.String(), func() (interface{}, error) { + // Recheck if some other goroutine managed to build an index in the meantime. + // (That is, it finished running this function and stored the index into the cache) + idx, err := s.search.GetIndex(ctx, key) + if err == nil && idx != nil { + return idx, nil + } + + // Get correct value of size + RV for building the index. This is important for our Bleve + // backend to decide whether to build index in-memory or as file-based. + stats, err := s.storage.GetResourceStats(ctx, key.Namespace, 0) + if err != nil { + return nil, fmt.Errorf("failed to get resource stats: %w", err) + } + + size := int64(0) + rv := int64(0) + for _, stat := range stats { + if stat.Namespace == key.Namespace && stat.Group == key.Group && stat.Resource == key.Resource { + size = stat.Count + rv = stat.ResourceVersion + break + } + } + + idx, _, err = s.build(ctx, key, size, rv) if err != nil { return nil, fmt.Errorf("error building search index, %w", err) } if idx == nil { return nil, fmt.Errorf("nil index after build") } + return idx, nil + }) + if err != nil { + return nil, err } - return idx, nil + return idxInt.(ResourceIndex), nil } func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64) (ResourceIndex, int64, error) { @@ -640,8 +660,6 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size } fields := s.builders.GetFields(nsr) - logger.Debug("Building index", "resource", nsr.Resource, "size", size, "rv", rv) - index, err := s.search.BuildIndex(ctx, nsr, size, rv, fields, func(index ResourceIndex) (int64, error) { rv, err = s.storage.ListIterator(ctx, &resourcepb.ListRequest{ Limit: 1000000000000, // big number diff --git a/pkg/storage/unified/resource/search_server_distributor.go b/pkg/storage/unified/resource/search_server_distributor.go index 4fbb7083aca..9637e3055cb 100644 --- a/pkg/storage/unified/resource/search_server_distributor.go +++ b/pkg/storage/unified/resource/search_server_distributor.go @@ -41,11 +41,8 @@ func ProvideSearchDistributorServer(cfg *setting.Cfg, features featuremgmt.Featu grpcServer := grpcHandler.GetServer() - resourcepb.RegisterResourceStoreServer(grpcServer, distributorServer) - // resourcepb.RegisterBulkStoreServer(grpcServer, distributorServer) resourcepb.RegisterResourceIndexServer(grpcServer, distributorServer) resourcepb.RegisterManagedObjectIndexServer(grpcServer, distributorServer) - resourcepb.RegisterBlobStoreServer(grpcServer, distributorServer) grpc_health_v1.RegisterHealthServer(grpcServer, healthService) _, err = grpcserver.ProvideReflectionService(cfg, grpcHandler) if err != nil { @@ -106,86 +103,6 @@ func (ds *distributorServer) GetStats(ctx context.Context, r *resourcepb.Resourc return client.GetStats(ctx, r) } -func (ds *distributorServer) Read(ctx context.Context, r *resourcepb.ReadRequest) (*resourcepb.ReadResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Read") - if err != nil { - return nil, err - } - - return client.Read(ctx, r) -} - -func (ds *distributorServer) Create(ctx context.Context, r *resourcepb.CreateRequest) (*resourcepb.CreateResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Create") - if err != nil { - return nil, err - } - - return client.Create(ctx, r) -} - -func (ds *distributorServer) Update(ctx context.Context, r *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Update") - if err != nil { - return nil, err - } - - return client.Update(ctx, r) -} - -func (ds *distributorServer) Delete(ctx context.Context, r *resourcepb.DeleteRequest) (*resourcepb.DeleteResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Key.Namespace, "Delete") - if err != nil { - return nil, err - } - - return client.Delete(ctx, r) -} - -func (ds *distributorServer) List(ctx context.Context, r *resourcepb.ListRequest) (*resourcepb.ListResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace, "List") - if err != nil { - return nil, err - } - - return client.List(ctx, r) -} - -func (ds *distributorServer) Watch(r *resourcepb.WatchRequest, srv resourcepb.ResourceStore_WatchServer) error { - // r -> consumer watch request - // srv -> stream connection with consumer - ctx := srv.Context() - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace, "Watch") - if err != nil { - return err - } - - // watchClient -> stream connection with storage-api pod - watchClient, err := client.Watch(ctx, r) - if err != nil { - return err - } - - // WARNING - // in Watch, all messages flow from the resource server (watchClient) to the consumer (srv) - // but since this is a streaming connection, in theory the consumer could also send a message to the server - // however for the sake of simplicity we are not handling it here - // but if we decide to handle bi-directional message passing in this method, we will need to update this - // we also never handle EOF err, as the server never closes the connection willingly - for { - msg, err := watchClient.Recv() - if err != nil { - return err - } - _ = srv.Send(msg) - } -} - -// TODO implement this if we want to support it in cloud -// func (ds *DistributorServer) BulkProcess(srv BulkStore_BulkProcessServer) error { -// return nil -// } - func (ds *distributorServer) CountManagedObjects(ctx context.Context, r *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) { ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Namespace, "CountManagedObjects") if err != nil { @@ -204,24 +121,6 @@ func (ds *distributorServer) ListManagedObjects(ctx context.Context, r *resource return client.ListManagedObjects(ctx, r) } -func (ds *distributorServer) PutBlob(ctx context.Context, r *resourcepb.PutBlobRequest) (*resourcepb.PutBlobResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Resource.Namespace, "PutBlob") - if err != nil { - return nil, err - } - - return client.PutBlob(ctx, r) -} - -func (ds *distributorServer) GetBlob(ctx context.Context, r *resourcepb.GetBlobRequest) (*resourcepb.GetBlobResponse, error) { - ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Resource.Namespace, "GetBlob") - if err != nil { - return nil, err - } - - return client.GetBlob(ctx, r) -} - func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, namespace string, methodName string) (context.Context, ResourceClient, error) { ringHasher := fnv.New32a() _, err := ringHasher.Write([]byte(namespace)) diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index b474eac8f9d..25f7f3280c0 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -2,7 +2,9 @@ package resource import ( "context" + "sync" "testing" + "time" "github.com/grafana/authlib/types" "github.com/stretchr/testify/mock" @@ -96,6 +98,7 @@ func (m *mockStorageBackend) ListHistory(ctx context.Context, req *resourcepb.Li // mockSearchBackend implements SearchBackend for testing with tracking capabilities type mockSearchBackend struct { + mu sync.Mutex buildIndexCalls []buildIndexCall buildEmptyIndexCalls []buildEmptyIndexCall } @@ -129,6 +132,9 @@ func (m *mockSearchBackend) BuildIndex(ctx context.Context, key NamespacedResour return nil, err } + m.mu.Lock() + defer m.mu.Unlock() + // Determine if this is an empty index based on size // Empty indexes are characterized by size == 0 if size == 0 { @@ -271,3 +277,58 @@ func TestBuildIndexes_MaxCountThreshold(t *testing.T) { }) } } + +func TestSearchGetOrCreateIndex(t *testing.T) { + // Setup mock implementations + storage := &mockStorageBackend{ + resourceStats: []ResourceStats{ + {NamespacedResource: NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, Count: 50, ResourceVersion: 11111111}, + }, + } + search := &mockSearchBackend{ + buildIndexCalls: []buildIndexCall{}, + buildEmptyIndexCalls: []buildEmptyIndexCall{}, + } + supplier := &TestDocumentBuilderSupplier{ + GroupsResources: map[string]string{ + "group": "resource", + }, + } + + // Create search support with the specified initMaxSize + opts := SearchOptions{ + Backend: search, + Resources: supplier, + WorkerThreads: 1, + InitMinCount: 1, // set min count to default for this test + InitMaxCount: 0, + } + + support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil) + require.NoError(t, err) + require.NotNil(t, support) + + start := make(chan struct{}) + + const concurrency = 100 + wg := sync.WaitGroup{} + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + _, _ = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}) + }() + } + + // Wait a bit for goroutines to start (hopefully) + time.Sleep(10 * time.Millisecond) + // Unblock all goroutines. + close(start) + wg.Wait() + + require.NotEmpty(t, search.buildIndexCalls) + require.Less(t, len(search.buildIndexCalls), concurrency, "Should not have built index more than a few times (ideally once)") + require.Equal(t, int64(50), search.buildIndexCalls[0].size) + require.Equal(t, int64(11111111), search.buildIndexCalls[0].resourceVersion) +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 35dc9de717b..b06ee3e7240 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -404,6 +404,8 @@ func (s *server) Stop(ctx context.Context) error { } // Old value indicates an update -- otherwise a create +// +//nolint:gocyclo func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resourcepb.ResourceKey, value, oldValue []byte) (*WriteEvent, *resourcepb.ErrorResult) { tmp := &unstructured.Unstructured{} err := tmp.UnmarshalJSON(value) @@ -436,6 +438,16 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resour return nil, NewBadRequestError("can not save annotation: " + utils.AnnoKeyGrantPermissions) } + // Verify that this resource can reference secure values + secure, err := obj.GetSecureValues() + if err != nil { + return nil, AsErrorResult(err) + } + if len(secure) > 0 { + // See: https://github.com/grafana/grafana/pull/107803 + return nil, NewBadRequestError("Saving secure values is not yet supported") + } + event := &WriteEvent{ Value: value, Key: key, @@ -782,6 +794,11 @@ func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resource return nil, apierrors.NewBadRequest( fmt.Sprintf("unable to read previous object, %v", err)) } + oldObj, err := utils.MetaAccessor(marker) + if err != nil { + return nil, err + } + obj, err := utils.MetaAccessor(marker) if err != nil { return nil, err @@ -793,6 +810,8 @@ func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resource obj.SetUpdatedBy(user.GetUID()) obj.SetGeneration(utils.DeletedGeneration) obj.SetAnnotation(utils.AnnoKeyKubectlLastAppliedConfig, "") // clears it + event.ObjectOld = oldObj + event.Object = obj event.Value, err = marker.MarshalJSON() if err != nil { return nil, apierrors.NewBadRequest( diff --git a/pkg/storage/unified/resource/storage_backend.go b/pkg/storage/unified/resource/storage_backend.go index 53a898e50b2..1fb0ad2e66a 100644 --- a/pkg/storage/unified/resource/storage_backend.go +++ b/pkg/storage/unified/resource/storage_backend.go @@ -63,6 +63,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in } rv := k.snowflake.Generate().Int64() + obj := event.Object // Write data. var action DataAction switch event.Type { @@ -87,10 +88,15 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in action = DataActionUpdated case resourcepb.WatchEvent_DELETED: action = DataActionDeleted + obj = event.ObjectOld default: return 0, fmt.Errorf("invalid event type: %d", event.Type) } + if obj == nil { + return 0, fmt.Errorf("object is nil") + } + // Build the search document doc, err := k.builder.BuildDocument(ctx, event.Key, rv, event.Value) if err != nil { @@ -119,7 +125,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in Name: event.Key.Name, ResourceVersion: rv, Action: action, - Folder: event.Object.GetFolder(), + Folder: obj.GetFolder(), }, Value: MetaData{ IndexableDocument: *doc, @@ -137,7 +143,7 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in Name: event.Key.Name, ResourceVersion: rv, Action: action, - Folder: event.Object.GetFolder(), + Folder: obj.GetFolder(), PreviousRV: event.PreviousRV, }) if err != nil { @@ -209,7 +215,6 @@ func (k *kvStorageBackend) ListIterator(ctx context.Context, req *resourcepb.Lis // Fetch the latest objects keys := make([]MetaDataKey, 0, min(defaultListBufferSize, req.Limit+1)) - idx := 0 for metaKey, err := range k.metaStore.ListResourceKeysAtRevision(ctx, MetaListRequestKey{ Namespace: req.Options.Key.Namespace, Group: req.Options.Key.Group, @@ -219,17 +224,15 @@ func (k *kvStorageBackend) ListIterator(ctx context.Context, req *resourcepb.Lis if err != nil { return 0, err } - // Skip the first offset items. This is not efficient, but it's a simple way to implement it for now. - if idx < int(offset) { - idx++ - continue - } keys = append(keys, metaKey) - // Only fetch the first limit items + 1 to get the next token. - if len(keys) >= int(req.Limit+1) { - break - } } + + sortMetaKeysByResourceVersion(keys, true) // sort ascending for sql parity + + if offset > 0 && int64(len(keys)) > offset { + keys = keys[offset:] + } + iter := kvListIterator{ keys: keys, currentIndex: -1, @@ -427,6 +430,19 @@ func sortByResourceVersion(filteredKeys []DataKey, sortAscending bool) { } } +// sortMetaKeysByResourceVersion sorts the metadata keys based on the sortAscending flag +func sortMetaKeysByResourceVersion(keys []MetaDataKey, sortAscending bool) { + if sortAscending { + sort.Slice(keys, func(i, j int) bool { + return keys[i].ResourceVersion < keys[j].ResourceVersion + }) + } else { + sort.Slice(keys, func(i, j int) bool { + return keys[i].ResourceVersion > keys[j].ResourceVersion + }) + } +} + // applyPagination filters keys based on pagination parameters func applyPagination(keys []DataKey, lastSeenRV int64, sortAscending bool) []DataKey { if lastSeenRV == 0 { diff --git a/pkg/storage/unified/resource/storage_backend_test.go b/pkg/storage/unified/resource/storage_backend_test.go index 899f7b2de25..bb1288458d0 100644 --- a/pkg/storage/unified/resource/storage_backend_test.go +++ b/pkg/storage/unified/resource/storage_backend_test.go @@ -72,6 +72,7 @@ func TestKvStorageBackend_WriteEvent_Success(t *testing.T) { }, Value: objectToJSONBytes(t, testObj), Object: metaAccessor, + ObjectOld: metaAccessor, PreviousRV: 100, } @@ -799,6 +800,8 @@ func TestKvStorageBackend_ListTrash_Success(t *testing.T) { // Delete the resource writeEvent.Type = resourcepb.WatchEvent_DELETED writeEvent.PreviousRV = rv1 + writeEvent.Object = metaAccessor + writeEvent.ObjectOld = metaAccessor rv2, err := backend.WriteEvent(ctx, writeEvent) require.NoError(t, err) @@ -989,8 +992,12 @@ func writeObject(t *testing.T, backend *kvStorageBackend, obj *unstructured.Unst }, Value: objectToJSONBytes(t, obj), Object: metaAccessor, + ObjectOld: metaAccessor, PreviousRV: previousRV, } + if eventType == resourcepb.WatchEvent_ADDED { + writeEvent.ObjectOld = nil + } return backend.WriteEvent(context.Background(), writeEvent) } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 503bd7a8dfb..a2415c528b0 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -3,6 +3,7 @@ package search import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "math" @@ -11,6 +12,7 @@ import ( "slices" "strconv" "strings" + "sync" "time" "github.com/blevesearch/bleve/v2" @@ -31,7 +33,6 @@ import ( authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -39,8 +40,6 @@ import ( const ( // tracingPrexfixBleve is the prefix used for tracing spans in the Bleve backend tracingPrexfixBleve = "unified_search.bleve." - // Default index cache cleanup TTL is 1 minute - indexCacheCleanupInterval = time.Minute ) var _ resource.SearchBackend = &bleveBackend{} @@ -57,7 +56,7 @@ type BleveOptions struct { // ?? not totally sure the units BatchSize int - // Index cache TTL for bleve indices + // Index cache TTL for bleve indices. 0 disables expiration for in-memory indexes. IndexCacheTTL time.Duration } @@ -65,9 +64,9 @@ type bleveBackend struct { tracer trace.Tracer log *slog.Logger opts BleveOptions - start time.Time - cache *localcache.CacheService + cacheMx sync.RWMutex + cache map[resource.NamespacedResource]*bleveIndex features featuremgmt.FeatureToggles indexMetrics *resource.BleveIndexMetrics @@ -77,6 +76,12 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, features featuremgm if opts.Root == "" { return nil, fmt.Errorf("bleve backend missing root folder configuration") } + absRoot, err := filepath.Abs(opts.Root) + if err != nil { + return nil, fmt.Errorf("error getting absolute path for bleve root folder %w", err) + } + opts.Root = absRoot + root, err := os.Stat(opts.Root) if err != nil { return nil, fmt.Errorf("error opening bleve root folder %w", err) @@ -85,35 +90,64 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, features featuremgm return nil, fmt.Errorf("bleve root is configured against a file (not folder)") } - bleveBackend := &bleveBackend{ + be := &bleveBackend{ log: slog.Default().With("logger", "bleve-backend"), tracer: tracer, - cache: localcache.New(opts.IndexCacheTTL, indexCacheCleanupInterval), + cache: map[resource.NamespacedResource]*bleveIndex{}, opts: opts, - start: time.Now(), features: features, indexMetrics: indexMetrics, } - go bleveBackend.updateIndexSizeMetric(opts.Root) + go be.updateIndexSizeMetric(opts.Root) - return bleveBackend, nil + return be, nil } -// This will return nil if the key does not exist -func (b *bleveBackend) GetIndex(ctx context.Context, key resource.NamespacedResource) (resource.ResourceIndex, error) { - val, ok := b.cache.Get(key.String()) - if !ok { +// GetIndex will return nil if the key does not exist +func (b *bleveBackend) GetIndex(_ context.Context, key resource.NamespacedResource) (resource.ResourceIndex, error) { + idx := b.getCachedIndex(key) + // Avoid returning typed nils. + if idx == nil { return nil, nil } - - idx, ok := val.(*bleveIndex) - if !ok { - return nil, fmt.Errorf("cache item is not a bleve index: %s", key.String()) - } return idx, nil } +func (b *bleveBackend) getCachedIndex(key resource.NamespacedResource) *bleveIndex { + // Check index with read-lock first. + b.cacheMx.RLock() + val := b.cache[key] + b.cacheMx.RUnlock() + + if val == nil { + return nil + } + + if val.expiration.IsZero() || val.expiration.After(time.Now()) { + // Not expired yet. + return val + } + + // We're dealing with expired index. We need to remove it from the cache and close it. + b.cacheMx.Lock() + val = b.cache[key] + delete(b.cache, key) + b.cacheMx.Unlock() + + if val == nil { + return nil + } + + // Index is no longer in the cache, but we need to close it. + err := val.index.Close() + if err != nil { + b.log.Error("failed to close index", "key", key, "err", err) + } + b.log.Info("index evicted from cache", "key", key) + return nil +} + // updateIndexSizeMetric sets the total size of all file-based indices metric. func (b *bleveBackend) updateIndexSizeMetric(indexPath string) { if b.indexMetrics == nil { @@ -147,28 +181,21 @@ func (b *bleveBackend) updateIndexSizeMetric(indexPath string) { } } -// Build an index from scratch -func (b *bleveBackend) BuildIndex(ctx context.Context, +// BuildIndex builds an index from scratch. +// If built successfully, the new index replaces the old index in the cache (if there was any). +func (b *bleveBackend) BuildIndex( + ctx context.Context, key resource.NamespacedResource, - - // When the size is known, it will be passed along here - // Depending on the size, the backend may choose different options (eg: memory vs disk) size int64, - - // The last known resource version can be used to know that we can skip calling the builder resourceVersion int64, - - // the non-standard searchable fields fields resource.SearchableDocumentFields, - - // The builder will write all documents before returning builder func(index resource.ResourceIndex) (int64, error), ) (resource.ResourceIndex, error) { _, span := b.tracer.Start(ctx, tracingPrexfixBleve+"BuildIndex") defer span.End() - var err error var index bleve.Index + fileIndexName := "" // Name of the file-based index, or empty for in-memory indexes. build := true mapper, err := GetBleveMappings(fields) @@ -176,61 +203,62 @@ func (b *bleveBackend) BuildIndex(ctx context.Context, return nil, err } - if size > b.opts.FileThreshold { - resourceDir := filepath.Join(b.opts.Root, key.Namespace, - fmt.Sprintf("%s.%s", key.Resource, key.Group), - ) - fname := fmt.Sprintf("rv%d", resourceVersion) - if resourceVersion == 0 { - fname = b.start.Format("tmp-20060102-150405") - } - dir := filepath.Join(resourceDir, fname) - if !isValidPath(dir, b.opts.Root) { - b.log.Error("Directory is not valid", "directory", dir) - } - if resourceVersion > 0 { - info, _ := os.Stat(dir) - if info != nil && info.IsDir() { - index, err = bleve.Open(dir) // NOTE, will use the same mappings!!! - if err == nil { - found, err := index.DocCount() - if err != nil || int64(found) != size { - b.log.Info("this size changed since the last time the index opened") - _ = index.Close() + cachedIndex := b.getCachedIndex(key) - // Pick a new file name - fname = b.start.Format("tmp-20060102-150405-changed") - dir = filepath.Join(resourceDir, fname) - index = nil - } else { - build = false // no need to build the index - } + logWithDetails := b.log.With("namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "size", size, "rv", resourceVersion) + + resourceDir := filepath.Join(b.opts.Root, cleanFileSegment(key.Namespace), cleanFileSegment(fmt.Sprintf("%s.%s", key.Resource, key.Group))) + + if size > b.opts.FileThreshold { + // We only check for the existing file-based index if we don't already have an open index for this key. + // This happens on startup, or when memory-based index has expired. (We don't expire file-based indexes) + // If we do have an unexpired cached index already, we always build a new index from scratch. + if cachedIndex == nil && resourceVersion > 0 { + index, fileIndexName = b.findPreviousFileBasedIndex(resourceDir, resourceVersion, size) + } + + if index != nil { + build = false + logWithDetails.Debug("Existing index found on filesystem", "directory", filepath.Join(resourceDir, fileIndexName)) + } else { + // Building index from scratch. Index name has a time component in it to be unique, but if + // we happen to create non-unique name, we bump the time and try again. + + indexDir := "" + now := time.Now() + for index == nil { + fileIndexName = formatIndexName(time.Now(), resourceVersion) + indexDir = filepath.Join(resourceDir, fileIndexName) + if !isPathWithinRoot(indexDir, b.opts.Root) { + return nil, fmt.Errorf("invalid path %s", indexDir) + } + + index, err = bleve.New(indexDir, mapper) + if errors.Is(err, bleve.ErrorIndexPathExists) { + now = now.Add(time.Second) // Bump time for next try + index = nil // Bleve actually returns non-nil value with ErrorIndexPathExists + continue + } + if err != nil { + return nil, fmt.Errorf("error creating new bleve index: %s %w", indexDir, err) } } + + logWithDetails.Info("Building index using filesystem", "directory", indexDir) } - if index == nil { - index, err = bleve.New(dir, mapper) - if err != nil { - err = fmt.Errorf("error creating new bleve index: %s %w", dir, err) - } - } - - // Start a background task to cleanup the old index directories - if index != nil && err == nil { - go b.cleanOldIndexes(resourceDir, fname) - } if b.indexMetrics != nil { b.indexMetrics.IndexTenants.WithLabelValues("file").Inc() } } else { index, err = bleve.NewMemOnly(mapper) + if err != nil { + return nil, fmt.Errorf("error creating new in-memory bleve index: %w", err) + } if b.indexMetrics != nil { b.indexMetrics.IndexTenants.WithLabelValues("memory").Inc() } - } - if err != nil { - return nil, err + logWithDetails.Info("Building index using memory") } // Batch all the changes @@ -249,28 +277,72 @@ func (b *bleveBackend) BuildIndex(ctx context.Context, } if build { + start := time.Now() _, err = builder(idx) if err != nil { return nil, err } + elapsed := time.Since(start) + logWithDetails.Info("Finished building index", "elapsed", elapsed) } - b.cache.SetDefault(key.String(), idx) + // Set expiration after building the index. Only expire in-memory indexes. + if fileIndexName == "" && b.opts.IndexCacheTTL > 0 { + idx.expiration = time.Now().Add(b.opts.IndexCacheTTL) + } + + // Store the index in the cache. + if idx.expiration.IsZero() { + logWithDetails.Info("Storing index in cache, with no expiration", "key", key) + } else { + logWithDetails.Info("Storing index in cache", "key", key, "expiration", idx.expiration) + } + + b.cacheMx.Lock() + prev := b.cache[key] + b.cache[key] = idx + b.cacheMx.Unlock() + + // If there was a previous index in the cache, close it. + if prev != nil { + err := prev.index.Close() + if err != nil { + logWithDetails.Error("failed to close previous index", "key", key, "err", err) + } + } + + // Start a background task to cleanup the old index directories. If we have built a new file-based index, + // the new name is ignored. If we have created in-memory index and fileIndexName is empty, all old directories can be removed. + go b.cleanOldIndexes(resourceDir, fileIndexName) + return idx, nil } -func (b *bleveBackend) cleanOldIndexes(dir string, skip string) { +func cleanFileSegment(input string) string { + input = strings.ReplaceAll(input, string(filepath.Separator), "_") + input = strings.ReplaceAll(input, "..", "_") + return input +} + +// cleanOldIndexes deletes all subdirectories inside dir, skipping directory with "skipName". +// "skipName" can be empty. +func (b *bleveBackend) cleanOldIndexes(dir string, skipName string) { files, err := os.ReadDir(dir) if err != nil { + if os.IsNotExist(err) { + return + } b.log.Warn("error cleaning folders from", "directory", dir, "error", err) return } for _, file := range files { - if file.IsDir() && file.Name() != skip { + if file.IsDir() && file.Name() != skipName { fpath := filepath.Join(dir, file.Name()) - if !isValidPath(dir, b.opts.Root) { - b.log.Error("Path is not valid", "directory", fpath, "error", err) + if !isPathWithinRoot(fpath, b.opts.Root) { + b.log.Warn("Skipping cleanup of directory", "directory", fpath) + continue } + err = os.RemoveAll(fpath) if err != nil { b.log.Error("Unable to remove old index folder", "directory", fpath, "error", err) @@ -281,31 +353,45 @@ func (b *bleveBackend) cleanOldIndexes(dir string, skip string) { } } -// isValidPath does a sanity check in case it tries to access a different dir -func isValidPath(path, safeDir string) bool { - if path == "" || safeDir == "" { +// isPathWithinRoot verifies that path is within given absoluteRoot. +func isPathWithinRoot(path, absoluteRoot string) bool { + if path == "" || absoluteRoot == "" { return false } - cleanPath := filepath.Clean(path) - cleanSafeDir := filepath.Clean(safeDir) - rel, err := filepath.Rel(cleanSafeDir, cleanPath) + path, err := filepath.Abs(path) if err != nil { return false } - return !strings.HasPrefix(rel, "..") && !strings.Contains(rel, "\\") + if !strings.HasPrefix(path, absoluteRoot) { + return false + } + return true +} + +// cacheKeys returns list of keys for indexes in the cache (including possibly expired ones). +func (b *bleveBackend) cacheKeys() []resource.NamespacedResource { + b.cacheMx.RLock() + defer b.cacheMx.RUnlock() + + keys := make([]resource.NamespacedResource, 0, len(b.cache)) + for k := range b.cache { + keys = append(keys, k) + } + return keys } // TotalDocs returns the total number of documents across all indices func (b *bleveBackend) TotalDocs() int64 { var totalDocs int64 - for _, v := range b.cache.Items() { - idx, ok := v.Object.(*bleveIndex) - if !ok { - b.log.Warn("cache item is not a bleve index", "key", v.Object) + // We iterate over keys and call getCachedIndex for each index individually. + // We do this to avoid keeping a lock for the entire TotalDocs function, since DocCount may be slow (due to disk access). + // Calling getCachedIndex also handles index expiration. + for _, key := range b.cacheKeys() { + idx := b.getCachedIndex(key) + if idx == nil { continue } - c, err := idx.index.DocCount() if err != nil { continue @@ -315,6 +401,74 @@ func (b *bleveBackend) TotalDocs() int64 { return totalDocs } +func formatIndexName(now time.Time, resourceVersion int64) string { + timestamp := now.Format("20060102-150405") + return fmt.Sprintf("%s-%d", timestamp, resourceVersion) +} + +func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string, resourceVersion int64, size int64) (bleve.Index, string) { + entries, err := os.ReadDir(resourceDir) + if err != nil { + return nil, "" + } + + indexName := "" + for _, ent := range entries { + if !ent.IsDir() { + continue + } + + parts := strings.Split(ent.Name(), "-") + if len(parts) != 3 { + continue + } + + // Last part is resourceVersion + indexRv, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + continue + } + if indexRv != resourceVersion { + continue + } + indexName = ent.Name() + break + } + + if indexName == "" { + return nil, "" + } + + indexDir := filepath.Join(resourceDir, indexName) + idx, err := bleve.Open(indexDir) + if err != nil { + return nil, "" + } + + cnt, err := idx.DocCount() + if err != nil { + _ = idx.Close() + return nil, "" + } + + if uint64(size) != cnt { + _ = idx.Close() + return nil, "" + } + + return idx, indexName +} + +func (b *bleveBackend) closeAllIndexes() { + b.cacheMx.Lock() + defer b.cacheMx.Unlock() + + for key, idx := range b.cache { + _ = idx.index.Close() + delete(b.cache, key) + } +} + type bleveIndex struct { key resource.NamespacedResource index bleve.Index @@ -322,6 +476,10 @@ type bleveIndex struct { standard resource.SearchableDocumentFields fields resource.SearchableDocumentFields + // When to expire and close the index. Zero value = no expiration. + // We only expire in-memory indexes. + expiration time.Time + // The values returned with all allFields []*resourcepb.ResourceTableColumnDefinition features featuremgmt.FeatureToggles diff --git a/pkg/storage/unified/search/bleve_integration_test.go b/pkg/storage/unified/search/bleve_integration_test.go index 6c28d560087..f76f89255e0 100644 --- a/pkg/storage/unified/search/bleve_integration_test.go +++ b/pkg/storage/unified/search/bleve_integration_test.go @@ -13,10 +13,6 @@ import ( ) func TestBleveSearchBackend(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - // Run the search backend test suite unitest.RunSearchBackendTest(t, func(ctx context.Context) resource.SearchBackend { tempDir := t.TempDir() diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index a2ea6afaced..b6e2bd05611 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -8,7 +8,9 @@ import ( "os" "path/filepath" "testing" + "time" + "github.com/blevesearch/bleve/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -671,65 +673,301 @@ func TestSafeInt64ToInt(t *testing.T) { } } -func Test_isValidPath(t *testing.T) { +func Test_isPathWithinRoot(t *testing.T) { tests := []struct { - name string - dir string - safeDir string - want bool + name string + dir string + root string + want bool }{ { - name: "valid path", - dir: "/path/to/my-file/", - safeDir: "/path/to/", - want: true, + name: "valid path", + dir: "/path/to/my-file/", + root: "/path/to/", + want: true, }, { - name: "valid path without trailing slash", - dir: "/path/to/my-file", - safeDir: "/path/to", - want: true, + name: "valid path without trailing slash", + dir: "/path/to/my-file", + root: "/path/to", + want: true, }, { - name: "path with double slashes", - dir: "/path//to//my-file/", - safeDir: "/path/to/", - want: true, + name: "path with double slashes", + dir: "/path//to//my-file/", + root: "/path/to/", + want: true, }, { - name: "invalid path: ..", - dir: "/path/../above/", - safeDir: "/path/to/", + name: "invalid path: ..", + dir: "/path/../above/", + root: "/path/to/", }, { - name: "invalid path: \\", - dir: "\\path/to", - safeDir: "/path/to/", + name: "invalid path: \\", + dir: "\\path/to", + root: "/path/to/", }, { - name: "invalid path: not under safe dir", - dir: "/path/to.txt", - safeDir: "/path/to/", + name: "invalid path: not under safe dir", + dir: "/path/to.txt", + root: "/path/to/", }, { - name: "invalid path: empty paths", - dir: "", - safeDir: "/path/to/", + name: "invalid path: empty paths", + dir: "", + root: "/path/to/", }, { - name: "invalid path: different path", - dir: "/other/path/to/my-file/", - safeDir: "/Some/other/path", + name: "invalid path: different path", + dir: "/other/path/to/my-file/", + root: "/Some/other/path", }, { - name: "invalid path: empty safe path", - dir: "/path/to/", - safeDir: "", + name: "invalid path: empty safe path", + dir: "/path/to/", + root: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, isValidPath(tt.dir, tt.safeDir)) + require.Equal(t, tt.want, isPathWithinRoot(tt.dir, tt.root)) }) } } + +func setupBleveBackend(t *testing.T, fileThreshold int, cacheTTL time.Duration, dir string) *bleveBackend { + if dir == "" { + dir = t.TempDir() + } + backend, err := NewBleveBackend(BleveOptions{ + Root: dir, + FileThreshold: int64(fileThreshold), + IndexCacheTTL: cacheTTL, + }, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering), nil) + require.NoError(t, err) + require.NotNil(t, backend) + t.Cleanup(backend.closeAllIndexes) + return backend +} + +func TestBleveInMemoryIndexExpiration(t *testing.T) { + backend := setupBleveBackend(t, 5, time.Nanosecond, "") + + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + builtIndex, err := backend.BuildIndex(context.Background(), ns, 1 /* below FileThreshold */, 100, nil, indexTestDocs(ns, 1)) + require.NoError(t, err) + + // Wait for index expiration, which is 1ns + time.Sleep(10 * time.Millisecond) + idx, err := backend.GetIndex(context.Background(), ns) + require.NoError(t, err) + require.Nil(t, idx) + + // Verify that builtIndex is now closed. + _, err = builtIndex.DocCount(context.Background(), "") + require.ErrorIs(t, err, bleve.ErrorIndexClosed) +} + +func TestBleveFileIndexExpiration(t *testing.T) { + backend := setupBleveBackend(t, 5, time.Nanosecond, "") + + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + // size=100 is above FileThreshold, this will be file-based index + builtIndex, err := backend.BuildIndex(context.Background(), ns, 100, 100, nil, indexTestDocs(ns, 1)) + require.NoError(t, err) + + // Wait for index expiration, which is 1ns + time.Sleep(10 * time.Millisecond) + idx, err := backend.GetIndex(context.Background(), ns) + require.NoError(t, err) + require.NotNil(t, idx) + + // Verify that builtIndex is still open. + cnt, err := builtIndex.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(1), cnt) +} + +func TestFileIndexIsReusedOnSameSizeAndRV(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + _, err := backend1.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, indexTestDocs(ns, 10)) + require.NoError(t, err) + backend1.closeAllIndexes() + + // We open new backend using same directory, and run indexing with same size (10) and RV (100). This should reuse existing index, and skip indexing. + backend2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 100, nil, indexTestDocs(ns, 1000)) + require.NoError(t, err) + + // Verify that we're reusing existing index and there is only 10 documents in it, not 1000. + cnt, err := idx.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(10), cnt) +} + +func TestFileIndexIsNotReusedOnDifferentSize(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, indexTestDocs(ns, 10)) + require.NoError(t, err) + backend1.closeAllIndexes() + + // We open new backend using same directory, but with different size. Index should be rebuilt. + backend2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + idx, err := backend2.BuildIndex(context.Background(), ns, 100, 100, nil, indexTestDocs(ns, 100)) + require.NoError(t, err) + + // Verify that index has updated number of documents. + cnt, err := idx.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(100), cnt) +} + +func TestFileIndexIsNotReusedOnDifferentRV(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + tmpDir := t.TempDir() + + backend1 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + _, err := backend1.BuildIndex(context.Background(), ns, 10, 100, nil, indexTestDocs(ns, 10)) + require.NoError(t, err) + backend1.closeAllIndexes() + + // We open new backend using same directory, but with different RV. Index should be rebuilt. + backend2 := setupBleveBackend(t, 5, time.Nanosecond, tmpDir) + idx, err := backend2.BuildIndex(context.Background(), ns, 10 /* file based */, 999999, nil, indexTestDocs(ns, 100)) + require.NoError(t, err) + + // Verify that index has updated number of documents. + cnt, err := idx.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(100), cnt) +} + +func TestRebuildingIndexClosesPreviousCachedIndex(t *testing.T) { + ns := resource.NamespacedResource{ + Namespace: "test", + Group: "group", + Resource: "resource", + } + + for name, testCase := range map[string]struct { + firstInMemory bool + secondInMemory bool + }{ + "in-memory, in-memory": {true, true}, + "in-memory, file": {true, false}, + "file, in-memory": {false, true}, + "file, file": {false, false}, + } { + t.Run(name, func(t *testing.T) { + backend := setupBleveBackend(t, 5, time.Nanosecond, "") + + firstSize := 100 + if testCase.firstInMemory { + firstSize = 1 + } + firstIndex, err := backend.BuildIndex(context.Background(), ns, int64(firstSize), 100, nil, indexTestDocs(ns, firstSize)) + require.NoError(t, err) + + secondSize := 100 + if testCase.firstInMemory { + secondSize = 1 + } + secondIndex, err := backend.BuildIndex(context.Background(), ns, int64(secondSize), 100, nil, indexTestDocs(ns, secondSize)) + require.NoError(t, err) + + // Verify that first and second index are different, and first one is now closed. + require.NotEqual(t, firstIndex, secondIndex) + + _, err = firstIndex.DocCount(context.Background(), "") + require.ErrorIs(t, err, bleve.ErrorIndexClosed) + + cnt, err := secondIndex.DocCount(context.Background(), "") + require.NoError(t, err) + require.Equal(t, int64(secondSize), cnt) + }) + } +} + +func indexTestDocs(ns resource.NamespacedResource, docs int) func(index resource.ResourceIndex) (int64, error) { + return func(index resource.ResourceIndex) (int64, error) { + var items []*resource.BulkIndexItem + for i := 0; i < docs; i++ { + items = append(items, &resource.BulkIndexItem{ + Action: resource.ActionIndex, + Doc: &resource.IndexableDocument{ + Key: &resourcepb.ResourceKey{ + Namespace: ns.Namespace, + Group: ns.Group, + Resource: ns.Resource, + Name: fmt.Sprintf("doc%d", i), + }, + Title: fmt.Sprintf("Document %d", i), + }, + }) + } + + err := index.BulkIndex(&resource.BulkIndexRequest{Items: items}) + return int64(docs), err + } +} + +func TestCleanOldIndexes(t *testing.T) { + dir := t.TempDir() + + b := setupBleveBackend(t, 5, time.Nanosecond, dir) + + t.Run("with skip", func(t *testing.T) { + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-1/a"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-2/b"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-3/c"), 0750)) + + b.cleanOldIndexes(dir, "index-2") + files, err := os.ReadDir(dir) + require.NoError(t, err) + require.Len(t, files, 1) + require.Equal(t, "index-2", files[0].Name()) + }) + + t.Run("without skip", func(t *testing.T) { + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-1/a"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-2/b"), 0750)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "index-3/c"), 0750)) + + b.cleanOldIndexes(dir, "") + files, err := os.ReadDir(dir) + require.NoError(t, err) + require.Len(t, files, 0) + }) +} diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index c9d2bf159be..a23cf9469ea 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -600,7 +600,7 @@ func (b *backend) listLatest(ctx context.Context, req *resourcepb.ListRequest, c return 0, fmt.Errorf("only works for the 'latest' resource version") } - iter := &listIter{sortAsc: false} + iter := &listIter{} err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error { var err error iter.listRV, err = b.fetchLatestRV(ctx, tx, b.dialect, req.Options.Key.Group, req.Options.Key.Resource) @@ -638,7 +638,7 @@ func (b *backend) listAtRevision(ctx context.Context, req *resourcepb.ListReques defer span.End() // Get the RV - iter := &listIter{listRV: req.ResourceVersion, sortAsc: false} + iter := &listIter{listRV: req.ResourceVersion} if req.NextPageToken != "" { continueToken, err := resource.GetContinueToken(req.NextPageToken) if err != nil { diff --git a/pkg/storage/unified/sql/data/resource_history_get.sql b/pkg/storage/unified/sql/data/resource_history_get.sql index 434ae8ffa01..fdb90d908b7 100644 --- a/pkg/storage/unified/sql/data/resource_history_get.sql +++ b/pkg/storage/unified/sql/data/resource_history_get.sql @@ -29,7 +29,7 @@ WHERE 1 = 1 AND {{ .Ident "resource_version" }} = {{ .Arg .ExactRV }} {{ end }} {{ if .SortAscending }} -ORDER BY resource_version ASC +ORDER BY {{ .Ident "resource_version" }} ASC {{ else }} -ORDER BY resource_version DESC +ORDER BY {{ .Ident "resource_version" }} DESC {{ end }} diff --git a/pkg/storage/unified/sql/data/resource_history_list.sql b/pkg/storage/unified/sql/data/resource_history_list.sql index 9a0bbe1fa78..58838c331cf 100644 --- a/pkg/storage/unified/sql/data/resource_history_list.sql +++ b/pkg/storage/unified/sql/data/resource_history_list.sql @@ -50,7 +50,7 @@ SELECT AND kv.{{ .Ident "name" }} = {{ .Arg .Request.Options.Key.Name }} {{ end }} {{ end }} - ORDER BY kv.{{ .Ident "namespace" }} ASC, kv.{{ .Ident "name" }} ASC + ORDER BY kv.{{ .Ident "resource_version" }} ASC {{ if (gt .Request.Limit 0) }} LIMIT {{ .Arg .Request.Limit }} OFFSET {{ .Arg .Request.Offset }} {{ end }} diff --git a/pkg/storage/unified/sql/data/resource_list.sql b/pkg/storage/unified/sql/data/resource_list.sql index 2041618a370..a2ee2f47435 100644 --- a/pkg/storage/unified/sql/data/resource_list.sql +++ b/pkg/storage/unified/sql/data/resource_list.sql @@ -23,5 +23,5 @@ SELECT AND {{ .Ident "name" }} = {{ .Arg .Request.Options.Key.Name }} {{ end }} {{ end }} - ORDER BY {{ .Ident "namespace" }} ASC, {{ .Ident "name" }} ASC + ORDER BY {{ .Ident "resource_version" }} ASC ; diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine.go b/pkg/storage/unified/sql/db/dbimpl/db_engine.go index 9712de24f02..28fe6f9483c 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine.go @@ -1,24 +1,14 @@ package dbimpl import ( - "cmp" "fmt" - "strings" "time" - "github.com/go-sql-driver/mysql" - "github.com/grafana/dskit/crypto/tls" - "github.com/grafana/grafana/pkg/util/xorm" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/unified/sql/db" ) -// tlsConfigName is the name of the TLS config that we register with the MySQL -// driver. -const tlsConfigName = "db_engine_tls" - func getEngine(config *sqlstore.DatabaseConfig) (*xorm.Engine, error) { switch config.Type { case dbTypeMySQL, dbTypePostgres, dbTypeSQLite: @@ -36,156 +26,3 @@ func getEngine(config *sqlstore.DatabaseConfig) (*xorm.Engine, error) { return nil, fmt.Errorf("unsupported database type: %s", config.Type) } } - -// Deprecated: use getEngine instead -func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { - config := mysql.NewConfig() - config.User = getter.String("user") - // accept the core Grafana jargon of `password` as well, originally Unified - // Storage used `pass` - config.Passwd = cmp.Or(getter.String("pass"), getter.String("password")) - config.Net = "tcp" - config.Addr = getter.String("host") - config.DBName = getter.String("name") - config.Params = map[string]string{ - // See: https://dev.mysql.com/doc/refman/en/sql-mode.html - "@@SESSION.sql_mode": "ANSI", - } - config.Collation = "utf8mb4_unicode_ci" - config.Loc = time.UTC - config.AllowNativePasswords = true - config.ClientFoundRows = true - config.ParseTime = true - - // Setup TLS for the database connection if configured. - if err := configureTLS(getter, config); err != nil { - return nil, fmt.Errorf("failed to configure TLS: %w", err) - } - - // allow executing multiple SQL statements in a single roundtrip, and also - // enable executing the CALL statement to run stored procedures that execute - // multiple SQL statements. - //config.MultiStatements = true - - if err := getter.Err(); err != nil { - return nil, fmt.Errorf("config error: %w", err) - } - - if strings.HasPrefix(config.Addr, "/") { - config.Net = "unix" - } - - engine, err := xorm.NewEngine(db.DriverMySQL, config.FormatDSN()) - if err != nil { - return nil, fmt.Errorf("open database: %w", err) - } - - engine.SetMaxOpenConns(getter.Int("max_open_conn", 0)) - engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) - maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second - engine.SetConnMaxLifetime(maxLifetime) - - return engine, nil -} - -func configureTLS(getter confGetter, config *mysql.Config) error { - sslMode := getter.String("ssl_mode") - - if sslMode == "true" || sslMode == "skip-verify" { - tlsCfg := tls.ClientConfig{ - CAPath: getter.String("ca_cert_path"), - CertPath: getter.String("client_cert_path"), - KeyPath: getter.String("client_key_path"), - ServerName: getter.String("server_cert_name"), - } - - rawTLSCfg, err := tlsCfg.GetTLSConfig() - if err != nil { - return fmt.Errorf("failed to get TLS config for mysql: %w", err) - } - - if sslMode == "skip-verify" { - rawTLSCfg.InsecureSkipVerify = true - } - - if err := mysql.RegisterTLSConfig(tlsConfigName, rawTLSCfg); err != nil { - return fmt.Errorf("failed to register TLS config for mysql: %w", err) - } - - config.TLSConfig = tlsConfigName - } - - // If the TLS mode is set in the database config, we need to set it here. - if tls := getter.String("tls"); tls != "" { - // If the user has provided TLS certs, we don't want to use the tls=, as - // they would override the TLS config that we set above. They both use the same - // parameter, so we need to check for that. - if sslMode == "true" { - return fmt.Errorf("cannot provide tls certs and tls= at the same time") - } - config.Params["tls"] = tls - } - - return nil -} - -// Deprecated: use getEngine instead -func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { - dsnKV := map[string]string{ - "user": getter.String("user"), - // accept the core Grafana jargon of `password` as well, originally - // Unified Storage used `pass` - "password": cmp.Or(getter.String("pass"), getter.String("password")), - "dbname": getter.String("name"), - "sslmode": cmp.Or(getter.String("ssl_mode"), "disable"), - "sslsni": getter.String("ssl_sni"), - "sslrootcert": getter.String("ca_cert_path"), - "sslkey": getter.String("client_key_path"), - "sslcert": getter.String("client_cert_path"), - } - - // TODO: probably interesting: - // "passfile", "statement_timeout", "lock_timeout", "connect_timeout" - - // TODO: for CockroachDB, we probably need to use the following: - // dsnKV["options"] = "-c enable_experimental_alter_column_type_general=true" - // Or otherwise specify it as: - // dsnKV["enable_experimental_alter_column_type_general"] = "true" - - // TODO: do we want to support these options in the DSN as well? - // "sslpassword", "krbspn", "krbsrvname", "target_session_attrs", "service", "servicefile" - - // More on Postgres connection string parameters: - // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING - - hostport := getter.String("host") - - if err := getter.Err(); err != nil { - return nil, fmt.Errorf("config error: %w", err) - } - - host, port, err := splitHostPortDefault(hostport, "127.0.0.1", "5432") - if err != nil { - return nil, fmt.Errorf("invalid host: %w", err) - } - dsnKV["host"] = host - dsnKV["port"] = port - - dsn, err := MakeDSN(dsnKV) - if err != nil { - return nil, fmt.Errorf("error building DSN: %w", err) - } - - // FIXME: get rid of xorm - engine, err := xorm.NewEngine(db.DriverPostgres, dsn) - if err != nil { - return nil, fmt.Errorf("open database: %w", err) - } - - engine.SetMaxOpenConns(getter.Int("max_open_conn", 0)) - engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) - maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second - engine.SetConnMaxLifetime(maxLifetime) - - return engine, nil -} diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go index fbaa147070a..d26f9ac1e62 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go @@ -1,17 +1,10 @@ package dbimpl import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" "os" "path/filepath" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,20 +12,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func newValidMySQLGetter(withKeyPrefix bool) confGetter { - var prefix string - if withKeyPrefix { - prefix = "db_" - } - return newTestConfGetter(map[string]string{ - prefix + "type": dbTypeMySQL, - prefix + "host": "/var/run/mysql.socket", - prefix + "name": "grafana", - prefix + "user": "user", - prefix + "password": "password", - }, prefix) -} - func TestNewResourceDbProvider(t *testing.T) { t.Parallel() @@ -88,7 +67,7 @@ func TestNewResourceDbProvider(t *testing.T) { engine, err := newResourceDBProvider(nil, cfg, nil) require.Error(t, err) require.Nil(t, engine) - require.Contains(t, err.Error(), "unknown") + require.Contains(t, err.Error(), "no database type specified") }) t.Run("Unknown database type", func(t *testing.T) { @@ -146,274 +125,3 @@ password = password require.Equal(t, dbTypeMySQL, engine.engine.Dialect().DriverName()) require.Contains(t, engine.engine.DataSourceName(), "overthere:3306") } - -func TestGetEngineMySQLFromConfig(t *testing.T) { - t.Parallel() - - t.Run("happy path - with key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(true)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("happy path - without key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(false)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("invalid string", func(t *testing.T) { - t.Parallel() - - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypeMySQL, - "db_host": "/var/run/mysql.socket", - "db_name": string(invalidUTF8ByteSequence), - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEngineMySQL(getter) - require.Nil(t, engine) - require.Error(t, err) - require.ErrorIs(t, err, errInvalidUTF8Sequence) - }) -} - -func newValidPostgresGetter(withKeyPrefix bool) confGetter { - var prefix string - if withKeyPrefix { - prefix = "db_" - } - return newTestConfGetter(map[string]string{ - prefix + "type": dbTypePostgres, - prefix + "host": "localhost", - prefix + "name": "grafana", - prefix + "user": "user", - prefix + "password": "password", - }, prefix) -} - -func TestGetEnginePostgresFromConfig(t *testing.T) { - t.Parallel() - - t.Run("happy path - with key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(true)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("happy path - without key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(false)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("invalid string", func(t *testing.T) { - t.Parallel() - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypePostgres, - "db_host": string(invalidUTF8ByteSequence), - "db_name": "grafana", - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEnginePostgres(getter) - - require.Nil(t, engine) - require.Error(t, err) - }) - - t.Run("invalid hostport", func(t *testing.T) { - t.Parallel() - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypePostgres, - "db_host": "1:1:1", - "db_name": "grafana", - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEnginePostgres(getter) - - require.Nil(t, engine) - require.Error(t, err) - }) -} - -func TestGetEngineMySQLTLS(t *testing.T) { - certs := generateTestCerts(t) - - tests := []struct { - name string - config map[string]string - shouldErr bool - }{ - { - name: "with TLS disabled", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "disable", - }, - }, - { - name: "with TLS skip-verify", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "skip-verify", - }, - }, - { - name: "with valid TLS certificates", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "true", - "ca_cert_path": certs.caFile, - "client_cert_path": certs.certFile, - "client_key_path": certs.keyFile, - "server_cert_name": "mysql.example.com", - }, - }, - { - name: "with invalid cert paths", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "true", - "ca_cert_path": "nonexistent/ca.pem", - "client_cert_path": "nonexistent/client-cert.pem", - "client_key_path": "nonexistent/client-key.pem", - "server_cert_name": "mysql.example.com", - }, - shouldErr: true, - }, - { - name: "with TLS certs and tls parameter", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "true", - "ca_cert_path": certs.caFile, - "client_cert_path": certs.certFile, - "client_key_path": certs.keyFile, - "server_cert_name": "mysql.example.com", - "tls": "preferred", - }, - shouldErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - getter := newTestConfGetter(tt.config, "") - engine, err := getEngineMySQL(getter) - - if tt.shouldErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - require.NotNil(t, engine) - }) - } -} - -type testCerts struct { - caFile string - certFile string - keyFile string -} - -func generateTestCerts(t *testing.T) testCerts { - t.Helper() - tempDir := t.TempDir() - - // Generate CA private key - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - // Generate CA certificate - ca := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: "Test CA", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - IsCA: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - } - - caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caKey.PublicKey, caKey) - require.NoError(t, err) - - clientKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - client := &x509.Certificate{ - SerialNumber: big.NewInt(2), - Subject: pkix.Name{ - CommonName: "Test Client", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - SubjectKeyId: []byte{1, 2, 3, 4, 5}, - } - - clientBytes, err := x509.CreateCertificate(rand.Reader, client, ca, &clientKey.PublicKey, caKey) - require.NoError(t, err) - - // Write certificates and keys to temporary files - caFile := filepath.Join(tempDir, "ca.pem") - certFile := filepath.Join(tempDir, "cert.pem") - keyFile := filepath.Join(tempDir, "key.pem") - - writePEMFile(t, caFile, "CERTIFICATE", caBytes) - writePEMFile(t, certFile, "CERTIFICATE", clientBytes) - writePEMFile(t, keyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(clientKey)) - - return testCerts{ - caFile: caFile, - certFile: certFile, - keyFile: keyFile, - } -} - -func writePEMFile(t *testing.T, filename string, blockType string, bytes []byte) { - t.Helper() - //nolint:gosec - file, err := os.Create(filename) - require.NoError(t, err) - //nolint:errcheck - defer file.Close() - - err = pem.Encode(file, &pem.Block{ - Type: blockType, - Bytes: bytes, - }) - require.NoError(t, err) -} diff --git a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go index 0109d994afd..83d41adb625 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go +++ b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go @@ -62,62 +62,36 @@ type resourceDBProvider struct { initErr error } -func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace.Tracer) (p *resourceDBProvider, err error) { - // Resource API has other configs in its section besides database ones, so - // we prefix them with "db_". We use the database config from core Grafana - // as fallback, and as it uses a dedicated INI section, then keys are not - // prefixed with "db_" - getter := newConfGetter(cfg.SectionWithEnvOverrides("resource_api"), "db_") - fallbackConfig, fallbackErr := sqlstore.NewDatabaseConfig(cfg, nil) - if fallbackErr != nil { - // Ignore error here and keep going. - fallbackConfig = nil - } - - logger := log.New("entity-db") - p = &resourceDBProvider{ +func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace.Tracer) (*resourceDBProvider, error) { + logger := log.New("resource-db") + p := &resourceDBProvider{ cfg: cfg, log: logger, - logQueries: getter.Bool("log_queries"), migrateFunc: migrations.MigrateResourceStore, tracer: tracer, } - dbType := getter.String("type") + dbType := cfg.SectionWithEnvOverrides("database").Key("type").String() + switch { - // Deprecated: First try with the config in the "resource_api" section, which is specific to Unified Storage - case dbType == dbTypePostgres: - logger.Info("Using resource_api section", "db_type", dbType) - p.registerMetrics = true - p.engine, err = getEnginePostgres(getter) - return p, err - - case dbType == dbTypeMySQL: - logger.Info("Using resource_api section", "db_type", dbType) - p.registerMetrics = true - p.engine, err = getEngineMySQL(getter) - return p, err - case dbType != "": - return p, fmt.Errorf("invalid db type specified: %s", dbType) - - // If we have an empty Resource API db config, try with the core Grafana database config - case fallbackConfig != nil && fallbackConfig.Type != "": - logger.Info("Using database section", "db_type", fallbackConfig.Type) + logger.Info("Using database section", "db_type", dbType) + dbCfg, err := sqlstore.NewDatabaseConfig(cfg, nil) + if err != nil { + return nil, err + } p.registerMetrics = true - p.engine, err = getEngine(fallbackConfig) + p.engine, err = getEngine(dbCfg) return p, err case grafanaDB != nil: - // try to use the grafana db connection (should only happen in tests) + // Try to use the grafana db connection, should only happen in tests. if newConfGetter(cfg.SectionWithEnvOverrides("database"), "").Bool(grafanaDBInstrumentQueriesKey) { return nil, errGrafanaDBInstrumentedNotSupported } p.engine = grafanaDB.GetEngine() + p.logQueries = cfg.SectionWithEnvOverrides("database").Key("log_queries").MustBool(false) return p, nil default: - if fallbackErr != nil { - return nil, fallbackErr - } return nil, fmt.Errorf("no database type specified") } } diff --git a/pkg/storage/unified/sql/db/mocks/DB.go b/pkg/storage/unified/sql/db/mocks/DB.go index ceda9e2865d..0177a0b3c1e 100644 --- a/pkg/storage/unified/sql/db/mocks/DB.go +++ b/pkg/storage/unified/sql/db/mocks/DB.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -83,7 +83,7 @@ func (_c *DB_BeginTx_Call) RunAndReturn(run func(context.Context, *sql.TxOptions return _c } -// DriverName provides a mock function with given fields: +// DriverName provides a mock function with no fields func (_m *DB) DriverName() string { ret := _m.Called() @@ -374,7 +374,7 @@ func (_c *DB_QueryRowContext_Call) RunAndReturn(run func(context.Context, string return _c } -// Stats provides a mock function with given fields: +// Stats provides a mock function with no fields func (_m *DB) Stats() sql.DBStats { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/Row.go b/pkg/storage/unified/sql/db/mocks/Row.go index 26b1b68c178..b4d0228d3c1 100644 --- a/pkg/storage/unified/sql/db/mocks/Row.go +++ b/pkg/storage/unified/sql/db/mocks/Row.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,7 +17,7 @@ func (_m *Row) EXPECT() *Row_Expecter { return &Row_Expecter{mock: &_m.Mock} } -// Err provides a mock function with given fields: +// Err provides a mock function with no fields func (_m *Row) Err() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/Rows.go b/pkg/storage/unified/sql/db/mocks/Rows.go index 33beddd5eed..dd653194844 100644 --- a/pkg/storage/unified/sql/db/mocks/Rows.go +++ b/pkg/storage/unified/sql/db/mocks/Rows.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,7 +17,7 @@ func (_m *Rows) EXPECT() *Rows_Expecter { return &Rows_Expecter{mock: &_m.Mock} } -// Close provides a mock function with given fields: +// Close provides a mock function with no fields func (_m *Rows) Close() error { ret := _m.Called() @@ -62,7 +62,7 @@ func (_c *Rows_Close_Call) RunAndReturn(run func() error) *Rows_Close_Call { return _c } -// Err provides a mock function with given fields: +// Err provides a mock function with no fields func (_m *Rows) Err() error { ret := _m.Called() @@ -107,7 +107,7 @@ func (_c *Rows_Err_Call) RunAndReturn(run func() error) *Rows_Err_Call { return _c } -// Next provides a mock function with given fields: +// Next provides a mock function with no fields func (_m *Rows) Next() bool { ret := _m.Called() @@ -152,7 +152,7 @@ func (_c *Rows_Next_Call) RunAndReturn(run func() bool) *Rows_Next_Call { return _c } -// NextResultSet provides a mock function with given fields: +// NextResultSet provides a mock function with no fields func (_m *Rows) NextResultSet() bool { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/Tx.go b/pkg/storage/unified/sql/db/mocks/Tx.go index 342e12a493c..4f70231aae3 100644 --- a/pkg/storage/unified/sql/db/mocks/Tx.go +++ b/pkg/storage/unified/sql/db/mocks/Tx.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -24,7 +24,7 @@ func (_m *Tx) EXPECT() *Tx_Expecter { return &Tx_Expecter{mock: &_m.Mock} } -// Commit provides a mock function with given fields: +// Commit provides a mock function with no fields func (_m *Tx) Commit() error { ret := _m.Called() @@ -269,7 +269,7 @@ func (_c *Tx_QueryRowContext_Call) RunAndReturn(run func(context.Context, string return _c } -// Rollback provides a mock function with given fields: +// Rollback provides a mock function with no fields func (_m *Tx) Rollback() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/db/mocks/result.go b/pkg/storage/unified/sql/db/mocks/result.go index 73aa331cd82..6999e42e194 100644 --- a/pkg/storage/unified/sql/db/mocks/result.go +++ b/pkg/storage/unified/sql/db/mocks/result.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -17,7 +17,7 @@ func (_m *Result) EXPECT() *Result_Expecter { return &Result_Expecter{mock: &_m.Mock} } -// LastInsertId provides a mock function with given fields: +// LastInsertId provides a mock function with no fields func (_m *Result) LastInsertId() (int64, error) { ret := _m.Called() @@ -72,7 +72,7 @@ func (_c *Result_LastInsertId_Call) RunAndReturn(run func() (int64, error)) *Res return _c } -// RowsAffected provides a mock function with given fields: +// RowsAffected provides a mock function with no fields func (_m *Result) RowsAffected() (int64, error) { ret := _m.Called() diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index 43ae7f3a45a..1e0e50ae4af 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -242,10 +242,13 @@ func (r *resourceHistoryReadLatestRVResponse) Results() (*resourceHistoryReadLat } type historyListRequest struct { - ResourceVersion, Limit, Offset int64 - Folder string - Options *resourcepb.ListOptions + ResourceVersion int64 + Limit int64 + Offset int64 + Folder string + Options *resourcepb.ListOptions } + type sqlResourceHistoryListRequest struct { sqltemplate.SQLTemplate Request *historyListRequest diff --git a/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go b/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplate.go similarity index 97% rename from pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go rename to pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplate.go index 86cbb32fd5b..baeb4d1542e 100644 --- a/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplateIface.go +++ b/pkg/storage/unified/sql/sqltemplate/mocks/SQLTemplate.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -171,7 +171,7 @@ func (_c *SQLTemplate_ArgPlaceholder_Call) RunAndReturn(run func(int) string) *S return _c } -// CurrentEpoch provides a mock function with given fields: +// CurrentEpoch provides a mock function with no fields func (_m *SQLTemplate) CurrentEpoch() string { ret := _m.Called() @@ -216,7 +216,7 @@ func (_c *SQLTemplate_CurrentEpoch_Call) RunAndReturn(run func() string) *SQLTem return _c } -// DialectName provides a mock function with given fields: +// DialectName provides a mock function with no fields func (_m *SQLTemplate) DialectName() string { ret := _m.Called() @@ -261,7 +261,7 @@ func (_c *SQLTemplate_DialectName_Call) RunAndReturn(run func() string) *SQLTemp return _c } -// GetArgs provides a mock function with given fields: +// GetArgs provides a mock function with no fields func (_m *SQLTemplate) GetArgs() []interface{} { ret := _m.Called() @@ -308,7 +308,7 @@ func (_c *SQLTemplate_GetArgs_Call) RunAndReturn(run func() []interface{}) *SQLT return _c } -// GetColNames provides a mock function with given fields: +// GetColNames provides a mock function with no fields func (_m *SQLTemplate) GetColNames() []string { ret := _m.Called() @@ -355,7 +355,7 @@ func (_c *SQLTemplate_GetColNames_Call) RunAndReturn(run func() []string) *SQLTe return _c } -// GetScanDest provides a mock function with given fields: +// GetScanDest provides a mock function with no fields func (_m *SQLTemplate) GetScanDest() []interface{} { ret := _m.Called() @@ -515,7 +515,7 @@ func (_c *SQLTemplate_Into_Call) RunAndReturn(run func(reflect.Value, string) (s return _c } -// Reset provides a mock function with given fields: +// Reset provides a mock function with no fields func (_m *SQLTemplate) Reset() { _m.Called() } @@ -543,7 +543,7 @@ func (_c *SQLTemplate_Reset_Call) Return() *SQLTemplate_Reset_Call { } func (_c *SQLTemplate_Reset_Call) RunAndReturn(run func()) *SQLTemplate_Reset_Call { - _c.Call.Return(run) + _c.Run(run) return _c } @@ -645,11 +645,11 @@ func (_c *SQLTemplate_SetDialect_Call) Return() *SQLTemplate_SetDialect_Call { } func (_c *SQLTemplate_SetDialect_Call) RunAndReturn(run func(sqltemplate.Dialect)) *SQLTemplate_SetDialect_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// Validate provides a mock function with given fields: +// Validate provides a mock function with no fields func (_m *SQLTemplate) Validate() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go b/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go index 50f71da1e28..c109512de5d 100644 --- a/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go +++ b/pkg/storage/unified/sql/sqltemplate/mocks/WithResults.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -171,7 +171,7 @@ func (_c *WithResults_ArgPlaceholder_Call[T]) RunAndReturn(run func(int) string) return _c } -// CurrentEpoch provides a mock function with given fields: +// CurrentEpoch provides a mock function with no fields func (_m *WithResults[T]) CurrentEpoch() string { ret := _m.Called() @@ -216,7 +216,7 @@ func (_c *WithResults_CurrentEpoch_Call[T]) RunAndReturn(run func() string) *Wit return _c } -// DialectName provides a mock function with given fields: +// DialectName provides a mock function with no fields func (_m *WithResults[T]) DialectName() string { ret := _m.Called() @@ -261,7 +261,7 @@ func (_c *WithResults_DialectName_Call[T]) RunAndReturn(run func() string) *With return _c } -// GetArgs provides a mock function with given fields: +// GetArgs provides a mock function with no fields func (_m *WithResults[T]) GetArgs() []interface{} { ret := _m.Called() @@ -308,7 +308,7 @@ func (_c *WithResults_GetArgs_Call[T]) RunAndReturn(run func() []interface{}) *W return _c } -// GetColNames provides a mock function with given fields: +// GetColNames provides a mock function with no fields func (_m *WithResults[T]) GetColNames() []string { ret := _m.Called() @@ -355,7 +355,7 @@ func (_c *WithResults_GetColNames_Call[T]) RunAndReturn(run func() []string) *Wi return _c } -// GetScanDest provides a mock function with given fields: +// GetScanDest provides a mock function with no fields func (_m *WithResults[T]) GetScanDest() []interface{} { ret := _m.Called() @@ -515,7 +515,7 @@ func (_c *WithResults_Into_Call[T]) RunAndReturn(run func(reflect.Value, string) return _c } -// Reset provides a mock function with given fields: +// Reset provides a mock function with no fields func (_m *WithResults[T]) Reset() { _m.Called() } @@ -543,11 +543,11 @@ func (_c *WithResults_Reset_Call[T]) Return() *WithResults_Reset_Call[T] { } func (_c *WithResults_Reset_Call[T]) RunAndReturn(run func()) *WithResults_Reset_Call[T] { - _c.Call.Return(run) + _c.Run(run) return _c } -// Results provides a mock function with given fields: +// Results provides a mock function with no fields func (_m *WithResults[T]) Results() (T, error) { ret := _m.Called() @@ -563,7 +563,9 @@ func (_m *WithResults[T]) Results() (T, error) { if rf, ok := ret.Get(0).(func() T); ok { r0 = rf() } else { - r0 = ret.Get(0).(T) + if ret.Get(0) != nil { + r0 = ret.Get(0).(T) + } } if rf, ok := ret.Get(1).(func() error); ok { @@ -700,11 +702,11 @@ func (_c *WithResults_SetDialect_Call[T]) Return() *WithResults_SetDialect_Call[ } func (_c *WithResults_SetDialect_Call[T]) RunAndReturn(run func(sqltemplate.Dialect)) *WithResults_SetDialect_Call[T] { - _c.Call.Return(run) + _c.Run(run) return _c } -// Validate provides a mock function with given fields: +// Validate provides a mock function with no fields func (_m *WithResults[T]) Validate() error { ret := _m.Called() diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go index d937abb70ab..88f9ed2c955 100644 --- a/pkg/storage/unified/sql/test/benchmark_test.go +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" test "github.com/grafana/grafana/pkg/storage/unified/testing" + "github.com/grafana/grafana/pkg/tests" ) func newTestBackend(b testing.TB) resource.StorageBackend { @@ -38,9 +39,7 @@ func newTestBackend(b testing.TB) resource.StorageBackend { } func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } + tests.SkipIntegrationTestInShortMode(t) opts := test.DefaultBenchmarkOptions() if db.IsTestDbSQLite() { opts.Concurrency = 1 // to avoid SQLite database is locked error @@ -49,9 +48,7 @@ func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { } func TestIntegrationBenchmarkResourceServer(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } + tests.SkipIntegrationTestInShortMode(t) ctx := context.Background() opts := &test.BenchmarkOptions{ diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 99ff031aab1..e33af48713f 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" unitest "github.com/grafana/grafana/pkg/storage/unified/testing" + "github.com/grafana/grafana/pkg/tests" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -95,9 +96,7 @@ func TestIntegrationSQLStorageBackend(t *testing.T) { } func TestIntegrationSearchAndStorage(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test in short mode") - } + tests.SkipIntegrationTestInShortMode(t) ctx := context.Background() diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read object history.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read object history.sql index 4cde1c5f536..66103fecdeb 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read object history.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_get-read object history.sql @@ -13,4 +13,4 @@ WHERE 1 = 1 AND `group` = 'gg' AND `resource` = 'rr' AND `name` = 'name' -ORDER BY resource_version DESC +ORDER BY `resource_version` DESC diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_list-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_list-single path.sql index 0599ac2e10f..4a49051687d 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_list-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_list-single path.sql @@ -24,6 +24,6 @@ SELECT AND maxkv.`name` = kv.`name` WHERE kv.`action` != 3 AND kv.`namespace` = 'ns' - ORDER BY kv.`namespace` ASC, kv.`name` ASC + ORDER BY kv.`resource_version` ASC LIMIT 10 OFFSET 0 ; diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_list-filter_on_namespace.sql b/pkg/storage/unified/sql/testdata/mysql--resource_list-filter_on_namespace.sql index d72fc1ddd1e..ffb9ed43f85 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_list-filter_on_namespace.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_list-filter_on_namespace.sql @@ -10,5 +10,5 @@ SELECT FROM `resource` WHERE 1 = 1 AND `namespace` = 'ns' - ORDER BY `namespace` ASC, `name` ASC + ORDER BY `resource_version` ASC ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read object history.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read object history.sql index b0b6a310419..680fa3ca0a0 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read object history.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_get-read object history.sql @@ -13,4 +13,4 @@ WHERE 1 = 1 AND "group" = 'gg' AND "resource" = 'rr' AND "name" = 'name' -ORDER BY resource_version DESC +ORDER BY "resource_version" DESC diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_list-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_list-single path.sql index 400b2a502cf..86aa57ad2f8 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_list-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_list-single path.sql @@ -24,6 +24,6 @@ SELECT AND maxkv."name" = kv."name" WHERE kv."action" != 3 AND kv."namespace" = 'ns' - ORDER BY kv."namespace" ASC, kv."name" ASC + ORDER BY kv."resource_version" ASC LIMIT 10 OFFSET 0 ; diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_list-filter_on_namespace.sql b/pkg/storage/unified/sql/testdata/postgres--resource_list-filter_on_namespace.sql index 41131a1520f..9268c75bb2b 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_list-filter_on_namespace.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_list-filter_on_namespace.sql @@ -10,5 +10,5 @@ SELECT FROM "resource" WHERE 1 = 1 AND "namespace" = 'ns' - ORDER BY "namespace" ASC, "name" ASC + ORDER BY "resource_version" ASC ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read object history.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read object history.sql index b0b6a310419..680fa3ca0a0 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read object history.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_get-read object history.sql @@ -13,4 +13,4 @@ WHERE 1 = 1 AND "group" = 'gg' AND "resource" = 'rr' AND "name" = 'name' -ORDER BY resource_version DESC +ORDER BY "resource_version" DESC diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_list-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_list-single path.sql index 400b2a502cf..86aa57ad2f8 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_list-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_list-single path.sql @@ -24,6 +24,6 @@ SELECT AND maxkv."name" = kv."name" WHERE kv."action" != 3 AND kv."namespace" = 'ns' - ORDER BY kv."namespace" ASC, kv."name" ASC + ORDER BY kv."resource_version" ASC LIMIT 10 OFFSET 0 ; diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_list-filter_on_namespace.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_list-filter_on_namespace.sql index 41131a1520f..9268c75bb2b 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_list-filter_on_namespace.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_list-filter_on_namespace.sql @@ -10,5 +10,5 @@ SELECT FROM "resource" WHERE 1 = 1 AND "namespace" = 'ns' - ORDER BY "namespace" ASC, "name" ASC + ORDER BY "resource_version" ASC ; diff --git a/pkg/storage/unified/testing/kv.go b/pkg/storage/unified/testing/kv.go index da91a8647d2..79704b48b55 100644 --- a/pkg/storage/unified/testing/kv.go +++ b/pkg/storage/unified/testing/kv.go @@ -43,10 +43,6 @@ func GenerateRandomKVPrefix() string { // RunKVTest runs the KV test suite func RunKVTest(t *testing.T, newKV NewKVFunc, opts *KVTestOptions) { - if testing.Short() { - t.Skip("skipping integration test") - } - if opts == nil { opts = &KVTestOptions{} } @@ -85,21 +81,19 @@ func runTestKVGet(t *testing.T, kv resource.KV, nsPrefix string) { t.Run("get existing key", func(t *testing.T) { // First save a key testValue := "test value for get" - err := kv.Save(ctx, section, "existing-key", strings.NewReader(testValue)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, "existing-key", strings.NewReader(testValue)) // Now get it - obj, err := kv.Get(ctx, section, "existing-key") + reader, err := kv.Get(ctx, section, "existing-key") require.NoError(t, err) - assert.Equal(t, "existing-key", obj.Key) // Read the value - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, testValue, string(value)) // Close the value reader - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) @@ -122,61 +116,72 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { t.Run("save new key", func(t *testing.T) { testValue := "new test value" - err := kv.Save(ctx, section, "new-key", strings.NewReader(testValue)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, "new-key", strings.NewReader(testValue)) // Verify it was saved - obj, err := kv.Get(ctx, section, "new-key") + reader, err := kv.Get(ctx, section, "new-key") require.NoError(t, err) - assert.Equal(t, "new-key", obj.Key) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, testValue, string(value)) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) t.Run("save overwrite existing key", func(t *testing.T) { // First save - err := kv.Save(ctx, section, "overwrite-key", strings.NewReader("old value")) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, "overwrite-key", strings.NewReader("old value")) // Overwrite newValue := "new value" - err = kv.Save(ctx, section, "overwrite-key", strings.NewReader(newValue)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, "overwrite-key", strings.NewReader(newValue)) // Verify it was updated - obj, err := kv.Get(ctx, section, "overwrite-key") + reader, err := kv.Get(ctx, section, "overwrite-key") require.NoError(t, err) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, newValue, string(value)) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) t.Run("save with empty section", func(t *testing.T) { - err := kv.Save(ctx, "", "some-key", strings.NewReader("some value")) + _, err := kv.Save(ctx, "", "some-key") assert.Error(t, err) assert.Contains(t, err.Error(), "section is required") }) t.Run("save binary data", func(t *testing.T) { binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD} - err := kv.Save(ctx, section, "binary-key", bytes.NewReader(binaryData)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, "binary-key", bytes.NewReader(binaryData)) // Verify binary data - obj, err := kv.Get(ctx, section, "binary-key") + reader, err := kv.Get(ctx, section, "binary-key") require.NoError(t, err) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, binaryData, value) - err = obj.Value.Close() + err = reader.Close() + require.NoError(t, err) + }) + + t.Run("save key with no data", func(t *testing.T) { + // Save a key with empty data + saveKVHelper(t, kv, ctx, section, "empty-key", strings.NewReader("")) + + // Verify it was saved with empty data + reader, err := kv.Get(ctx, section, "empty-key") + require.NoError(t, err) + + value, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Equal(t, "", string(value)) + assert.Len(t, value, 0) + err = reader.Close() require.NoError(t, err) }) } @@ -187,11 +192,10 @@ func runTestKVDelete(t *testing.T, kv resource.KV, nsPrefix string) { t.Run("delete existing key", func(t *testing.T) { // First create a key - err := kv.Save(ctx, section, "delete-key", strings.NewReader("delete me")) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, "delete-key", strings.NewReader("delete me")) // Verify it exists - _, err = kv.Get(ctx, section, "delete-key") + _, err := kv.Get(ctx, section, "delete-key") require.NoError(t, err) // Delete it @@ -224,8 +228,7 @@ func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) { // Setup test data testKeys := []string{"a1", "a2", "b1", "b2", "c1"} for _, key := range testKeys { - err := kv.Save(ctx, section, key, strings.NewReader("value"+key)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, key, strings.NewReader("value"+key)) } t.Run("list all keys", func(t *testing.T) { @@ -251,6 +254,19 @@ func runTestKVKeys(t *testing.T, kv resource.KV, nsPrefix string) { assert.Contains(t, errors[0].Error(), "section is required") assert.Empty(t, keys) }) + + t.Run("list keys returns 0 keys", func(t *testing.T) { + // Use a different section with no keys + emptySection := nsPrefix + "-empty-keys" + + var keys []string + for k, err := range kv.Keys(ctx, emptySection, resource.ListOptions{}) { + require.NoError(t, err) + keys = append(keys, k) + } + assert.Empty(t, keys) + assert.Len(t, keys, 0) + }) } func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) { @@ -260,8 +276,7 @@ func runTestKVKeysWithLimits(t *testing.T, kv resource.KV, nsPrefix string) { // Setup test data testKeys := []string{"a1", "a2", "b1", "b2", "c1", "c2", "d1", "d2"} for _, key := range testKeys { - err := kv.Save(ctx, section, key, strings.NewReader("value"+key)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, key, strings.NewReader("value"+key)) } t.Run("keys with limit", func(t *testing.T) { @@ -315,8 +330,7 @@ func runTestKVKeysWithSort(t *testing.T, kv resource.KV, nsPrefix string) { // Setup test data testKeys := []string{"a1", "a2", "b1", "b2", "c1"} for _, key := range testKeys { - err := kv.Save(ctx, section, key, strings.NewReader("value"+key)) - require.NoError(t, err) + saveKVHelper(t, kv, ctx, section, key, strings.NewReader("value"+key)) } t.Run("keys in ascending order (default)", func(t *testing.T) { @@ -383,20 +397,32 @@ func runTestKVConcurrent(t *testing.T, kv resource.KV, nsPrefix string) { value := fmt.Sprintf("concurrent-value-%d-%d", goroutineID, j) // Save - err = kv.Save(ctx, section, key, strings.NewReader(value)) + writer, err := kv.Save(ctx, section, key) + if err != nil { + return + } + defer func() { + err := writer.Close() + require.NoError(t, err) + }() + _, err = io.Copy(writer, strings.NewReader(value)) + if err != nil { + return + } + err = writer.Close() if err != nil { return } // Get immediately - obj, err := kv.Get(ctx, section, key) + reader, err := kv.Get(ctx, section, key) if err != nil { return } - readValue, err := io.ReadAll(obj.Value) + readValue, err := io.ReadAll(reader) require.NoError(t, err) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) assert.Equal(t, value, string(readValue)) } @@ -423,7 +449,19 @@ func runTestKVConcurrent(t *testing.T, kv resource.KV, nsPrefix string) { value := fmt.Sprintf("concurrent-ops-value-%d", goroutineID) // Save - err = kv.Save(ctx, section, key, strings.NewReader(value)) + writer, err := kv.Save(ctx, section, key) + if err != nil { + return + } + defer func() { + err := writer.Close() + require.NoError(t, err) + }() + _, err = io.Copy(writer, strings.NewReader(value)) + if err != nil { + return + } + err = writer.Close() if err != nil { return } @@ -488,3 +526,14 @@ func runTestKVUnixTimestamp(t *testing.T, kv resource.KV, nsPrefix string) { require.InDelta(t, timestamp1, timestamp2, 1) }) } + +// saveKVHelper is a helper function to save data to KV store using the new WriteCloser interface +func saveKVHelper(t *testing.T, kv resource.KV, ctx context.Context, section, key string, value io.Reader) { + t.Helper() + writer, err := kv.Save(ctx, section, key) + require.NoError(t, err) + _, err = io.Copy(writer, value) + require.NoError(t, err) + err = writer.Close() + require.NoError(t, err) +} diff --git a/pkg/storage/unified/testing/search_backend.go b/pkg/storage/unified/testing/search_backend.go index 505e1b1a43d..57d887682c6 100644 --- a/pkg/storage/unified/testing/search_backend.go +++ b/pkg/storage/unified/testing/search_backend.go @@ -24,10 +24,6 @@ type NewSearchBackendFunc func(ctx context.Context) resource.SearchBackend // RunSearchBackendTest runs the search backend test suite func RunSearchBackendTest(t *testing.T, newBackend NewSearchBackendFunc, opts *TestOptions) { - if testing.Short() { - t.Skip("skipping integration test") - } - if opts == nil { opts = &TestOptions{} } diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index 2f369c6d233..2390a6e68fb 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -175,8 +175,8 @@ func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBa require.NoError(t, err) require.Nil(t, resp.Error) require.Len(t, resp.Items, 2) - require.Contains(t, string(resp.Items[0].Value), "item2 MODIFIED") - require.Contains(t, string(resp.Items[1].Value), "item3 ADDED") + require.Contains(t, string(resp.Items[0].Value), "item3 ADDED") + require.Contains(t, string(resp.Items[1].Value), "item2 MODIFIED") require.GreaterOrEqual(t, resp.ResourceVersion, rv5) // rv5 is the latest resource version }) @@ -372,11 +372,11 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend require.NoError(t, err) require.Nil(t, res.Error) require.Len(t, res.Items, 5) - // should be sorted by key ASC + // should be sorted by resource_version ASC require.Contains(t, string(res.Items[0].Value), "item1 ADDED") - require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") - require.Contains(t, string(res.Items[2].Value), "item4 ADDED") - require.Contains(t, string(res.Items[3].Value), "item5 ADDED") + require.Contains(t, string(res.Items[1].Value), "item4 ADDED") + require.Contains(t, string(res.Items[2].Value), "item5 ADDED") + require.Contains(t, string(res.Items[3].Value), "item2 MODIFIED") require.Contains(t, string(res.Items[4].Value), "item6 ADDED") require.Empty(t, res.NextPageToken) @@ -399,8 +399,8 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend continueToken, err := resource.GetContinueToken(res.NextPageToken) require.NoError(t, err) require.Contains(t, string(res.Items[0].Value), "item1 ADDED") - require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") - require.Contains(t, string(res.Items[2].Value), "item4 ADDED") + require.Contains(t, string(res.Items[1].Value), "item4 ADDED") + require.Contains(t, string(res.Items[2].Value), "item5 ADDED") require.GreaterOrEqual(t, continueToken.ResourceVersion, rv8) }) @@ -438,13 +438,12 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend }, }) require.NoError(t, err) - require.NoError(t, err) require.Nil(t, res.Error) require.Len(t, res.Items, 3) t.Log(res.Items) require.Contains(t, string(res.Items[0].Value), "item1 ADDED") - require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") - require.Contains(t, string(res.Items[2].Value), "item4 ADDED") + require.Contains(t, string(res.Items[1].Value), "item4 ADDED") + require.Contains(t, string(res.Items[2].Value), "item5 ADDED") continueToken, err := resource.GetContinueToken(res.NextPageToken) require.NoError(t, err) @@ -471,15 +470,84 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend require.NoError(t, err) require.Nil(t, res.Error) require.Len(t, res.Items, 2) - t.Log(res.Items) - require.Contains(t, string(res.Items[0].Value), "item4 ADDED") - require.Contains(t, string(res.Items[1].Value), "item5 ADDED") + require.Contains(t, string(res.Items[0].Value), "item5 ADDED") + require.Contains(t, string(res.Items[1].Value), "item2 MODIFIED") continueToken, err = resource.GetContinueToken(res.NextPageToken) require.NoError(t, err) require.Equal(t, rv8, continueToken.ResourceVersion) require.Equal(t, int64(4), continueToken.StartOffset) }) + + t.Run("Paginate through latest items one by one", func(t *testing.T) { + baseKey := &resourcepb.ResourceKey{ + Namespace: ns, + Group: "group", + Resource: "resource", + } + expectedItems := []string{"item1 ADDED", "item4 ADDED", "item5 ADDED", "item2 MODIFIED", "item6 ADDED"} + var allItems []*resourcepb.ResourceWrapper + var nextPageToken string + + for i, expectedValue := range expectedItems { + req := &resourcepb.ListRequest{ + Limit: 1, + NextPageToken: nextPageToken, + Options: &resourcepb.ListOptions{ + Key: baseKey, + }, + } + + res, err := server.List(ctx, req) + require.NoError(t, err) + require.Nil(t, res.Error) + require.Len(t, res.Items, 1) + require.Contains(t, string(res.Items[0].Value), expectedValue) + allItems = append(allItems, res.Items[0]) + + if i < len(expectedItems)-1 { + require.NotEmpty(t, res.NextPageToken, "should have a continue token for page %d", i+1) + nextPageToken = res.NextPageToken + } else { + require.Empty(t, res.NextPageToken, "should not have a continue token on the last page") + } + } + require.Len(t, allItems, len(expectedItems)) + }) + + t.Run("Paginate latest with a limit larger than remaining items", func(t *testing.T) { + baseKey := &resourcepb.ResourceKey{ + Namespace: ns, + Group: "group", + Resource: "resource", + } + // Request first 3 items (out of 5 total) + req := &resourcepb.ListRequest{ + Limit: 3, + Options: &resourcepb.ListOptions{ + Key: baseKey, + }, + } + res1, err := server.List(ctx, req) + require.NoError(t, err) + require.Nil(t, res1.Error) + require.Len(t, res1.Items, 3) + require.NotEmpty(t, res1.NextPageToken) + require.Contains(t, string(res1.Items[0].Value), "item1 ADDED") + require.Contains(t, string(res1.Items[1].Value), "item4 ADDED") + require.Contains(t, string(res1.Items[2].Value), "item5 ADDED") + + // Request next page with a large limit + req.Limit = 10 // Larger than the 2 remaining items + req.NextPageToken = res1.NextPageToken + res2, err := server.List(ctx, req) + require.NoError(t, err) + require.Nil(t, res2.Error) + require.Len(t, res2.Items, 2) // Should only get the 2 remaining items + require.Contains(t, string(res2.Items[0].Value), "item2 MODIFIED") + require.Contains(t, string(res2.Items[1].Value), "item6 ADDED") + require.Empty(t, res2.NextPageToken, "should be no continue token on the last page") + }) } func runTestIntegrationBackendListHistory(t *testing.T, backend resource.StorageBackend, nsPrefix string) { @@ -659,7 +727,6 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage require.NoError(t, err) require.Nil(t, res.Error) require.Len(t, res.Items, 2) - t.Log(res.Items) require.Contains(t, string(res.Items[0].Value), "item1 MODIFIED") require.Equal(t, rvHistory2, res.Items[0].ResourceVersion) require.Contains(t, string(res.Items[1].Value), "item1 MODIFIED") @@ -1106,7 +1173,7 @@ func writeEvent(ctx context.Context, store resource.StorageBackend, name string, } meta.SetFolder(options.Folder) - return store.WriteEvent(ctx, resource.WriteEvent{ + event := resource.WriteEvent{ Type: action, Value: options.Value, GUID: uuid.New().String(), @@ -1116,8 +1183,32 @@ func writeEvent(ctx context.Context, store resource.StorageBackend, name string, Resource: options.Resource, Name: name, }, - Object: meta, - }) + } + switch action { + case resourcepb.WatchEvent_DELETED: + event.ObjectOld = meta + + obj, err := utils.MetaAccessor(res) + if err != nil { + return 0, err + } + now := metav1.Now() + obj.SetDeletionTimestamp(&now) + obj.SetUpdatedTimestamp(&now.Time) + obj.SetManagedFields(nil) + obj.SetFinalizers(nil) + obj.SetGeneration(utils.DeletedGeneration) + obj.SetAnnotation(utils.AnnoKeyKubectlLastAppliedConfig, "") // clears it + event.Object = obj + case resourcepb.WatchEvent_ADDED: + event.Object = meta + case resourcepb.WatchEvent_MODIFIED: + event.Object = meta // + event.ObjectOld = meta + default: + panic(fmt.Sprintf("invalid action: %s", action)) + } + return store.WriteEvent(ctx, event) } func newServer(t *testing.T, b resource.StorageBackend) resource.ResourceServer { diff --git a/pkg/storage/unified/testing/storage_backend_test.go b/pkg/storage/unified/testing/storage_backend_test.go index c0be0065c2f..a1da3b82e72 100644 --- a/pkg/storage/unified/testing/storage_backend_test.go +++ b/pkg/storage/unified/testing/storage_backend_test.go @@ -11,7 +11,6 @@ import ( ) func TestBadgerKVStorageBackend(t *testing.T) { - t.Skip("failing with 'panic: DB Closed'") RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { opts := badger.DefaultOptions("").WithInMemory(true).WithLogger(nil) db, err := badger.Open(opts) diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 45b82107af4..e74d718d9bf 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -22,8 +22,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg", - "large": "public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg" + "small": "public/plugins/alertlist/img/icn-singlestat-panel.svg", + "large": "public/plugins/alertlist/img/icn-singlestat-panel.svg" }, "build": {}, "screenshots": null, @@ -76,8 +76,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/alertmanager/img/logo.svg", - "large": "public/app/plugins/datasource/alertmanager/img/logo.svg" + "small": "public/plugins/alertmanager/img/logo.svg", + "large": "public/plugins/alertmanager/img/logo.svg" }, "build": {}, "screenshots": null, @@ -133,8 +133,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/annolist/img/icn-annolist-panel.svg", - "large": "public/app/plugins/panel/annolist/img/icn-annolist-panel.svg" + "small": "public/plugins/annolist/img/icn-annolist-panel.svg", + "large": "public/plugins/annolist/img/icn-annolist-panel.svg" }, "build": {}, "screenshots": null, @@ -191,22 +191,22 @@ } ], "logos": { - "small": "public/app/plugins/datasource/azuremonitor/img/logo.jpg", - "large": "public/app/plugins/datasource/azuremonitor/img/logo.jpg" + "small": "public/plugins/grafana-azure-monitor-datasource/img/logo.jpg", + "large": "public/plugins/grafana-azure-monitor-datasource/img/logo.jpg" }, "build": {}, "screenshots": [ { "name": "Azure Contoso Loans", - "path": "public/app/plugins/datasource/azuremonitor/img/contoso_loans_grafana_dashboard.png" + "path": "public/plugins/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png" }, { "name": "Azure Monitor Network", - "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_network.png" + "path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_network.png" }, { "name": "Azure Monitor CPU", - "path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png" + "path": "public/plugins/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png" } ], "version": "12.1.0-pre", @@ -260,8 +260,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/barchart/img/barchart.svg", - "large": "public/app/plugins/panel/barchart/img/barchart.svg" + "small": "public/plugins/barchart/img/barchart.svg", + "large": "public/plugins/barchart/img/barchart.svg" }, "build": {}, "screenshots": null, @@ -310,8 +310,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg", - "large": "public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg" + "small": "public/plugins/bargauge/img/icon_bar_gauge.svg", + "large": "public/plugins/bargauge/img/icon_bar_gauge.svg" }, "build": {}, "screenshots": null, @@ -360,8 +360,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/candlestick/img/candlestick.svg", - "large": "public/app/plugins/panel/candlestick/img/candlestick.svg" + "small": "public/plugins/candlestick/img/candlestick.svg", + "large": "public/plugins/candlestick/img/candlestick.svg" }, "build": {}, "screenshots": null, @@ -415,8 +415,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/canvas/img/icn-canvas.svg", - "large": "public/app/plugins/panel/canvas/img/icn-canvas.svg" + "small": "public/plugins/canvas/img/icn-canvas.svg", + "large": "public/plugins/canvas/img/icn-canvas.svg" }, "build": {}, "screenshots": null, @@ -465,8 +465,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png", - "large": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png" + "small": "public/plugins/cloudwatch/img/amazon-web-services.png", + "large": "public/plugins/cloudwatch/img/amazon-web-services.png" }, "build": {}, "screenshots": null, @@ -518,8 +518,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg" + "small": "public/plugins/dashlist/img/icn-dashlist-panel.svg", + "large": "public/plugins/dashlist/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -568,8 +568,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/datagrid/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/datagrid/img/icn-table-panel.svg" + "small": "public/plugins/datagrid/img/icn-table-panel.svg", + "large": "public/plugins/datagrid/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -622,8 +622,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg", - "large": "public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg" + "small": "public/plugins/elasticsearch/img/elasticsearch.svg", + "large": "public/plugins/elasticsearch/img/elasticsearch.svg" }, "build": {}, "screenshots": null, @@ -679,8 +679,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg", - "large": "public/app/plugins/panel/flamegraph/img/icn-flamegraph.svg" + "small": "public/plugins/flamegraph/img/icn-flamegraph.svg", + "large": "public/plugins/flamegraph/img/icn-flamegraph.svg" }, "build": {}, "screenshots": null, @@ -729,8 +729,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/gauge/img/icon_gauge.svg", - "large": "public/app/plugins/panel/gauge/img/icon_gauge.svg" + "small": "public/plugins/gauge/img/icon_gauge.svg", + "large": "public/plugins/gauge/img/icon_gauge.svg" }, "build": {}, "screenshots": null, @@ -779,8 +779,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/geomap/img/icn-geomap.svg", - "large": "public/app/plugins/panel/geomap/img/icn-geomap.svg" + "small": "public/plugins/geomap/img/icn-geomap.svg", + "large": "public/plugins/geomap/img/icn-geomap.svg" }, "build": {}, "screenshots": null, @@ -825,8 +825,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg" + "small": "public/plugins/gettingstarted/img/icn-dashlist-panel.svg", + "large": "public/plugins/gettingstarted/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -875,8 +875,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg", - "large": "public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg" + "small": "public/plugins/stackdriver/img/cloud_monitoring_logo.svg", + "large": "public/plugins/stackdriver/img/cloud_monitoring_logo.svg" }, "build": {}, "screenshots": null, @@ -929,8 +929,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg", - "large": "public/app/plugins/datasource/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg" + "small": "public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg", + "large": "public/plugins/grafana-pyroscope-datasource/img/grafana_pyroscope_icon.svg" }, "build": {}, "screenshots": null, @@ -995,8 +995,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/graphite/img/graphite_logo.png", - "large": "public/app/plugins/datasource/graphite/img/graphite_logo.png" + "small": "public/plugins/graphite/img/graphite_logo.png", + "large": "public/plugins/graphite/img/graphite_logo.png" }, "build": {}, "screenshots": null, @@ -1045,8 +1045,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg", - "large": "public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg" + "small": "public/plugins/heatmap/img/icn-heatmap-panel.svg", + "large": "public/plugins/heatmap/img/icn-heatmap-panel.svg" }, "build": {}, "screenshots": null, @@ -1095,8 +1095,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/histogram/img/histogram.svg", - "large": "public/app/plugins/panel/histogram/img/histogram.svg" + "small": "public/plugins/histogram/img/histogram.svg", + "large": "public/plugins/histogram/img/histogram.svg" }, "build": {}, "screenshots": null, @@ -1150,8 +1150,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/influxdb/img/influxdb_logo.svg", - "large": "public/app/plugins/datasource/influxdb/img/influxdb_logo.svg" + "small": "public/plugins/influxdb/img/influxdb_logo.svg", + "large": "public/plugins/influxdb/img/influxdb_logo.svg" }, "build": {}, "screenshots": null, @@ -1212,8 +1212,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/jaeger/img/jaeger_logo.svg", - "large": "public/app/plugins/datasource/jaeger/img/jaeger_logo.svg" + "small": "public/plugins/jaeger/img/jaeger_logo.svg", + "large": "public/plugins/jaeger/img/jaeger_logo.svg" }, "build": {}, "screenshots": null, @@ -1262,8 +1262,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/logs/img/icn-logs-panel.svg", - "large": "public/app/plugins/panel/logs/img/icn-logs-panel.svg" + "small": "public/plugins/logs/img/icn-logs-panel.svg", + "large": "public/plugins/logs/img/icn-logs-panel.svg" }, "build": {}, "screenshots": null, @@ -1320,8 +1320,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/loki/img/loki_icon.svg", - "large": "public/app/plugins/datasource/loki/img/loki_icon.svg" + "small": "public/plugins/loki/img/loki_icon.svg", + "large": "public/plugins/loki/img/loki_icon.svg" }, "build": {}, "screenshots": null, @@ -1370,8 +1370,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/mssql/img/sql_server_logo.svg", - "large": "public/app/plugins/datasource/mssql/img/sql_server_logo.svg" + "small": "public/plugins/mssql/img/sql_server_logo.svg", + "large": "public/plugins/mssql/img/sql_server_logo.svg" }, "build": {}, "screenshots": null, @@ -1420,8 +1420,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/mysql/img/mysql_logo.svg", - "large": "public/app/plugins/datasource/mysql/img/mysql_logo.svg" + "small": "public/plugins/mysql/img/mysql_logo.svg", + "large": "public/plugins/mysql/img/mysql_logo.svg" }, "build": {}, "screenshots": null, @@ -1470,8 +1470,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/news/img/news.svg", - "large": "public/app/plugins/panel/news/img/news.svg" + "small": "public/plugins/news/img/news.svg", + "large": "public/plugins/news/img/news.svg" }, "build": {}, "screenshots": null, @@ -1520,8 +1520,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg", - "large": "public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg" + "small": "public/plugins/nodeGraph/img/icn-node-graph.svg", + "large": "public/plugins/nodeGraph/img/icn-node-graph.svg" }, "build": {}, "screenshots": null, @@ -1570,8 +1570,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png", - "large": "public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png" + "small": "public/plugins/opentsdb/img/opentsdb_logo.png", + "large": "public/plugins/opentsdb/img/opentsdb_logo.png" }, "build": {}, "screenshots": null, @@ -1624,8 +1624,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/parca/img/logo-small.svg", - "large": "public/app/plugins/datasource/parca/img/logo-small.svg" + "small": "public/plugins/parca/img/logo-small.svg", + "large": "public/plugins/parca/img/logo-small.svg" }, "build": {}, "screenshots": null, @@ -1679,8 +1679,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/piechart/img/icon_piechart.svg", - "large": "public/app/plugins/panel/piechart/img/icon_piechart.svg" + "small": "public/plugins/piechart/img/icon_piechart.svg", + "large": "public/plugins/piechart/img/icon_piechart.svg" }, "build": {}, "screenshots": null, @@ -1729,8 +1729,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-postgresql-datasource/img/postgresql_logo.svg", - "large": "public/app/plugins/datasource/grafana-postgresql-datasource/img/postgresql_logo.svg" + "small": "public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg", + "large": "public/plugins/grafana-postgresql-datasource/img/postgresql_logo.svg" }, "build": {}, "screenshots": null, @@ -1783,8 +1783,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg", - "large": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg" + "small": "public/plugins/prometheus/img/prometheus_logo.svg", + "large": "public/plugins/prometheus/img/prometheus_logo.svg" }, "build": {}, "screenshots": null, @@ -1833,8 +1833,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/stat/img/icn-singlestat-panel.svg", - "large": "public/app/plugins/panel/stat/img/icn-singlestat-panel.svg" + "small": "public/plugins/stat/img/icn-singlestat-panel.svg", + "large": "public/plugins/stat/img/icn-singlestat-panel.svg" }, "build": {}, "screenshots": null, @@ -1883,8 +1883,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/state-timeline/img/timeline.svg", - "large": "public/app/plugins/panel/state-timeline/img/timeline.svg" + "small": "public/plugins/state-timeline/img/timeline.svg", + "large": "public/plugins/state-timeline/img/timeline.svg" }, "build": {}, "screenshots": null, @@ -1933,8 +1933,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/status-history/img/status.svg", - "large": "public/app/plugins/panel/status-history/img/status.svg" + "small": "public/plugins/status-history/img/status.svg", + "large": "public/plugins/status-history/img/status.svg" }, "build": {}, "screenshots": null, @@ -1983,8 +1983,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/table/img/icn-table-panel.svg", - "large": "public/app/plugins/panel/table/img/icn-table-panel.svg" + "small": "public/plugins/table/img/icn-table-panel.svg", + "large": "public/plugins/table/img/icn-table-panel.svg" }, "build": {}, "screenshots": null, @@ -2037,8 +2037,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/tempo/img/tempo_logo.svg", - "large": "public/app/plugins/datasource/tempo/img/tempo_logo.svg" + "small": "public/plugins/tempo/img/tempo_logo.svg", + "large": "public/plugins/tempo/img/tempo_logo.svg" }, "build": {}, "screenshots": null, @@ -2087,8 +2087,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/grafana-testdata-datasource/img/testdata.svg", - "large": "public/app/plugins/datasource/grafana-testdata-datasource/img/testdata.svg" + "small": "public/plugins/grafana-testdata-datasource/img/testdata.svg", + "large": "public/plugins/grafana-testdata-datasource/img/testdata.svg" }, "build": {}, "screenshots": null, @@ -2137,8 +2137,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/text/img/icn-text-panel.svg", - "large": "public/app/plugins/panel/text/img/icn-text-panel.svg" + "small": "public/plugins/text/img/icn-text-panel.svg", + "large": "public/plugins/text/img/icn-text-panel.svg" }, "build": {}, "screenshots": null, @@ -2187,8 +2187,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg", - "large": "public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg" + "small": "public/plugins/timeseries/img/icn-timeseries-panel.svg", + "large": "public/plugins/timeseries/img/icn-timeseries-panel.svg" }, "build": {}, "screenshots": null, @@ -2237,8 +2237,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/traces/img/traces-panel.svg", - "large": "public/app/plugins/panel/traces/img/traces-panel.svg" + "small": "public/plugins/traces/img/traces-panel.svg", + "large": "public/plugins/traces/img/traces-panel.svg" }, "build": {}, "screenshots": null, @@ -2287,8 +2287,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/trend/img/trend.svg", - "large": "public/app/plugins/panel/trend/img/trend.svg" + "small": "public/plugins/trend/img/trend.svg", + "large": "public/plugins/trend/img/trend.svg" }, "build": {}, "screenshots": null, @@ -2333,8 +2333,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg", - "large": "public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg" + "small": "public/plugins/welcome/img/icn-dashlist-panel.svg", + "large": "public/plugins/welcome/img/icn-dashlist-panel.svg" }, "build": {}, "screenshots": null, @@ -2383,8 +2383,8 @@ } ], "logos": { - "small": "public/app/plugins/panel/xychart/img/icn-xychart.svg", - "large": "public/app/plugins/panel/xychart/img/icn-xychart.svg" + "small": "public/plugins/xychart/img/icn-xychart.svg", + "large": "public/plugins/xychart/img/icn-xychart.svg" }, "build": {}, "screenshots": null, @@ -2440,8 +2440,8 @@ } ], "logos": { - "small": "public/app/plugins/datasource/zipkin/img/zipkin-logo.svg", - "large": "public/app/plugins/datasource/zipkin/img/zipkin-logo.svg" + "small": "public/plugins/zipkin/img/zipkin-logo.svg", + "large": "public/plugins/zipkin/img/zipkin-logo.svg" }, "build": {}, "screenshots": null, @@ -2467,4 +2467,4 @@ "signatureOrg": "", "angularDetected": false } -] \ No newline at end of file +] diff --git a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go index 56a9ecf2706..37a982a65f5 100644 --- a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go +++ b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go @@ -663,3 +663,66 @@ func TestIntegrationDataConsistency(t *testing.T) { assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers) }) } + +func TestIntegrationExtraConfigsConflicts(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{"alertingImportAlertmanagerAPI"}, + }) + + cliCfg := helper.Org1.Admin.NewRestConfig() + legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) + + client := common.NewRoutingTreeClient(t, helper.Org1.Admin) + + // Now upload a new extra config + testAlertmanagerConfigYAML := ` +route: + receiver: default + +receivers: +- name: default + webhook_configs: + - url: 'http://localhost/webhook' +` + + headers := map[string]string{ + "Content-Type": "application/yaml", + "X-Grafana-Alerting-Config-Identifier": "external-system", + "X-Grafana-Alerting-Merge-Matchers": "imported=true", + } + + // Post the configuration to Grafana + response := legacyCli.ConvertPrometheusPostAlertmanagerConfig(t, definitions.AlertmanagerUserConfig{ + AlertmanagerConfig: testAlertmanagerConfigYAML, + }, headers) + require.Equal(t, "success", response.Status) + + current, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + updated := current.Copy().(*v0alpha1.RoutingTree) + updated.Spec.Routes = append(updated.Spec.Routes, v0alpha1.RoutingTreeRoute{ + Matchers: []v0alpha1.RoutingTreeMatcher{ + { + Label: "imported", + Type: v0alpha1.RoutingTreeMatcherTypeEqual, + Value: "true", + }, + }, + }) + + _, err = client.Update(ctx, updated, v1.UpdateOptions{}) + require.Error(t, err) + require.Truef(t, errors.IsBadRequest(err), "Should get BadRequest error but got: %s", err) + + // Now delete extra config + legacyCli.ConvertPrometheusDeleteAlertmanagerConfig(t, headers) + + // and try again + _, err = client.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) +} diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index c107eacb2c9..900b7ed7da2 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -2,6 +2,7 @@ package dashboards import ( "context" + "fmt" "strings" "testing" @@ -13,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" @@ -112,74 +114,28 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper, gvr schema.Group func TestIntegrationDashboardsAppV0Alpha1(t *testing.T) { gvr := schema.GroupVersionResource{ - Group: dashboardV1.GROUP, - Version: dashboardV1.VERSION, + Group: dashboardV0.GROUP, + Version: dashboardV0.VERSION, Resource: "dashboards", } if testing.Short() { t.Skip("skipping integration test") } - t.Run("v0alpha1 with dual writer mode 0", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 0, + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("v0alpha1 with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, }, - }, + }) + runDashboardTest(t, helper, gvr) }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 1", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 1, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 2", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 2, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 3", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 3, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v0alpha1 with dual writer mode 4", func(t *testing.T) { - t.Skip("skipping test because of authorizer issue") - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 4, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) + } } func TestIntegrationDashboardsAppV1(t *testing.T) { @@ -192,66 +148,46 @@ func TestIntegrationDashboardsAppV1(t *testing.T) { t.Skip("skipping integration test") } - t.Run("v1 with dual writer mode 0", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 0, + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, }, - }, + }) + runDashboardTest(t, helper, gvr) }) - runDashboardTest(t, helper, gvr) - }) + } +} - t.Run("v1 with dual writer mode 1", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 1, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) +func TestIntegrationDashboardsAppV2(t *testing.T) { + gvr := schema.GroupVersionResource{ + Group: dashboardV2.GROUP, + Version: dashboardV2.VERSION, + Resource: "dashboards", + } + if testing.Short() { + t.Skip("skipping integration test") + } - t.Run("v1 with dual writer mode 2", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 2, + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, }, - }, + }) + runDashboardTest(t, helper, gvr) }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v1 with dual writer mode 3", func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 3, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) - - t.Run("v1 with dual writer mode 4", func(t *testing.T) { - t.Skip("skipping test because of authorizer issue") - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - DisableAnonymous: true, - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - "dashboards.dashboard.grafana.app": { - DualWriterMode: 4, - }, - }, - }) - runDashboardTest(t, helper, gvr) - }) + } } func TestIntegrationLegacySupport(t *testing.T) { diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index 2215808970c..42e4d9d9a5f 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -58,11 +58,10 @@ type TestContext struct { // TestIntegrationValidation tests the dashboard K8s API func TestIntegrationValidation(t *testing.T) { if testing.Short() { - t.Skip("skipping integration test2") + t.Skip("skipping integration test") } - // TODO: Skip mode3 - borken due to race conditions while setting default permissions across storage backends - dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2} + dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} for _, dualWriterMode := range dualWriterModes { t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { // Create a K8sTestHelper which will set up a real API server @@ -71,6 +70,7 @@ func TestIntegrationValidation(t *testing.T) { EnableFeatureToggles: []string{ featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature featuremgmt.FlagUnifiedStorageSearch, + featuremgmt.FlagKubernetesDashboards, // Enable FE-only dashboard feature flag }, UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "dashboards.dashboard.grafana.app": { @@ -81,6 +81,36 @@ func TestIntegrationValidation(t *testing.T) { testIntegrationValidationForServer(t, helper, dualWriterMode) }) } + + for _, dualWriterMode := range dualWriterModes { + t.Run(fmt.Sprintf("DualWriterMode %d - kubernetesDashboards disabled", dualWriterMode), func(t *testing.T) { + // Create a K8sTestHelper which will set up a real API server + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature + featuremgmt.FlagUnifiedStorageSearch, + }, + DisableFeatureToggles: []string{ + featuremgmt.FlagKubernetesDashboards, + }, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: dualWriterMode, + }, + }}) + + t.Cleanup(func() { + helper.Shutdown() + }) + + org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) + + t.Run("Dashboard permission tests", func(t *testing.T) { + runDashboardPermissionTests(t, org1Ctx, false) + }) + }) + } } func testIntegrationValidationForServer(t *testing.T, helper *apis.K8sTestHelper, dualWriterMode rest.DualWriterMode) { @@ -106,7 +136,7 @@ func testIntegrationValidationForServer(t *testing.T, helper *apis.K8sTestHelper }) t.Run("Dashboard permission tests", func(t *testing.T) { - runDashboardPermissionTests(t, org1Ctx) + runDashboardPermissionTests(t, org1Ctx, true) }) t.Run("Dashboard LIST API test", func(t *testing.T) { @@ -1225,7 +1255,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { } // Run tests for dashboard permissions -func runDashboardPermissionTests(t *testing.T, ctx TestContext) { +func runDashboardPermissionTests(t *testing.T, ctx TestContext, kubernetesDashboardsEnabled bool) { t.Helper() // Get clients for each user @@ -1348,8 +1378,22 @@ func runDashboardPermissionTests(t *testing.T, ctx TestContext) { // Clean up dashboard err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) - err = viewerClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) - require.NoError(t, err) + + if kubernetesDashboardsEnabled { + // In case kubernetesDashboards feature flag is set to true, + // we don't grant admin permission to dashboard creator on nested folders. + // This means that the viewer will not be able to delete the dashboard. + err = viewerClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) + require.Error(t, err) + err = adminClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + } else { + // In case kubernetesDashboards feature flag is set to false, + // we grant admin permission to dashboard creator on nested folders. + // This means that the viewer will be able to delete the dashboard. + err = viewerClient.Resource.Delete(context.Background(), dashViewer.GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + } // Clean up the folder err = adminFolderClient.Resource.Delete(context.Background(), folderUID, v1.DeleteOptions{}) diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 35722cbc964..5001e90f42c 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -18,6 +18,7 @@ import ( folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/api/dtos" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -44,6 +45,10 @@ func TestIntegrationFoldersApp(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ @@ -119,71 +124,94 @@ func TestIntegrationFoldersApp(t *testing.T) { }`, string(v1Disco)) }) - t.Run("with dual write (unified storage, mode 0)", func(t *testing.T) { - doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode0, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - })) - }) + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + modeDw := grafanarest.DualWriterMode(mode) - t.Run("with dual write (unified storage, mode 1)", func(t *testing.T) { - doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v)", modeDw), func(t *testing.T) { + doFolderTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - })) - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + })) + }) - t.Run("with dual write (unified storage, mode 1, create nested folders)", func(t *testing.T) { - doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create nested folders)", modeDw), func(t *testing.T) { + doNestedCreateTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) - t.Run("with dual write (unified storage, mode 1, create existing folder)", func(t *testing.T) { - doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create existing folder)", modeDw), func(t *testing.T) { + doCreateDuplicateFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) + + t.Run(fmt.Sprintf("when creating a folder, mode %v, it should trim leading and trailing spaces", modeDw), func(t *testing.T) { + doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) + + t.Run(fmt.Sprintf("with dual write (unified storage, mode %v, create circular reference folder)", modeDw), func(t *testing.T) { + doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, + featuremgmt.FlagNestedFolders, + }, + })) + }) + } // This is a general test for the unified storage list operation. We don't have a common test // directory for now, so we (search and storage) keep it here as we own this part of the tests. @@ -216,40 +244,6 @@ func TestIntegrationFoldersApp(t *testing.T) { }) } }) - - t.Run("when creating a folder it should trim leading and trailing spaces", func(t *testing.T) { - doCreateEnsureTitleIsTrimmedTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) - - t.Run("with dual write (unified storage, mode 1, create circular reference folder)", func(t *testing.T) { - doCreateCircularReferenceFolderTest(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, - }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagNestedFolders, - }, - })) - }) } func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper { @@ -275,20 +269,25 @@ func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper require.NotNil(t, legacyCreate.Result) uid := legacyCreate.Result.UID require.NotEmpty(t, uid) + //nolint:staticcheck + id := legacyCreate.Result.ID + require.NotEmpty(t, id) + idStr := fmt.Sprintf("%d", id) expectedResult := `{ "apiVersion": "folder.grafana.app/v1beta1", "kind": "Folder", "metadata": { "creationTimestamp": "${creationTimestamp}", - "labels": {"grafana.app/deprecatedInternalID":"1"}, + "labels": {"grafana.app/deprecatedInternalID":"` + idStr + `"}, "name": "` + uid + `", "namespace": "default", "resourceVersion": "${resourceVersion}", "uid": "${uid}" }, "spec": { - "title": "Test" + "title": "Test", + "description": "" }, "status": {} }` @@ -591,6 +590,9 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" @@ -662,52 +664,56 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { }, } - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + for _, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + }) - user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) + user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - parentPayload := `{ + parentPayload := `{ "title": "Test/parent", "uid": "parentuid" }` - parentCreate := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(parentPayload), - }, &folder.Folder{}) - require.NotNil(t, parentCreate.Result) - parentUID := parentCreate.Result.UID - require.NotEmpty(t, parentUID) + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + parentUID := parentCreate.Result.UID + require.NotEmpty(t, parentUID) - resp := apis.DoRequest(helper, apis.RequestParams{ - User: user, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(tc.input), - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, resp.Response.StatusCode) + resp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(tc.input), + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, resp.Response.StatusCode) - if tc.expectedCode == http.StatusOK { - require.Equal(t, "uid", resp.Result.UID) - require.Equal(t, "Folder", resp.Result.Title) - } - }) + if tc.expectedCode == http.StatusOK { + require.Equal(t, "uid", resp.Result.UID) + require.Equal(t, "Folder", resp.Result.Title) + } + }) + } } } @@ -715,6 +721,9 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } type testCase struct { description string @@ -763,108 +772,115 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { }, } - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + for _, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + }) - // Create parent folder - parentPayload := `{ + // Create parent folder + parentPayload := `{ "title": "testparent", "uid": "parentuid" }` - parentCreate := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(parentPayload), - }, &folder.Folder{}) - require.NotNil(t, parentCreate.Result) - parentUID := parentCreate.Result.UID - require.NotEmpty(t, parentUID) + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + parentUID := parentCreate.Result.UID + require.NotEmpty(t, parentUID) - // Create descendant folder - payload := "{ \"uid\": \"descuid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" - resp := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(payload), - }, &dtos.Folder{}) - require.Equal(t, http.StatusOK, resp.Response.StatusCode) - - user := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions) - - // Get with accesscontrol disabled - getResp := apis.DoRequest(helper, apis.RequestParams{ - User: user, - Method: http.MethodGet, - Path: "/api/folders/descuid", - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, getResp.Response.StatusCode) - require.NotNil(t, getResp.Result) - - require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead]) - require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite]) - - parents := getResp.Result.Parents - require.Equal(t, len(tc.expectedParentUIDs), len(parents)) - require.Equal(t, len(tc.expectedParentTitles), len(parents)) - for i := 0; i < len(tc.expectedParentUIDs); i++ { - require.Equal(t, tc.expectedParentUIDs[i], parents[i].UID) - require.Equal(t, tc.expectedParentTitles[i], parents[i].Title) - } - - // Get with accesscontrol enabled - if tc.checkAccessControl { - acPerms := []resourcepermissions.SetResourcePermissionCommand{ - { - Actions: []string{dashboards.ActionFoldersRead}, - Resource: "folders", - ResourceAttribute: "uid", - ResourceID: "*", - }, - { - Actions: []string{dashboards.ActionFoldersWrite}, - Resource: "folders", - ResourceAttribute: "uid", - ResourceID: "parentuid", - }, - } - acUser := helper.CreateUser("acuser", apis.Org1, org.RoleNone, acPerms) - - getWithAC := apis.DoRequest(helper, apis.RequestParams{ - User: acUser, - Method: http.MethodGet, - Path: "/api/folders/descuid?accesscontrol=true", + // Create descendant folder + payload := "{ \"uid\": \"descuid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" + resp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(payload), }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode) - require.NotNil(t, getWithAC.Result) + require.Equal(t, http.StatusOK, resp.Response.StatusCode) - require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead]) - require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite]) - } - }) + user := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions) + + // Get with accesscontrol disabled + getResp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodGet, + Path: "/api/folders/descuid", + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, getResp.Response.StatusCode) + require.NotNil(t, getResp.Result) + + require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead]) + require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite]) + + parents := getResp.Result.Parents + require.Equal(t, len(tc.expectedParentUIDs), len(parents)) + require.Equal(t, len(tc.expectedParentTitles), len(parents)) + for i := 0; i < len(tc.expectedParentUIDs); i++ { + require.Equal(t, tc.expectedParentUIDs[i], parents[i].UID) + require.Equal(t, tc.expectedParentTitles[i], parents[i].Title) + } + + // Get with accesscontrol enabled + if tc.checkAccessControl { + acPerms := []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{dashboards.ActionFoldersRead}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "*", + }, + { + Actions: []string{dashboards.ActionFoldersWrite}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "parentuid", + }, + } + acUser := helper.CreateUser("acuser", apis.Org1, org.RoleNone, acPerms) + + getWithAC := apis.DoRequest(helper, apis.RequestParams{ + User: acUser, + Method: http.MethodGet, + Path: "/api/folders/descuid?accesscontrol=true", + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode) + require.NotNil(t, getWithAC.Result) + + require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead]) + require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite]) + } + }) + } } } // TestFoldersCreateAPIEndpointK8S is the counterpart of pkg/api/folder_test.go TestFoldersCreateAPIEndpoint -func TestFoldersCreateAPIEndpointK8S(t *testing.T) { +func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}" folderWithTitleEmpty := "{ \"title\": \"\"}" @@ -902,7 +918,7 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) { description: "folder creation fails without permissions to create a folder", input: folderWithoutParentInput, expectedCode: http.StatusForbidden, - expectedMessage: dashboards.ErrFolderAccessDenied.Error(), + expectedMessage: fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", "folders:create"), permissions: []resourcepermissions.SetResourcePermissionCommand{}, }, { @@ -940,76 +956,80 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) { }, } - for _, tc := range tcs { - t.Run(testDescription(tc.description, tc.expectedFolderSvcError), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + for _, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+testDescription(tc.description, tc.expectedFolderSvcError), mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesClientDashboardsFolders, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - }) - - userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - - if tc.createSecondRecord { - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvr, }) - create2 := apis.DoRequest(helper, apis.RequestParams{ - User: client.Args.User, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(tc.input), - }, &folder.Folder{}) - require.NotEmpty(t, create2.Response) - require.Equal(t, http.StatusOK, create2.Response.StatusCode) - } - addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() - login := userTest.Identity.GetLogin() - baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr) + userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf( - "%s%s", - baseUrl, - "/api/folders", - ), bytes.NewBuffer([]byte(tc.input))) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") + if tc.createSecondRecord { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + create2 := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(tc.input), + }, &folder.Folder{}) + require.NotEmpty(t, create2.Response) + require.Equal(t, http.StatusOK, create2.Response.StatusCode) + } - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.NotNil(t, resp) - require.Equal(t, tc.expectedCode, resp.StatusCode) + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() + login := userTest.Identity.GetLogin() + baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr) - type folderWithMessage struct { - dtos.Folder - Message string `json:"message"` - } + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf( + "%s%s", + baseUrl, + "/api/folders", + ), bytes.NewBuffer([]byte(tc.input))) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") - folder := folderWithMessage{} - err = json.NewDecoder(resp.Body).Decode(&folder) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, tc.expectedCode, resp.StatusCode) - if tc.expectedCode == http.StatusOK { - require.Equal(t, "uid", folder.UID) - require.Equal(t, "Folder", folder.Title) - } + type folderWithMessage struct { + dtos.Folder + Message string `json:"message"` + } - if tc.expectedMessage != "" { - require.Equal(t, tc.expectedMessage, folder.Message) - } - }) + folder := folderWithMessage{} + err = json.NewDecoder(resp.Body).Decode(&folder) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + if tc.expectedCode == http.StatusOK { + require.Equal(t, "uid", folder.UID) + require.Equal(t, "Folder", folder.Title) + } + + if tc.expectedMessage != "" { + require.Equal(t, tc.expectedMessage, folder.Message) + } + }) + } } } @@ -1022,10 +1042,13 @@ func testDescription(description string, expectedErr error) string { } // There are no counterpart of TestFoldersGetAPIEndpointK8S in pkg/api/folder_test.go -func TestFoldersGetAPIEndpointK8S(t *testing.T) { +func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } + if !db.IsTestDbSQLite() { + t.Skip("test only on sqlite for now") + } type testCase struct { description string @@ -1062,6 +1085,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { expectedOutput: []dtos.FolderSearchHit{ {UID: "foo", Title: "Folder 1"}, {UID: "qux", Title: "Folder 3"}, + {UID: folder.SharedWithMeFolder.UID, Title: folder.SharedWithMeFolder.Title}, }, permissions: folderReadAndCreatePermission, }, @@ -1107,7 +1131,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { } // test on all dualwriter modes - for mode := 1; mode <= 4; mode++ { + for mode := 0; mode <= 4; mode++ { for _, tc := range tcs { t.Run(fmt.Sprintf("Mode: %d, %s", mode, tc.description), func(t *testing.T) { modeDw := grafanarest.DualWriterMode(mode) @@ -1123,6 +1147,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { }, EnableFeatureToggles: []string{ featuremgmt.FlagNestedFolders, + featuremgmt.FlagUnifiedStorageSearch, featuremgmt.FlagKubernetesClientDashboardsFolders, }, }) diff --git a/pkg/tests/apis/iam/iam_test.go b/pkg/tests/apis/iam/iam_test.go index 8bcd7d3742a..19d16d341c9 100644 --- a/pkg/tests/apis/iam/iam_test.go +++ b/pkg/tests/apis/iam/iam_test.go @@ -67,7 +67,8 @@ func TestIntegrationIdentity(t *testing.T) { "spec": { "email": "staff@Org1", "title": "staff" - } + }, + "status": {} } ] }`, found) @@ -84,20 +85,40 @@ func TestIntegrationIdentity(t *testing.T) { found = teamClient.SpecJSON(rsp) require.JSONEq(t, `[ { + "disabled": false, "email": "admin@localhost", - "login": "admin" + "emailVerified": false, + "grafanaAdmin": true, + "login": "admin", + "name": "", + "provisioned": false }, { + "disabled": false, "email": "admin2-1", - "login": "admin2-1" + "emailVerified": false, + "grafanaAdmin": true, + "login": "admin2-1", + "name": "", + "provisioned": false }, { + "disabled": false, "email": "editor-1", - "login": "editor-1" + "emailVerified": false, + "grafanaAdmin": false, + "login": "editor-1", + "name": "", + "provisioned": false }, { + "disabled": false, "email": "viewer-1", - "login": "viewer-1" + "emailVerified": false, + "grafanaAdmin": false, + "login": "viewer-1", + "name": "", + "provisioned": false } ]`, found) @@ -114,20 +135,40 @@ func TestIntegrationIdentity(t *testing.T) { found = teamClient.SpecJSON(rsp) require.JSONEq(t, `[ { + "disabled": false, "email": "admin2-1", - "login": "admin2-1" + "emailVerified": false, + "grafanaAdmin": true, + "login": "admin2-1", + "name": "", + "provisioned": false }, { + "disabled": false, "email": "admin2-2", - "login": "admin2-2" + "emailVerified": false, + "grafanaAdmin": false, + "login": "admin2-2", + "name": "", + "provisioned": false }, { + "disabled": false, "email": "editor-2", - "login": "editor-2" + "emailVerified": false, + "grafanaAdmin": false, + "login": "editor-2", + "name": "", + "provisioned": false }, { + "disabled": false, "email": "viewer-2", - "login": "viewer-2" + "emailVerified": false, + "grafanaAdmin": false, + "login": "viewer-2", + "name": "", + "provisioned": false } ]`, found) }) diff --git a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json index 7ef15e8f82b..03fbbfc37c3 100644 --- a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json @@ -865,6 +865,296 @@ } ] }, + "/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/checks/{name}/status": { + "get": { + "tags": [ + "Check" + ], + "description": "read status of the specified Check", + "operationId": "getCheckStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "advisor.grafana.app", + "version": "v0alpha1", + "kind": "Check" + } + }, + "put": { + "tags": [ + "Check" + ], + "description": "replace status of the specified Check", + "operationId": "replaceCheckStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "advisor.grafana.app", + "version": "v0alpha1", + "kind": "Check" + } + }, + "patch": { + "tags": [ + "Check" + ], + "description": "partially update status of the specified Check", + "operationId": "updateCheckStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.Check" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "advisor.grafana.app", + "version": "v0alpha1", + "kind": "Check" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Check", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/checktypes": { "get": { "tags": [ @@ -1694,6 +1984,296 @@ } } ] + }, + "/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/checktypes/{name}/status": { + "get": { + "tags": [ + "CheckType" + ], + "description": "read status of the specified CheckType", + "operationId": "getCheckTypeStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "advisor.grafana.app", + "version": "v0alpha1", + "kind": "CheckType" + } + }, + "put": { + "tags": [ + "CheckType" + ], + "description": "replace status of the specified CheckType", + "operationId": "replaceCheckTypeStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "advisor.grafana.app", + "version": "v0alpha1", + "kind": "CheckType" + } + }, + "patch": { + "tags": [ + "CheckType" + ], + "description": "partially update status of the specified CheckType", + "operationId": "updateCheckTypeStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckType" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "advisor.grafana.app", + "version": "v0alpha1", + "kind": "CheckType" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the CheckType", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] } }, "components": { @@ -1723,6 +2303,7 @@ ] }, "spec": { + "description": "Spec is the spec of the Check", "default": {}, "allOf": [ { @@ -1809,6 +2390,33 @@ } ] }, + "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReport": { + "type": "object", + "required": [ + "count", + "failures" + ], + "properties": { + "count": { + "description": "Number of elements analyzed", + "type": "integer", + "format": "int64", + "default": 0 + }, + "failures": { + "description": "List of failures", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReportFailure" + } + ] + } + } + } + }, "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReportFailure": { "type": "object", "required": [ @@ -1899,7 +2507,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckV0alpha1StatusReport" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReport" } ] } @@ -1930,6 +2538,7 @@ ] }, "spec": { + "description": "Spec is the spec of the CheckType", "default": {}, "allOf": [ { @@ -2102,33 +2711,6 @@ } } }, - "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckV0alpha1StatusReport": { - "type": "object", - "required": [ - "count", - "failures" - ], - "properties": { - "count": { - "description": "Number of elements analyzed", - "type": "integer", - "format": "int64", - "default": 0 - }, - "failures": { - "description": "List of failures", - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckReportFailure" - } - ] - } - } - } - }, "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CheckstatusOperatorState": { "type": "object", "required": [ diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 2f4dafbaa16..b5e33c9e980 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3058,6 +3058,20 @@ "sort": { "type": "string", "default": "" + }, + "staticOptions": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption" + } + ] + } + }, + "staticOptionsOrder": { + "type": "string" } } }, diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index 3d70923422a..6c3fae072bb 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -95,27 +95,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountList" } } } @@ -253,17 +253,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccount" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccount" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccount" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount" } } } @@ -321,7 +321,7 @@ "content": { "*/*": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserTeamList" + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountTokenList" } } } @@ -331,14 +331,14 @@ "x-kubernetes-group-version-kind": { "group": "iam.grafana.app", "version": "v0alpha1", - "kind": "UserTeamList" + "kind": "ServiceAccountTokenList" } }, "parameters": [ { "name": "name", "in": "path", - "description": "name of the UserTeamList", + "description": "name of the ServiceAccountTokenList", "required": true, "schema": { "type": "string", @@ -920,27 +920,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingList" } } } @@ -1078,17 +1078,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBinding" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBinding" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBinding" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBinding" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBinding" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBinding" } } } @@ -1146,27 +1146,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList" } } } @@ -1304,17 +1304,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Team" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.Team" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Team" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.Team" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Team" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.Team" } } } @@ -1421,27 +1421,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserList" } } } @@ -1579,17 +1579,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.User" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.User" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.User" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.User" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.User" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.User" } } } @@ -1686,6 +1686,731 @@ }, "components": { "schemas": { + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the ServiceAccount", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "ServiceAccount", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccount" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "ServiceAccountList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountSpec": { + "type": "object", + "required": [ + "title", + "disabled" + ], + "properties": { + "disabled": { + "type": "boolean", + "default": false + }, + "title": { + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountstatusOperatorState" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.Team": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Team", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "Team", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBinding": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the TeamBinding", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "TeamBinding", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBinding" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "TeamBindingList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingSpec": { + "type": "object", + "required": [ + "subjects", + "teamRef" + ], + "properties": { + "subjects": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject" + } + ] + } + }, + "teamRef": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingTeamRef" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingstatusOperatorState" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingTeamRef": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name is the unique identifier for a team.", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingspecSubject": { + "type": "object", + "required": [ + "name", + "permission" + ], + "properties": { + "name": { + "description": "uid of the identity", + "type": "string", + "default": "" + }, + "permission": { + "description": "permission of the identity in the team", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.Team" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "TeamList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamSpec": { + "type": "object", + "required": [ + "title", + "email" + ], + "properties": { + "email": { + "type": "string", + "default": "" + }, + "title": { + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamstatusOperatorState" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.User": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the User", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "User", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.User" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "iam.grafana.app", + "kind": "UserList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserSpec": { + "type": "object", + "required": [ + "disabled", + "email", + "emailVerified", + "grafanaAdmin", + "login", + "name", + "provisioned" + ], + "properties": { + "disabled": { + "type": "boolean", + "default": false + }, + "email": { + "type": "string", + "default": "" + }, + "emailVerified": { + "type": "boolean", + "default": false + }, + "grafanaAdmin": { + "type": "boolean", + "default": false + }, + "login": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + }, + "provisioned": { + "description": "What to do with salt, rands and password?", + "type": "boolean", + "default": false + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserstatusOperatorState" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { "type": "object", "additionalProperties": true, @@ -1895,104 +2620,21 @@ } } }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccount": { - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" - } - ] - }, - "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountSpec" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "ServiceAccount", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountList": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "items": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccount" - } - ] - } - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "ServiceAccountList", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountSpec": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - }, - "title": { - "type": "string" - } - } - }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountToken": { "type": "object", "required": [ "created" ], "properties": { - "created": {}, - "expires": {}, - "lastUsed": {}, + "created": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + }, + "expires": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + }, + "lastUsed": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + }, "name": { "type": "string" }, @@ -2002,103 +2644,6 @@ } }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountTokenList": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "items": { - "type": "array", - "items": { - "default": {} - } - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {} - } - } - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Team": { - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" - } - ] - }, - "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamSpec" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "Team", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBinding": { - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" - } - ] - }, - "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingSpec" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "TeamBinding", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingList": { "type": "object", "required": [ "items" @@ -2114,7 +2659,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBinding" + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.ServiceAccountToken" } ] } @@ -2135,73 +2680,7 @@ "x-kubernetes-group-version-kind": [ { "group": "iam.grafana.app", - "kind": "TeamBindingList", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamBindingSpec": { - "type": "object", - "properties": { - "subjects": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamSubject" - } - ] - } - }, - "team": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamRef" - } - ] - } - } - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamList": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "items": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.Team" - } - ] - } - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "TeamList", + "kind": "ServiceAccountTokenList", "version": "v0alpha1" } ] @@ -2300,17 +2779,6 @@ } } }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamSpec": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.TeamSubject": { "type": "object", "required": [ @@ -2319,12 +2787,7 @@ "properties": { "identity": { "description": "Identity is a reference to the identity of this subject.", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.IdentityRef" - } - ] + "default": {} }, "permission": { "description": "Permission subject has in team.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`", @@ -2336,104 +2799,6 @@ } } }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.User": { - "type": "object", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" - } - ] - }, - "spec": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserSpec" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "User", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserList": { - "type": "object", - "required": [ - "items" - ], - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "items": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.iam.v0alpha1.User" - } - ] - } - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "iam.grafana.app", - "kind": "UserList", - "version": "v0alpha1" - } - ] - }, - "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserSpec": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - }, - "email": { - "type": "string" - }, - "emailVerified": { - "type": "boolean" - }, - "login": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, "com.github.grafana.grafana.pkg.apis.iam.v0alpha1.UserTeam": { "type": "object", "properties": { @@ -2500,6 +2865,1514 @@ } ] }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRole": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the CoreRole", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec": { + "type": "object", + "required": [ + "title", + "description", + "version", + "group", + "permissions" + ], + "properties": { + "description": { + "type": "string", + "default": "" + }, + "group": { + "type": "string", + "default": "" + }, + "permissions": { + "description": "delegatable?: bool created? updated?", + "type": "array", + "items": { + "default": {} + } + }, + "title": { + "description": "Display name of the role", + "type": "string", + "default": "" + }, + "version": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolespecPermission": { + "type": "object", + "required": [ + "action", + "scope" + ], + "properties": { + "action": { + "description": "RBAC action (e.g: \"dashbaords:read\")", + "type": "string", + "default": "" + }, + "scope": { + "description": "RBAC scope (e.g: \"dashboards:uid:dash1\")", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRolestatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRole": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the GlobalRole", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBinding": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the GlobalRoleBinding", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec": { + "type": "object", + "required": [ + "subjects", + "roleRef" + ], + "properties": { + "roleRef": { + "default": {} + }, + "subjects": { + "type": "array", + "items": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecRoleRef": { + "type": "object", + "required": [ + "kind", + "name" + ], + "properties": { + "kind": { + "description": "kind of role", + "type": "string", + "default": "" + }, + "name": { + "description": "uid of the role", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingspecSubject": { + "type": "object", + "required": [ + "kind", + "name" + ], + "properties": { + "kind": { + "description": "kind of the identity getting the permission", + "type": "string", + "default": "" + }, + "name": { + "description": "uid of the identity", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec": { + "type": "object", + "required": [ + "title", + "description", + "version", + "group", + "permissions" + ], + "properties": { + "description": { + "type": "string", + "default": "" + }, + "group": { + "type": "string", + "default": "" + }, + "permissions": { + "description": "delegatable?: bool created? updated?", + "type": "array", + "items": { + "default": {} + } + }, + "title": { + "description": "Display name of the role", + "type": "string", + "default": "" + }, + "version": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolespecPermission": { + "type": "object", + "required": [ + "action", + "scope" + ], + "properties": { + "action": { + "description": "RBAC action (e.g: \"dashbaords:read\")", + "type": "string", + "default": "" + }, + "scope": { + "description": "RBAC scope (e.g: \"dashboards:uid:dash1\")", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRolestatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermission": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the ResourcePermission", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec": { + "type": "object", + "required": [ + "resource", + "permissions" + ], + "properties": { + "permissions": { + "type": "array", + "items": { + "default": {} + } + }, + "resource": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecPermission": { + "type": "object", + "required": [ + "kind", + "name", + "verbs" + ], + "properties": { + "kind": { + "description": "kind of the identity getting the permission", + "type": "string", + "default": "" + }, + "name": { + "description": "uid of the identity getting the permission", + "type": "string", + "default": "" + }, + "verbs": { + "description": "list of actions granted to the user (e.g. \"admin\" or \"get\", \"update\")", + "type": "array", + "items": { + "type": "string", + "default": "" + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionspecResource": { + "type": "object", + "required": [ + "apiGroup", + "resource", + "name" + ], + "properties": { + "apiGroup": { + "description": "api group of the resource (e.g: \"folder.grafana.app\")", + "type": "string", + "default": "" + }, + "name": { + "description": "uid of the resource (e.g: \"fold1\")", + "type": "string", + "default": "" + }, + "resource": { + "description": "kind of the resource (e.g: \"folders\")", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Role": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the Role", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBinding": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the RoleBinding", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec": { + "type": "object", + "required": [ + "subjects", + "roleRef" + ], + "properties": { + "roleRef": { + "default": {} + }, + "subjects": { + "type": "array", + "items": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecRoleRef": { + "type": "object", + "required": [ + "kind", + "name" + ], + "properties": { + "kind": { + "description": "kind of role", + "type": "string", + "default": "" + }, + "name": { + "description": "uid of the role", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingspecSubject": { + "type": "object", + "required": [ + "kind", + "name" + ], + "properties": { + "kind": { + "description": "kind of the identity getting the permission", + "type": "string", + "default": "" + }, + "name": { + "description": "uid of the identity", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec": { + "type": "object", + "required": [ + "title", + "description", + "version", + "group", + "permissions" + ], + "properties": { + "description": { + "type": "string", + "default": "" + }, + "group": { + "type": "string", + "default": "" + }, + "permissions": { + "description": "delegatable?: bool created? updated?", + "type": "array", + "items": { + "default": {} + } + }, + "title": { + "description": "Display name of the role", + "type": "string", + "default": "" + }, + "version": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolespecPermission": { + "type": "object", + "required": [ + "action", + "scope" + ], + "properties": { + "action": { + "description": "RBAC action (e.g: \"dashbaords:read\")", + "type": "string", + "default": "" + }, + "scope": { + "description": "RBAC scope (e.g: \"dashboards:uid:dash1\")", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RolestatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccount": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the ServiceAccount", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec": { + "type": "object", + "required": [ + "title", + "disabled" + ], + "properties": { + "disabled": { + "type": "boolean", + "default": false + }, + "title": { + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.Team": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the Team", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBinding": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the TeamBinding", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec": { + "type": "object", + "required": [ + "subjects", + "teamRef" + ], + "properties": { + "subjects": { + "type": "array", + "items": { + "default": {} + } + }, + "teamRef": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingTeamRef": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Name is the unique identifier for a team.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingspecSubject": { + "type": "object", + "required": [ + "name", + "permission" + ], + "properties": { + "name": { + "description": "uid of the identity", + "type": "string", + "default": "" + }, + "permission": { + "description": "permission of the identity in the team", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": { + "type": "object", + "required": [ + "title", + "email" + ], + "properties": { + "email": { + "type": "string", + "default": "" + }, + "title": { + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.User": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + }, + "spec": { + "description": "Spec is the spec of the User", + "default": {} + }, + "status": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec": { + "type": "object", + "required": [ + "disabled", + "email", + "emailVerified", + "grafanaAdmin", + "login", + "name", + "provisioned" + ], + "properties": { + "disabled": { + "type": "boolean", + "default": false + }, + "email": { + "type": "string", + "default": "" + }, + "emailVerified": { + "type": "boolean", + "default": false + }, + "grafanaAdmin": { + "type": "boolean", + "default": false + }, + "login": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + }, + "provisioned": { + "description": "What to do with salt, rands and password?", + "type": "boolean", + "default": false + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + }, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", diff --git a/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json index 017323b2a1e..e1ce97830be 100644 --- a/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/investigations.grafana.app-v0alpha1.json @@ -865,6 +865,296 @@ } ] }, + "/apis/investigations.grafana.app/v0alpha1/namespaces/{namespace}/investigationindexes/{name}/status": { + "get": { + "tags": [ + "InvestigationIndex" + ], + "description": "read status of the specified InvestigationIndex", + "operationId": "getInvestigationIndexStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "investigations.grafana.app", + "version": "v0alpha1", + "kind": "InvestigationIndex" + } + }, + "put": { + "tags": [ + "InvestigationIndex" + ], + "description": "replace status of the specified InvestigationIndex", + "operationId": "replaceInvestigationIndexStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "investigations.grafana.app", + "version": "v0alpha1", + "kind": "InvestigationIndex" + } + }, + "patch": { + "tags": [ + "InvestigationIndex" + ], + "description": "partially update status of the specified InvestigationIndex", + "operationId": "updateInvestigationIndexStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.InvestigationIndex" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "investigations.grafana.app", + "version": "v0alpha1", + "kind": "InvestigationIndex" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the InvestigationIndex", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/investigations.grafana.app/v0alpha1/namespaces/{namespace}/investigations": { "get": { "tags": [ @@ -1694,6 +1984,296 @@ } } ] + }, + "/apis/investigations.grafana.app/v0alpha1/namespaces/{namespace}/investigations/{name}/status": { + "get": { + "tags": [ + "Investigation" + ], + "description": "read status of the specified Investigation", + "operationId": "getInvestigationStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "investigations.grafana.app", + "version": "v0alpha1", + "kind": "Investigation" + } + }, + "put": { + "tags": [ + "Investigation" + ], + "description": "replace status of the specified Investigation", + "operationId": "replaceInvestigationStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "investigations.grafana.app", + "version": "v0alpha1", + "kind": "Investigation" + } + }, + "patch": { + "tags": [ + "Investigation" + ], + "description": "partially update status of the specified Investigation", + "operationId": "updateInvestigationStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.investigations.pkg.apis.investigations.v0alpha1.Investigation" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "investigations.grafana.app", + "version": "v0alpha1", + "kind": "Investigation" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Investigation", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] } }, "components": { diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index 8079e4bf0b4..15ca8b2677e 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -1859,6 +1859,79 @@ } ] }, + "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/repositories/{name}/refs": { + "get": { + "tags": [ + "Repository" + ], + "summary": "Repository refs listing", + "description": "Get the repository references", + "operationId": "getRepositoryRefs", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {} + }, + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {} + } + } + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "provisioning.grafana.app", + "version": "v0alpha1", + "kind": "RefList" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the RefList", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/repositories/{name}/render/{guid}": { "get": { "tags": [ @@ -2499,6 +2572,37 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.BitbucketRepositoryConfig": { + "type": "object", + "required": [ + "branch" + ], + "properties": { + "branch": { + "description": "The branch to use in the repository.", + "type": "string", + "default": "" + }, + "encryptedToken": { + "description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", + "type": "string", + "format": "byte", + "x-kubernetes-list-type": "atomic" + }, + "path": { + "description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", + "type": "string" + }, + "token": { + "description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", + "type": "string" + }, + "url": { + "description": "The repository URL (e.g. `https://bitbucket.org/example/test`).", + "type": "string" + } + } + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ErrorDetails": { "type": "object", "required": [ @@ -2622,6 +2726,37 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.GitLabRepositoryConfig": { + "type": "object", + "required": [ + "branch" + ], + "properties": { + "branch": { + "description": "The branch to use in the repository.", + "type": "string", + "default": "" + }, + "encryptedToken": { + "description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.", + "type": "string", + "format": "byte", + "x-kubernetes-list-type": "atomic" + }, + "path": { + "description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.", + "type": "string" + }, + "token": { + "description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.", + "type": "string" + }, + "url": { + "description": "The repository URL (e.g. `https://gitlab.com/example/test`).", + "type": "string" + } + } + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.GitRepositoryConfig": { "type": "object", "required": [ @@ -3037,6 +3172,70 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RefItem": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "hash": { + "description": "The SHA hash of the commit this ref points to", + "type": "string" + }, + "name": { + "description": "The name of the reference (branch or tag)", + "type": "string", + "default": "" + }, + "refURL": { + "description": "The URL to the reference (branch or tag)", + "type": "string" + } + } + }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RefList": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.RefItem" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "provisioning.grafana.app", + "kind": "RefList", + "version": "v0alpha1" + } + ] + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.Repository": { "description": "When this code is changed, make sure to update the code generation. As of writing, this can be done via the hack dir in the root of the repo: ./hack/update-codegen.sh provisioning If you've opened the generated files in this dir at some point in VSCode, you may also have to re-open them to clear errors.", "type": "object", @@ -3134,6 +3333,14 @@ "type" ], "properties": { + "bitbucket": { + "description": "The repository on Bitbucket. Mutually exclusive with local | github | git.", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.BitbucketRepositoryConfig" + } + ] + }, "description": { "description": "Repository description", "type": "string" @@ -3154,6 +3361,14 @@ } ] }, + "gitlab": { + "description": "The repository on GitLab. Mutually exclusive with local | github | git.", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.GitLabRepositoryConfig" + } + ] + }, "local": { "description": "The repository on the local file system. Mutually exclusive with local | github.", "allOf": [ @@ -3177,12 +3392,14 @@ "default": "" }, "type": { - "description": "The repository type. When selected oneOf the values below should be non-nil\n\nPossible enum values:\n - `\"git\"`\n - `\"github\"`\n - `\"local\"`", + "description": "The repository type. When selected oneOf the values below should be non-nil\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"git\"`\n - `\"github\"`\n - `\"gitlab\"`\n - `\"local\"`", "type": "string", "default": "", "enum": [ + "bitbucket", "git", "github", + "gitlab", "local" ] }, @@ -3291,12 +3508,14 @@ "default": "" }, "type": { - "description": "The repository type\n\nPossible enum values:\n - `\"git\"`\n - `\"github\"`\n - `\"local\"`", + "description": "The repository type\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"git\"`\n - `\"github\"`\n - `\"gitlab\"`\n - `\"local\"`", "type": "string", "default": "", "enum": [ + "bitbucket", "git", "github", + "gitlab", "local" ] }, @@ -3325,6 +3544,21 @@ "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", "type": "string" }, + "availableRepositoryTypes": { + "description": "AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc)", + "type": "array", + "items": { + "type": "string", + "default": "", + "enum": [ + "bitbucket", + "git", + "github", + "gitlab", + "local" + ] + } + }, "items": { "type": "array", "items": { @@ -3539,12 +3773,14 @@ "default": "" }, "type": { - "description": "The repository type\n\nPossible enum values:\n - `\"git\"`\n - `\"github\"`\n - `\"local\"`", + "description": "The repository type\n\nPossible enum values:\n - `\"bitbucket\"`\n - `\"git\"`\n - `\"github\"`\n - `\"gitlab\"`\n - `\"local\"`", "type": "string", "default": "", "enum": [ + "bitbucket", "git", "github", + "gitlab", "local" ] } diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index bf63e2358fc..bd398addb04 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -2,10 +2,7 @@ package provisioning import ( "context" - "crypto/sha256" - "encoding/hex" "encoding/json" - "net/http" "os" "path" "strings" @@ -13,7 +10,6 @@ import ( "text/template" "time" - gh "github.com/google/go-github/v70/github" ghmock "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -201,6 +197,13 @@ func withLogs(opts *testinfra.GrafanaOpts) { opts.EnableLog = true } +func useAppPlatformSecrets(opts *testinfra.GrafanaOpts) { + opts.EnableFeatureToggles = append(opts.EnableFeatureToggles, + featuremgmt.FlagProvisioningSecretsService, + featuremgmt.FlagSecretsManagementAppPlatform, + ) +} + func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper { provisioningPath := t.TempDir() opts := testinfra.GrafanaOpts{ @@ -224,20 +227,8 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper } helper := apis.NewK8sTestHelper(t, opts) - helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( - ghmock.WithRequestMatchHandler(ghmock.GetUser, ghAlwaysWrite(t, &gh.User{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), - ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposByOwnerByRepo, ghAlwaysWrite(t, &gh.Repository{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposBranchesByOwnerByRepoByBranch, ghAlwaysWrite(t, &gh.Branch{})), - ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, ghAlwaysWrite(t, &gh.Tree{})), - ghmock.WithRequestMatchHandler( - ghmock.DeleteReposHooksByOwnerByRepoByHookId, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - }), - ), - ) + // FIXME: keeping this line here to keep the dependency around until we have tests which use this again. + helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient() repositories := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, @@ -310,33 +301,6 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper } } -func ghAlwaysWrite(t *testing.T, body any) http.HandlerFunc { - marshalled := ghmock.MustMarshal(body) - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, err := w.Write(marshalled) - require.NoError(t, err, "failed to write body in mock") - }) -} - -func ghHandleTree(t *testing.T, refs map[string][]*gh.TreeEntry) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sha := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] - require.NotEmpty(t, sha, "sha path parameter was missing?") - - entries := refs[sha] - require.NotNil(t, entries, "no entries for sha %s", sha) - - tree := &gh.Tree{ - SHA: gh.Ptr(sha), - Truncated: gh.Ptr(false), - Entries: entries, - } - - _, err := w.Write(ghmock.MustMarshal(tree)) - require.NoError(t, err, "failed to write body in mock") - }) -} - func mustNestedString(obj map[string]interface{}, fields ...string) string { v, _, err := unstructured.NestedString(obj, fields...) if err != nil { @@ -350,15 +314,6 @@ func asJSON(obj any) []byte { return jj } -func treeEntryDir(dirName string, sha string) *gh.TreeEntry { - return &gh.TreeEntry{ - SHA: gh.Ptr(sha), - Path: gh.Ptr(dirName), - Type: gh.Ptr("tree"), - Mode: gh.Ptr("040000"), - } -} - func unstructuredToRepository(t *testing.T, obj *unstructured.Unstructured) *provisioning.Repository { bytes, err := obj.MarshalJSON() require.NoError(t, err) @@ -369,33 +324,3 @@ func unstructuredToRepository(t *testing.T, obj *unstructured.Unstructured) *pro return repo } - -func treeEntry(fpath string, content []byte) *gh.TreeEntry { - sha := sha256.Sum256(content) - - return &gh.TreeEntry{ - SHA: gh.Ptr(hex.EncodeToString(sha[:])), - Path: gh.Ptr(fpath), - Size: gh.Ptr(len(content)), - Type: gh.Ptr("blob"), - Mode: gh.Ptr("100644"), - Content: gh.Ptr(string(content)), - } -} - -func repoContent(fpath string, content []byte) *gh.RepositoryContent { - sha := sha256.Sum256(content) - typ := "blob" - if strings.HasSuffix(fpath, "/") { - typ = "tree" - } - - return &gh.RepositoryContent{ - SHA: gh.Ptr(hex.EncodeToString(sha[:])), - Name: gh.Ptr(path.Base(fpath)), - Path: &fpath, - Size: gh.Ptr(len(content)), - Type: &typ, - Content: gh.Ptr(string(content)), - } -} diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index f9a4be946fc..4040a854b7a 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -3,16 +3,14 @@ package provisioning import ( "context" "encoding/json" + "fmt" "net/http" "os" "path/filepath" - "regexp" "strings" "testing" "time" - gh "github.com/google/go-github/v70/github" - ghmock "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -22,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/tests/apis" @@ -85,6 +84,36 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { Suffix("files/"). Do(context.Background()) require.NoError(t, rsp.Error()) + + // Verify that we can list refs + rsp = helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(name). + Suffix("refs"). + Do(context.Background()) + + if expectedRepo.Spec.Type == provisioning.LocalRepositoryType { + require.ErrorContains(t, rsp.Error(), "does not support versioned operations") + } else { + require.NoError(t, rsp.Error()) + refs := &provisioning.RefList{} + err = rsp.Into(refs) + require.NoError(t, err) + require.True(t, len(refs.Items) >= 1, "should have at least one ref") + + var foundBranch bool + for _, ref := range refs.Items { + // FIXME: this assertion should be improved for all git types and take things from config + if ref.Name == "integration-test" { + require.Equal(t, "0f3370c212b04b9704e00f6926ef339bf91c7a1b", ref.Hash) + require.Equal(t, "https://github.com/grafana/grafana-git-sync-demo/tree/integration-test", ref.RefURL) + foundBranch = true + } + } + + require.True(t, foundBranch, "branch should be found") + } }) } @@ -99,6 +128,22 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { err := rsp.Into(settings) require.NoError(t, err) require.Len(t, settings.Items, len(inputFiles)) + + // FIXME: this should be an enterprise integration test + if extensions.IsEnterprise { + require.ElementsMatch(t, []provisioning.RepositoryType{ + provisioning.LocalRepositoryType, + provisioning.GitHubRepositoryType, + provisioning.GitRepositoryType, + provisioning.BitbucketRepositoryType, + provisioning.GitLabRepositoryType, + }, settings.AvailableRepositoryTypes) + } else { + require.ElementsMatch(t, []provisioning.RepositoryType{ + provisioning.LocalRepositoryType, + provisioning.GitHubRepositoryType, + }, settings.AvailableRepositoryTypes) + } }) t.Run("Repositories are reported in stats", func(t *testing.T) { @@ -173,7 +218,7 @@ func TestIntegrationProvisioning_FailInvalidSchema(t *testing.T) { }, time.Second*10, time.Millisecond*10, "Expected to be able to start a sync job") require.EventuallyWithT(t, func(collect *assert.CollectT) { - //helper.TriggerJobProcessing(t) + // helper.TriggerJobProcessing(t) result, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "jobs", string(jobObj.GetUID())) @@ -211,50 +256,25 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) { helper := runGrafana(t) ctx := context.Background() - helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( - ghmock.WithRequestMatchHandler(ghmock.GetUser, ghAlwaysWrite(t, &gh.User{Name: gh.Ptr("github-user")})), - ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), - ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{ID: gh.Ptr(int64(123))})), - ghmock.WithRequestMatchHandler(ghmock.GetReposByOwnerByRepo, ghAlwaysWrite(t, &gh.Repository{ID: gh.Ptr(int64(234))})), - ghmock.WithRequestMatchHandler( - ghmock.GetReposBranchesByOwnerByRepoByBranch, - ghAlwaysWrite(t, &gh.Branch{ - Name: gh.Ptr("main"), - Commit: &gh.RepositoryCommit{SHA: gh.Ptr("deadbeef")}, - }), - ), - ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, - ghHandleTree(t, map[string][]*gh.TreeEntry{ - "deadbeef": { - treeEntryDir("grafana", "subtree"), - }, - "subtree": { - treeEntry("dashboard.json", helper.LoadFile("testdata/all-panels.json")), - treeEntryDir("subdir", "subtree2"), - treeEntry("subdir/dashboard2.yaml", helper.LoadFile("testdata/text-options.json")), - }, - })), - ghmock.WithRequestMatchHandler( - ghmock.GetReposContentsByOwnerByRepoByPath, - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - pathRegex := regexp.MustCompile(`/repos/[^/]+/[^/]+/contents/(.*)`) - matches := pathRegex.FindStringSubmatch(r.URL.Path) - require.NotNil(t, matches, "no match for contents?") - path := matches[1] + // FIXME: instead of using an existing GitHub repository, we should create a new one for the tests and a branch + // This was the previous structure + // ghmock.WithRequestMatchHandler(ghmock.GetReposGitTreesByOwnerByRepoByTreeSha, + // ghHandleTree(t, map[string][]*gh.TreeEntry{ + // "deadbeef": { + // treeEntryDir("grafana", "subtree"), + // }, + // "subtree": { + // treeEntry("dashboard.json", helper.LoadFile("testdata/all-panels.json")), + // treeEntryDir("subdir", "subtree2"), + // treeEntry("subdir/dashboard2.yaml", helper.LoadFile("testdata/text-options.json")), + // }, + // })), - var err error - switch path { - case "grafana/dashboard.json": - _, err = w.Write(ghmock.MustMarshal(repoContent(path, helper.LoadFile("testdata/all-panels.json")))) - case "grafana/subdir/dashboard2.yaml": - _, err = w.Write(ghmock.MustMarshal(repoContent(path, helper.LoadFile("testdata/text-options.json")))) - default: - t.Fatalf("got unexpected path: %s", path) - } - require.NoError(t, err) - }), - ), - ) + // FIXME: uncomment these to implement webhook integration tests. + // helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient( + // ghmock.WithRequestMatchHandler(ghmock.GetReposHooksByOwnerByRepo, ghAlwaysWrite(t, []*gh.Hook{})), + // ghmock.WithRequestMatchHandler(ghmock.PostReposHooksByOwnerByRepo, ghAlwaysWrite(t, &gh.Hook{ID: gh.Ptr(int64(123))})), + // ) const repo = "github-create-test" _, err := helper.Repositories.Resource.Create(ctx, @@ -279,8 +299,10 @@ func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) { for _, v := range found.Items { names = append(names, v.GetName()) } - assert.Contains(t, names, "n1jR8vnnz", "should contain dashboard.json's contents") - assert.Contains(t, names, "WZ7AhQiVz", "should contain dashboard2.yaml's contents") + require.Len(t, names, 3, "should have three dashboards") + assert.Contains(t, names, "adg5vbj", "should contain dashboard.json's contents") + assert.Contains(t, names, "admfz74", "should contain dashboard2.yaml's contents") + assert.Contains(t, names, "adn5mxb", "should contain dashboard2.yaml's contents") err = helper.Repositories.Resource.Delete(ctx, repo, metav1.DeleteOptions{}) require.NoError(t, err, "should delete values") @@ -651,3 +673,128 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) { require.Nil(t, obj["status"], "should not have a status element") } } + +func TestIntegrationProvisioning_DeleteResources(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "delete-test-repo" + localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo, + "SyncEnabled": true, + "SyncTarget": "instance", + }) + _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // create the structure: + // dashboard1.json + // folder/ + // dashboard2.json + // nested/ + // dashboard3.json + dashboard1 := helper.LoadFile("testdata/all-panels.json") + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Body(dashboard1). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error()) + dashboard2 := helper.LoadFile("testdata/text-options.json") + result = helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "folder", "dashboard2.json"). + Body(dashboard2). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error()) + dashboard3 := helper.LoadFile("testdata/timeline-demo.json") + result = helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "folder", "nested", "dashboard3.json"). + Body(dashboard3). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error()) + + // make sure we don't fail when there is a .keep file in a folder + helper.CopyToProvisioningPath(t, "testdata/.keep", "folder/nested/.keep") + + dashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 3, len(dashboards.Items)) + + folders, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(folders.Items)) + + t.Run("delete individual dashboard file, should delete from repo and grafana", func(t *testing.T) { + result := helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "dashboard1.json"). + Do(ctx) + require.NoError(t, result.Error()) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "dashboard1.json") + require.Error(t, err) + dashboards, err = helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(dashboards.Items)) + }) + + t.Run("delete folder, should delete from repo and grafana all nested resources too", func(t *testing.T) { + // need to delete directly through the url, because the k8s client doesn't support `/` in a subresource + // but that is needed by gitsync to know that it is a folder + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr().String() + url := fmt.Sprintf("http://admin:admin@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/folder/", addr, repo) + req, err := http.NewRequest(http.MethodDelete, url, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + // should be deleted from the repo + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder") + require.Error(t, err) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "dashboard2.json") + require.Error(t, err) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "nested") + require.Error(t, err) + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "folder", "nested", "dashboard3.json") + require.Error(t, err) + + // all should be deleted from grafana + for _, d := range dashboards.Items { + _, err = helper.DashboardsV1.Resource.Get(ctx, d.GetName(), metav1.GetOptions{}) + require.Error(t, err) + } + for _, f := range folders.Items { + _, err = helper.Folders.Resource.Get(ctx, f.GetName(), metav1.GetOptions{}) + require.Error(t, err) + } + }) + + t.Run("deleting a non-existent file should fail", func(t *testing.T) { + result := helper.AdminREST.Delete(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "non-existent.json"). + Do(ctx) + require.Error(t, result.Error()) + }) +} diff --git a/pkg/tests/apis/provisioning/secrets_test.go b/pkg/tests/apis/provisioning/secrets_test.go new file mode 100644 index 00000000000..6f1e152eca0 --- /dev/null +++ b/pkg/tests/apis/provisioning/secrets_test.go @@ -0,0 +1,531 @@ +package provisioning + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "strings" + "testing" + "time" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestIntegrationProvisioning_LegacySecrets(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + ctx := context.Background() + + type expectedField struct { + Path []string + ExpectedDecryptedValue string + } + + secretsService := helper.GetEnv().RepositorySecrets + tests := []struct { + name string + values map[string]any + inputFile string + expectedFields []expectedField + }{ + { + name: "github token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/github-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedDecryptedValue: "some-token", + }, + }, + }, + { + name: "git token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/git-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedDecryptedValue: "some-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + repo := unstructuredToRepository(t, output) + + // Move encrypted token mutation + for _, expectedField := range test.expectedFields { + value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name") + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + }) + } +} + +func TestIntegrationProvisioning_Secrets_LegacyUpdate(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + updateOptions := metav1.UpdateOptions{} + ctx := context.Background() + + secretsService := helper.GetEnv().RepositorySecrets + + type expectedField struct { + Path []string + ExpectedValue string + ExpectedDecryptedValue string + } + + tests := []struct { + name string + values map[string]any + inputFile string + updateValues map[string]any + expectedFields []expectedField + }{ + { + name: "update github token (legacy secrets)", + values: map[string]any{ + "Token": "initial-token", + }, + inputFile: "testdata/github-readonly.json.tmpl", + updateValues: map[string]any{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + { + name: "update git token (legacy secrets)", + values: map[string]any{ + "Token": "initial-token", + }, + inputFile: "testdata/git-readonly.json.tmpl", + updateValues: map[string]any{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create initial resource + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + + // Prepare updated resource + updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues) + // Set the same name and resourceVersion for update + updatedInput.Object["metadata"].(map[string]any)["name"] = name + + // Fetch current resourceVersion + current, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to get current resource for update") + updatedInput.Object["metadata"].(map[string]any)["resourceVersion"] = current.Object["metadata"].(map[string]any)["resourceVersion"] + + _, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions) + require.NoError(t, err, "failed to update resource") + + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource after update") + repo := unstructuredToRepository(t, output) + + for _, expectedField := range test.expectedFields { + value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name") + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + }) + } +} + +func TestIntegrationProvisioning_Secrets(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t, useAppPlatformSecrets) + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + ctx := context.Background() + + secretsService := helper.GetEnv().RepositorySecrets + + type expectedField struct { + Path []string + ExpectedValue string + ExpectedDecryptedValue string + } + // TODO: Add test of fallbacks + tests := []struct { + name string + values map[string]any + inputFile string + expectedFields []expectedField + }{ + { + name: "github token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/github-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedValue: "github-token", + ExpectedDecryptedValue: "some-token", + }, + }, + }, + { + name: "git token encrypted", + values: map[string]any{ + "Token": "some-token", + }, + inputFile: "testdata/git-readonly.json.tmpl", + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedValue: "git-token", + ExpectedDecryptedValue: "some-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + repo := unstructuredToRepository(t, output) + + // Move encrypted token mutation + for _, expectedField := range test.expectedFields { + value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + + if expectedField.ExpectedValue != "" { + require.Equal(t, name+"-"+expectedField.ExpectedValue, value) + } + + if expectedField.ExpectedDecryptedValue != "" { + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + } + }) + } +} + +func TestIntegrationProvisioning_Secrets_Update(t *testing.T) { + ctx := context.Background() + helper := runGrafana(t, useAppPlatformSecrets) + secretsService := helper.GetEnv().RepositorySecrets + createOptions := metav1.CreateOptions{} + updateOptions := metav1.UpdateOptions{} + + type expectedField struct { + Path []string + ExpectedValue string + ExpectedDecryptedValue string + } + + tests := []struct { + name string + inputFile string + values map[string]interface{} + updateValues map[string]interface{} + expectedFields []expectedField + updatedFields []expectedField + }{ + { + name: "update encrypted git token", + inputFile: "testdata/git-readonly.json.tmpl", + values: map[string]interface{}{ + "Token": "initial-token", + }, + updateValues: map[string]interface{}{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedValue: "git-token", + ExpectedDecryptedValue: "initial-token", + }, + }, + updatedFields: []expectedField{ + { + Path: []string{"spec", "git", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "git", "encryptedToken"}, + ExpectedValue: "git-token", + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + { + name: "update encrypted github token", + inputFile: "testdata/github-readonly.json.tmpl", + values: map[string]interface{}{ + "Token": "initial-token", + }, + updateValues: map[string]interface{}{ + "Token": "updated-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedValue: "github-token", + ExpectedDecryptedValue: "initial-token", + }, + }, + updatedFields: []expectedField{ + { + Path: []string{"spec", "github", "token"}, + ExpectedDecryptedValue: "", + }, + { + Path: []string{"spec", "github", "encryptedToken"}, + ExpectedValue: "github-token", + ExpectedDecryptedValue: "updated-token", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create initial resource + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + + // Update the resource + updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues) + // Set the same name and resourceVersion for update + _ = unstructured.SetNestedField(updatedInput.Object, name, "metadata", "name") + _ = unstructured.SetNestedField(updatedInput.Object, output.GetResourceVersion(), "metadata", "resourceVersion") + _, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions) + require.NoError(t, err, "failed to update resource") + + updatedOutput, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back updated resource") + updatedRepo := unstructuredToRepository(t, updatedOutput) + + // Check updated fields + for _, expectedField := range test.updatedFields { + value, decrypted := encryptedField(t, secretsService, updatedRepo, updatedOutput.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "") + + if expectedField.ExpectedValue != "" { + require.Equal(t, name+"-"+expectedField.ExpectedValue, value) + } + + if expectedField.ExpectedDecryptedValue != "" { + require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted) + } + } + }) + } +} + +func TestIntegrationProvisioning_Secrets_Removal(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := runGrafana(t, useAppPlatformSecrets) + secretsService := helper.GetEnv().RepositorySecrets + createOptions := metav1.CreateOptions{} + + type expectedField struct { + Path []string + } + + tests := []struct { + name string + inputFile string + values map[string]interface{} + expectedFields []expectedField + updatedFields []expectedField + }{ + { + name: "remove encrypted git token", + inputFile: "testdata/git-readonly.json.tmpl", + values: map[string]interface{}{ + "Token": "initial-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "git", "encryptedToken"}, + }, + }, + }, + { + name: "remove encrypted github token", + inputFile: "testdata/github-readonly.json.tmpl", + values: map[string]interface{}{ + "Token": "initial-token", + }, + expectedFields: []expectedField{ + { + Path: []string{"spec", "github", "encryptedToken"}, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create initial resource + input := helper.RenderObject(t, test.inputFile, test.values) + _, err := helper.Repositories.Resource.Create(ctx, input, createOptions) + require.NoError(t, err, "failed to create resource") + + name := mustNestedString(input.Object, "metadata", "name") + output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err, "failed to read back resource") + + repo := unstructuredToRepository(t, output) + + // Set the same name and resourceVersion for update + err = helper.Repositories.Resource.Delete(ctx, name, metav1.DeleteOptions{}) + require.NoError(t, err, "failed to delete resource") + + for _, expectedField := range test.expectedFields { + secretName, found, err := base64DecodedField(output.Object, expectedField.Path) + require.NoError(t, err, "failed to decode base64 value") + require.True(t, found, "secretName should be found") + require.NotEmpty(t, secretName) + + var lastDecrypted []byte + require.Eventually(t, func() bool { + lastDecrypted, err = secretsService.Decrypt(ctx, repo, secretName) + return err != nil && errors.Is(err, contracts.ErrDecryptNotFound) + }, 1000*time.Second, 500*time.Millisecond, "expected ErrDecryptNotFound error, got %v", lastDecrypted) + } + }) + } +} + +func encryptedField(t *testing.T, secretsService secrets.RepositorySecrets, repo *provisioning.Repository, obj map[string]any, path []string, expectedValue bool) (string, string) { + value, found, err := base64DecodedField(obj, path) + if err != nil { + require.NoError(t, err, "failed to decode base64 value") + } + + if expectedValue { + decrypted, err := secretsService.Decrypt(context.Background(), repo, value) + require.NoError(t, err, "failed to eecrypt value") + return value, string(decrypted) + } else { + require.False(t, found, "value should not be found") + return "", "" + } +} + +func base64DecodedField(obj map[string]any, path []string) (string, bool, error) { + value, found, err := unstructured.NestedFieldNoCopy(obj, path...) + if err != nil { + return "", false, err + } + + if !found { + return "", false, nil + } + + valueStr, ok := value.(string) + if !ok { + return "", false, fmt.Errorf("value is not a string") + } + + decodedValue, err := base64.StdEncoding.DecodeString(valueStr) + if err != nil { + return "", false, fmt.Errorf("failed to decode base64 valueStr: %w", err) + } + + return string(decodedValue), true, nil +} diff --git a/pkg/tests/apis/provisioning/testdata/.keep b/pkg/tests/apis/provisioning/testdata/.keep new file mode 100644 index 00000000000..633f69fd54e --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/.keep @@ -0,0 +1 @@ +# This file ensures the folder/nested directory is tracked in version control \ No newline at end of file diff --git a/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl new file mode 100644 index 00000000000..38f0201b49f --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/git-readonly.json.tmpl @@ -0,0 +1,24 @@ +{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "metadata": { + "name": "{{ or .Name "git-repository" }}" + }, + "spec": { + "title": "{{ or .Title .Name "Git repository" }}", + "description": "{{ or .Description .Name "Load grafana dashboard from fake repository" }}", + "type": "git", + "git": { + "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", + "branch": "{{ or .Branch "integration-test" }}", + "token": "{{ or .Token "" }}", + "path": "{{ or .Path "grafana/" }}" + }, + "sync": { + "enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }}, + "target": "{{ or .SyncTarget "folder" }}", + "intervalSeconds": {{ or .SyncIntervalSeconds 60 }} + }, + "workflows": [] + } +} diff --git a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl index 220b0936b0e..86489c7d65c 100644 --- a/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl +++ b/pkg/tests/apis/provisioning/testdata/github-readonly.json.tmpl @@ -9,10 +9,10 @@ "description": "{{ or .Description .Name "Load grafana dashboard from fake repository" }}", "type": "github", "github": { - "url": "{{ or .URL "https://github.com/grafana/git-ui-sync-demo" }}", - "branch": "{{ or .Branch "dummy" }}", + "url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}", + "branch": "{{ or .Branch "integration-test" }}", "generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }}, - "token": "{{ or .Token "github_pat_dummy" }}", + "token": "{{ or .Token "" }}", "path": "{{ or .Path "grafana/" }}" }, "sync": { diff --git a/pkg/tests/apis/provisioning/testdata/timeline-demo.json b/pkg/tests/apis/provisioning/testdata/timeline-demo.json new file mode 120000 index 00000000000..af7d0ce7aac --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/timeline-demo.json @@ -0,0 +1 @@ +../../../../../devenv/dev-dashboards/panel-timeline/timeline-demo.json \ No newline at end of file diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index 74aba425b91..eb9b460c6b8 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -82,6 +83,8 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes runstore = true } + err = featuremgmt.InitOpenFeatureWithCfg(cfg) + require.NoError(t, err) env, err := server.InitializeForTest(t, t, cfg, serverOpts, apiServerOpts) require.NoError(t, err) diff --git a/pkg/tests/utils.go b/pkg/tests/utils.go index f81683c80f4..4ea428df9f7 100644 --- a/pkg/tests/utils.go +++ b/pkg/tests/utils.go @@ -5,10 +5,13 @@ import ( "crypto/tls" "net/url" "os" + "strings" "testing" "github.com/go-openapi/strfmt" goapi "github.com/grafana/grafana-openapi-client-go/client" + "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/resourcepermissions" @@ -19,9 +22,18 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/require" ) +func SkipIntegrationTestInShortMode(t testing.TB) { + t.Helper() + if !strings.HasPrefix(t.Name(), "TestIntegration") { + t.Fatal("test is not an integration test") + } + if testing.Short() { + t.Skip("skipping integration test in short mode") + } +} + func CreateUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCommand) int64 { t.Helper() diff --git a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go index 861924839ec..598accb4794 100644 --- a/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/azuremonitor/kinds/dataquery/types_dataquery_gen.go @@ -13,6 +13,7 @@ package dataquery import ( json "encoding/json" + fmt "fmt" ) type AzureMonitorQuery struct { @@ -835,3 +836,58 @@ type StringOrBoolOrFloat64OrSelectableValue struct { func NewStringOrBoolOrFloat64OrSelectableValue() *StringOrBoolOrFloat64OrSelectableValue { return &StringOrBoolOrFloat64OrSelectableValue{} } + +// MarshalJSON implements a custom JSON marshalling logic to encode `StringOrBoolOrFloat64OrSelectableValue` as JSON. +func (resource StringOrBoolOrFloat64OrSelectableValue) MarshalJSON() ([]byte, error) { + if resource.String != nil { + return json.Marshal(resource.String) + } + if resource.Bool != nil { + return json.Marshal(resource.Bool) + } + if resource.Float64 != nil { + return json.Marshal(resource.Float64) + } + if resource.SelectableValue != nil { + return json.Marshal(resource.SelectableValue) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode StringOrBoolOrFloat64OrSelectableValue from JSON. +func (resource *StringOrBoolOrFloat64OrSelectableValue) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + fields := make(map[string]json.RawMessage) + if err := json.Unmarshal(raw, &fields); err != nil { + return err + } + + if fields["String"] != nil { + if err := json.Unmarshal(fields["String"], &resource.String); err != nil { + return fmt.Errorf("error decoding field 'String': %w", err) + } + } + + if fields["Bool"] != nil { + if err := json.Unmarshal(fields["Bool"], &resource.Bool); err != nil { + return fmt.Errorf("error decoding field 'Bool': %w", err) + } + } + + if fields["Float64"] != nil { + if err := json.Unmarshal(fields["Float64"], &resource.Float64); err != nil { + return fmt.Errorf("error decoding field 'Float64': %w", err) + } + } + + if fields["SelectableValue"] != nil { + if err := json.Unmarshal(fields["SelectableValue"], &resource.SelectableValue); err != nil { + return fmt.Errorf("error decoding field 'SelectableValue': %w", err) + } + } + + return nil +} diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 28567e4e5d5..919b853887e 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "slices" "time" @@ -73,6 +74,7 @@ func (ds *DataSource) newAWSConfig(ctx context.Context, region string) (aws.Conf Region: region, AccessKey: ds.Settings.AccessKey, SecretKey: ds.Settings.SecretKey, + HTTPClient: &http.Client{}, } if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled { authSettings.ProxyOptions = ds.ProxyOpts diff --git a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go index 0057d07e182..76c706094a6 100644 --- a/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/cloudwatch/kinds/dataquery/types_dataquery_gen.go @@ -211,6 +211,12 @@ func NewQueryEditorProperty() *QueryEditorProperty { } } +type QueryEditorPropertyType string + +const ( + QueryEditorPropertyTypeString QueryEditorPropertyType = "string" +) + type QueryEditorArrayExpression struct { Type QueryEditorArrayExpressionType `json:"type"` Expressions ArrayOfQueryEditorExpressionOrArrayOfQueryEditorArrayExpression `json:"expressions"` @@ -279,12 +285,6 @@ func NewQueryEditorOperatorValueType() *QueryEditorOperatorValueType { return NewStringOrBoolOrInt64OrArrayOfQueryEditorOperatorType() } -type QueryEditorPropertyType string - -const ( - QueryEditorPropertyTypeString QueryEditorPropertyType = "string" -) - type LogsQueryLanguage string const ( diff --git a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go index ecd4d038beb..a53802ad738 100644 --- a/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/elasticsearch/kinds/dataquery/types_dataquery_gen.go @@ -13,6 +13,7 @@ package dataquery import ( json "encoding/json" + fmt "fmt" ) type BucketAggregation = DateHistogramOrHistogramOrTermsOrFiltersOrGeoHashGridOrNested @@ -1549,6 +1550,43 @@ func NewStringOrDataqueryInlineScript() *StringOrDataqueryInlineScript { return &StringOrDataqueryInlineScript{} } +// MarshalJSON implements a custom JSON marshalling logic to encode `StringOrDataqueryInlineScript` as JSON. +func (resource StringOrDataqueryInlineScript) MarshalJSON() ([]byte, error) { + if resource.String != nil { + return json.Marshal(resource.String) + } + if resource.DataqueryInlineScript != nil { + return json.Marshal(resource.DataqueryInlineScript) + } + + return []byte("null"), nil +} + +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode StringOrDataqueryInlineScript from JSON. +func (resource *StringOrDataqueryInlineScript) UnmarshalJSON(raw []byte) error { + if raw == nil { + return nil + } + fields := make(map[string]json.RawMessage) + if err := json.Unmarshal(raw, &fields); err != nil { + return err + } + + if fields["String"] != nil { + if err := json.Unmarshal(fields["String"], &resource.String); err != nil { + return fmt.Errorf("error decoding field 'String': %w", err) + } + } + + if fields["DataqueryInlineScript"] != nil { + if err := json.Unmarshal(fields["DataqueryInlineScript"], &resource.DataqueryInlineScript); err != nil { + return fmt.Errorf("error decoding field 'DataqueryInlineScript': %w", err) + } + } + + return nil +} + type BucketScriptOrCumulativeSumOrDerivativeOrSerialDiffOrRawDataOrRawDocumentOrUniqueCountOrPercentilesOrExtendedStatsOrMinOrMaxOrSumOrAverageOrMovingAverageOrMovingFunctionOrLogsOrRateOrTopMetrics struct { BucketScript *BucketScript `json:"BucketScript,omitempty"` CumulativeSum *CumulativeSum `json:"CumulativeSum,omitempty"` diff --git a/pkg/tsdb/influxdb/fsql/arrow.go b/pkg/tsdb/influxdb/fsql/arrow.go index a816918d27c..71c480875c0 100644 --- a/pkg/tsdb/influxdb/fsql/arrow.go +++ b/pkg/tsdb/influxdb/fsql/arrow.go @@ -124,7 +124,7 @@ func newFrame(schema *arrow.Schema) *data.Frame { func newField(f arrow.Field) *data.Field { switch f.Type.ID() { - case arrow.STRING: + case arrow.STRING, arrow.STRING_VIEW: return newDataField[string](f) case arrow.FLOAT32: return newDataField[float32](f) @@ -239,6 +239,8 @@ func copyData(field *data.Field, col arrow.Array) error { return err } } + case arrow.STRING_VIEW: + copyBasic[string](field, array.NewStringViewData(colData)) case arrow.STRING: copyBasic[string](field, array.NewStringData(colData)) case arrow.UINT8: diff --git a/pkg/tsdb/influxdb/fsql/arrow_test.go b/pkg/tsdb/influxdb/fsql/arrow_test.go index 442cf32811a..9002d9a5b3b 100644 --- a/pkg/tsdb/influxdb/fsql/arrow_test.go +++ b/pkg/tsdb/influxdb/fsql/arrow_test.go @@ -469,6 +469,43 @@ func TestCopyData_Float64(t *testing.T) { assert.Equal(t, float64(3.3), *field.CopyAt(2).(*float64)) } +func TestCopyData_StringView(t *testing.T) { + // Non-nullable StringView + field := data.NewField("field", nil, []string{}) + builder := array.NewStringViewBuilder(memory.DefaultAllocator) + builder.Append("apple") + builder.Append("banana") + builder.Append("cherry") + arr := builder.NewArray() + defer arr.Release() + + svArr := array.NewStringViewData(arr.Data()) + defer svArr.Release() + + err := copyData(field, svArr) + assert.NoError(t, err) + assert.Equal(t, "apple", field.CopyAt(0)) + assert.Equal(t, "banana", field.CopyAt(1)) + assert.Equal(t, "cherry", field.CopyAt(2)) + + // Nullable StringView + field = data.NewField("field", nil, []*string{}) + builder = array.NewStringViewBuilder(memory.DefaultAllocator) + builder.Append("dog") + builder.AppendNull() + builder.Append("cat") + arr2 := builder.NewArray() + defer arr2.Release() + svArr2 := array.NewStringViewData(arr2.Data()) + defer svArr2.Release() + + err = copyData(field, svArr2) + assert.NoError(t, err) + assert.Equal(t, "dog", *(field.CopyAt(0).(*string))) + assert.Nil(t, field.CopyAt(1)) + assert.Equal(t, "cat", *(field.CopyAt(2).(*string))) +} + func TestCustomMetadata(t *testing.T) { schema := arrow.NewSchema([]arrow.Field{ { diff --git a/pkg/util/testutil/mocks/T.go b/pkg/util/testutil/mocks/T.go index 2e2c110fb82..d85781703b0 100644 --- a/pkg/util/testutil/mocks/T.go +++ b/pkg/util/testutil/mocks/T.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.1. DO NOT EDIT. +// Code generated by mockery v2.53.4. DO NOT EDIT. package mocks @@ -50,11 +50,11 @@ func (_c *T_Cleanup_Call) Return() *T_Cleanup_Call { } func (_c *T_Cleanup_Call) RunAndReturn(run func(func())) *T_Cleanup_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// Deadline provides a mock function with given fields: +// Deadline provides a mock function with no fields func (_m *T) Deadline() (time.Time, bool) { ret := _m.Called() @@ -149,11 +149,11 @@ func (_c *T_Errorf_Call) Return() *T_Errorf_Call { } func (_c *T_Errorf_Call) RunAndReturn(run func(string, ...interface{})) *T_Errorf_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// FailNow provides a mock function with given fields: +// FailNow provides a mock function with no fields func (_m *T) FailNow() { _m.Called() } @@ -181,11 +181,11 @@ func (_c *T_FailNow_Call) Return() *T_FailNow_Call { } func (_c *T_FailNow_Call) RunAndReturn(run func()) *T_FailNow_Call { - _c.Call.Return(run) + _c.Run(run) return _c } -// Helper provides a mock function with given fields: +// Helper provides a mock function with no fields func (_m *T) Helper() { _m.Called() } @@ -213,7 +213,7 @@ func (_c *T_Helper_Call) Return() *T_Helper_Call { } func (_c *T_Helper_Call) RunAndReturn(run func()) *T_Helper_Call { - _c.Call.Return(run) + _c.Run(run) return _c } diff --git a/playwright.config.ts b/playwright.config.ts index 39ac6aebd5f..648e464f387 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,7 +3,8 @@ import path, { dirname } from 'path'; import { PluginOptions } from '@grafana/plugin-e2e'; -const testDirRoot = 'e2e/plugin-e2e/'; +const testDirRoot = 'e2e-playwright'; +const pluginDirRoot = path.join(testDirRoot, 'plugin-e2e'); export default defineConfig({ fullyParallel: true, @@ -11,7 +12,9 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, - reporter: 'html', + reporter: [ + ['html'], // pretty + ], use: { baseURL: `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`, trace: 'retain-on-failure', @@ -19,6 +22,8 @@ export default defineConfig({ username: 'admin', password: 'admin', }, + screenshot: 'only-on-failure', + permissions: ['clipboard-read', 'clipboard-write'], provisioningRootDir: path.join(process.cwd(), process.env.PROV_DIR ?? 'conf/provisioning'), }, projects: [ @@ -44,7 +49,7 @@ export default defineConfig({ // Run all tests in parallel using user with admin role { name: 'admin', - testDir: path.join(testDirRoot, '/plugin-e2e-api-tests/as-admin-user'), + testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-admin-user'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -54,7 +59,7 @@ export default defineConfig({ // Run all tests in parallel using user with viewer role { name: 'viewer', - testDir: path.join(testDirRoot, '/plugin-e2e-api-tests/as-viewer-user'), + testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-viewer-user'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/viewer.json', @@ -63,7 +68,7 @@ export default defineConfig({ }, { name: 'elasticsearch', - testDir: path.join(testDirRoot, '/elasticsearch'), + testDir: path.join(pluginDirRoot, '/elasticsearch'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -72,7 +77,7 @@ export default defineConfig({ }, { name: 'mysql', - testDir: path.join(testDirRoot, '/mysql'), + testDir: path.join(pluginDirRoot, '/mysql'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -81,7 +86,7 @@ export default defineConfig({ }, { name: 'mssql', - testDir: path.join(testDirRoot, '/mssql'), + testDir: path.join(pluginDirRoot, '/mssql'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -90,7 +95,7 @@ export default defineConfig({ }, { name: 'extensions-test-app', - testDir: 'e2e/test-plugins/grafana-extensionstest-app', + testDir: path.join(testDirRoot, '/test-plugins/grafana-extensionstest-app'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -99,7 +104,7 @@ export default defineConfig({ }, { name: 'grafana-e2etest-datasource', - testDir: 'e2e/test-plugins/grafana-test-datasource', + testDir: path.join(testDirRoot, '/test-plugins/grafana-test-datasource'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -108,7 +113,7 @@ export default defineConfig({ }, { name: 'cloudwatch', - testDir: path.join(testDirRoot, '/cloudwatch'), + testDir: path.join(pluginDirRoot, '/cloudwatch'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -117,7 +122,7 @@ export default defineConfig({ }, { name: 'azuremonitor', - testDir: path.join(testDirRoot, '/azuremonitor'), + testDir: path.join(pluginDirRoot, '/azuremonitor'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -126,7 +131,7 @@ export default defineConfig({ }, { name: 'cloudmonitoring', - testDir: path.join(testDirRoot, '/cloudmonitoring'), + testDir: path.join(pluginDirRoot, '/cloudmonitoring'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -135,7 +140,7 @@ export default defineConfig({ }, { name: 'graphite', - testDir: path.join(testDirRoot, '/graphite'), + testDir: path.join(pluginDirRoot, '/graphite'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -144,7 +149,7 @@ export default defineConfig({ }, { name: 'influxdb', - testDir: path.join(testDirRoot, '/influxdb'), + testDir: path.join(pluginDirRoot, '/influxdb'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -153,7 +158,7 @@ export default defineConfig({ }, { name: 'opentsdb', - testDir: path.join(testDirRoot, '/opentsdb'), + testDir: path.join(pluginDirRoot, '/opentsdb'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -162,7 +167,7 @@ export default defineConfig({ }, { name: 'jaeger', - testDir: path.join(testDirRoot, '/jaeger'), + testDir: path.join(pluginDirRoot, '/jaeger'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -171,7 +176,7 @@ export default defineConfig({ }, { name: 'grafana-postgresql-datasource', - testDir: path.join(testDirRoot, '/grafana-postgresql-datasource'), + testDir: path.join(pluginDirRoot, '/grafana-postgresql-datasource'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -180,7 +185,50 @@ export default defineConfig({ }, { name: 'zipkin', - testDir: path.join(testDirRoot, '/zipkin'), + testDir: path.join(pluginDirRoot, '/zipkin'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'scenarios', + testDir: path.join(testDirRoot, '/scenarios'), + use: { + ...devices['Desktop Chrome'], + }, + }, + { + name: 'various', + testDir: path.join(testDirRoot, '/various-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'panels', + testDir: path.join(testDirRoot, '/panels-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'smoke', + testDir: path.join(testDirRoot, '/smoke-tests-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'dashboards', + testDir: path.join(testDirRoot, '/dashboards-suite'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -196,5 +244,23 @@ export default defineConfig({ }, dependencies: ['authenticate'], }, + { + name: 'cloud-plugins', + testDir: path.join(testDirRoot, '/cloud-plugins-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'dashboard-new-layouts', + testDir: path.join(testDirRoot, '/dashboard-new-layouts'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, ], }); diff --git a/playwright.storybook.config.ts b/playwright.storybook.config.ts new file mode 100644 index 00000000000..faa18ce7d05 --- /dev/null +++ b/playwright.storybook.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; + +import { PluginOptions } from '@grafana/plugin-e2e'; + +const testDirRoot = 'e2e-playwright'; + +export default defineConfig({ + fullyParallel: true, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:9001', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { + command: 'yarn storybook', + url: 'http://localhost:9001', + stdout: 'ignore', + stderr: 'pipe', + }, + projects: [ + { + name: 'storybook', + testDir: path.join(testDirRoot, '/storybook'), + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index d431451f492..6d5885e3bdd 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5764,6 +5764,8 @@ "errorCode": { "type": "string", "enum": [ + "ALERT_RULES_QUOTA_REACHED", + "ALERT_RULES_GROUP_QUOTA_REACHED", "DATASOURCE_NAME_CONFLICT", "DATASOURCE_INVALID_URL", "DATASOURCE_ALREADY_MANAGED", diff --git a/public/api-merged.json b/public/api-merged.json index 0706252d551..2f2eed4fca5 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17319,6 +17319,8 @@ "errorCode": { "type": "string", "enum": [ + "ALERT_RULES_QUOTA_REACHED", + "ALERT_RULES_GROUP_QUOTA_REACHED", "DATASOURCE_NAME_CONFLICT", "DATASOURCE_INVALID_URL", "DATASOURCE_ALREADY_MANAGED", @@ -22068,7 +22070,7 @@ "type": "object", "properties": { "Object": { - "description": "Object is a JSON compatible map with string, float, int, bool, []interface{},\nor map[string]interface{} children.", + "description": "Object is a JSON compatible map with string, float, int, bool, []any,\nor map[string]any children.", "type": "object", "additionalProperties": {} } diff --git a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts b/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts index 86ca7fa2a38..d054b90a9f4 100644 --- a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts @@ -1,15 +1,11 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'Check', 'CheckType'] as const; +export const addTagTypes = ['Check', 'CheckType'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ - getApiResources: build.query({ - query: () => ({ url: `/apis/advisor.grafana.app/v0alpha1/` }), - providesTags: ['API Discovery'], - }), listCheck: build.query({ query: (queryArg) => ({ url: `/checks`, @@ -43,29 +39,6 @@ const injectedRtkApi = api }), invalidatesTags: ['Check'], }), - deletecollectionCheck: build.mutation({ - query: (queryArg) => ({ - url: `/checks`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - continue: queryArg['continue'], - dryRun: queryArg.dryRun, - fieldSelector: queryArg.fieldSelector, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - }, - }), - invalidatesTags: ['Check'], - }), getCheck: build.query({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -75,20 +48,6 @@ const injectedRtkApi = api }), providesTags: ['Check'], }), - replaceCheck: build.mutation({ - query: (queryArg) => ({ - url: `/checks/${queryArg.name}`, - method: 'PUT', - body: queryArg.check, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['Check'], - }), deleteCheck: build.mutation({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -138,81 +97,6 @@ const injectedRtkApi = api }), providesTags: ['CheckType'], }), - createCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes`, - method: 'POST', - body: queryArg.checkType, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['CheckType'], - }), - deletecollectionCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - continue: queryArg['continue'], - dryRun: queryArg.dryRun, - fieldSelector: queryArg.fieldSelector, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - }, - }), - invalidatesTags: ['CheckType'], - }), - getCheckType: build.query({ - query: (queryArg) => ({ - url: `/checktypes/${queryArg.name}`, - params: { - pretty: queryArg.pretty, - }, - }), - providesTags: ['CheckType'], - }), - replaceCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes/${queryArg.name}`, - method: 'PUT', - body: queryArg.checkType, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['CheckType'], - }), - deleteCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes/${queryArg.name}`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - }, - }), - invalidatesTags: ['CheckType'], - }), updateCheckType: build.mutation({ query: (queryArg) => ({ url: `/checktypes/${queryArg.name}`, @@ -232,8 +116,6 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; -export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; -export type GetApiResourcesApiArg = void; export type ListCheckApiResponse = /** status 200 OK */ CheckList; export type ListCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -294,57 +176,6 @@ export type CreateCheckApiArg = { fieldValidation?: string; check: Check; }; -export type DeletecollectionCheckApiResponse = /** status 200 OK */ Status; -export type DeletecollectionCheckApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; -}; export type GetCheckApiResponse = /** status 200 OK */ Check; export type GetCheckApiArg = { /** name of the Check */ @@ -352,20 +183,6 @@ export type GetCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; }; -export type ReplaceCheckApiResponse = /** status 200 OK */ Check | /** status 201 Created */ Check; -export type ReplaceCheckApiArg = { - /** name of the Check */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - check: Check; -}; export type DeleteCheckApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; export type DeleteCheckApiArg = { /** name of the Check */ @@ -444,110 +261,6 @@ export type ListCheckTypeApiArg = { /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ watch?: boolean; }; -export type CreateCheckTypeApiResponse = /** status 200 OK */ - | CheckType - | /** status 201 Created */ CheckType - | /** status 202 Accepted */ CheckType; -export type CreateCheckTypeApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - checkType: CheckType; -}; -export type DeletecollectionCheckTypeApiResponse = /** status 200 OK */ Status; -export type DeletecollectionCheckTypeApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; -}; -export type GetCheckTypeApiResponse = /** status 200 OK */ CheckType; -export type GetCheckTypeApiArg = { - /** name of the CheckType */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; -}; -export type ReplaceCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; -export type ReplaceCheckTypeApiArg = { - /** name of the CheckType */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - checkType: CheckType; -}; -export type DeleteCheckTypeApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; -export type DeleteCheckTypeApiArg = { - /** name of the CheckType */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; -}; export type UpdateCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; export type UpdateCheckTypeApiArg = { /** name of the CheckType */ @@ -564,38 +277,6 @@ export type UpdateCheckTypeApiArg = { force?: boolean; patch: Patch; }; -export type ApiResource = { - /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ - categories?: string[]; - /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ - group?: string; - /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ - kind: string; - /** name is the plural name of the resource. */ - name: string; - /** namespaced indicates if a resource is namespaced or not. */ - namespaced: boolean; - /** shortNames is a list of suggested short names of the resource. */ - shortNames?: string[]; - /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ - singularName: string; - /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ - storageVersionHash?: string; - /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ - verbs: string[]; - /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ - version?: string; -}; -export type ApiResourceList = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** groupVersion is the group and version this APIResourceList is for. */ - groupVersion: string; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - /** resources contains the name of the resources and if they are namespaced. */ - resources: ApiResource[]; -}; export type Time = string; export type FieldsV1 = object; export type ManagedFieldsEntry = { @@ -709,14 +390,14 @@ export type CheckReportFailure = { itemID: string; /** Links to actions that can be taken to resolve the failure */ links: CheckErrorLink[]; - /** More information about the failure */ + /** More information about the failure, not meant to be displayed to the user. Used for LLM suggestions. */ moreInfo?: string; /** Severity of the failure */ severity: string; /** Step ID that the failure is associated with */ stepID: string; }; -export type CheckV0Alpha1StatusReport = { +export type CheckReport = { /** Number of elements analyzed */ count: number; /** List of failures */ @@ -731,7 +412,7 @@ export type CheckStatus = { operatorStates?: { [key: string]: CheckstatusOperatorState; }; - report: CheckV0Alpha1StatusReport; + report: CheckReport; }; export type Check = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ @@ -739,6 +420,7 @@ export type Check = { /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; metadata: ObjectMeta; + /** Spec is the spec of the Check */ spec: CheckSpec; status: CheckStatus; }; @@ -843,6 +525,7 @@ export type CheckType = { /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; metadata: ObjectMeta; + /** Spec is the spec of the CheckType */ spec: CheckTypeSpec; status: CheckTypeStatus; }; diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index 65d3ae16f55..8f25d5fe74b 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -229,6 +229,10 @@ const injectedRtkApi = api query: (queryArg) => ({ url: `/repositories/${queryArg.name}/jobs/${queryArg.uid}` }), providesTags: ['Repository'], }), + getRepositoryRefs: build.query({ + query: (queryArg) => ({ url: `/repositories/${queryArg.name}/refs` }), + providesTags: ['Repository'], + }), getRepositoryRenderWithPath: build.query< GetRepositoryRenderWithPathApiResponse, GetRepositoryRenderWithPathApiArg @@ -590,6 +594,18 @@ export type GetRepositoryJobsWithPathApiArg = { /** Original Job UID */ uid: string; }; +export type GetRepositoryRefsApiResponse = /** status 200 OK */ { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: any[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: any; +}; +export type GetRepositoryRefsApiArg = { + /** name of the RefList */ + name: string; +}; export type GetRepositoryRenderWithPathApiResponse = unknown; export type GetRepositoryRenderWithPathApiArg = { /** name of the Repository */ @@ -835,6 +851,20 @@ export type JobList = { kind?: string; metadata?: ListMeta; }; +export type BitbucketRepositoryConfig = { + /** The branch to use in the repository. */ + branch: string; + /** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */ + encryptedToken?: string; + /** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash. + + When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */ + path?: string; + /** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */ + token?: string; + /** The repository URL (e.g. `https://bitbucket.org/example/test`). */ + url?: string; +}; export type GitRepositoryConfig = { /** The branch to use in the repository. */ branch: string; @@ -865,6 +895,20 @@ export type GitHubRepositoryConfig = { /** The repository URL (e.g. `https://github.com/example/test`). */ url?: string; }; +export type GitLabRepositoryConfig = { + /** The branch to use in the repository. */ + branch: string; + /** Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted. */ + encryptedToken?: string; + /** Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash. + + When specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found. */ + path?: string; + /** Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again. */ + token?: string; + /** The repository URL (e.g. `https://gitlab.com/example/test`). */ + url?: string; +}; export type LocalRepositoryConfig = { path?: string; }; @@ -881,12 +925,16 @@ export type SyncOptions = { target: 'folder' | 'instance'; }; export type RepositorySpec = { + /** The repository on Bitbucket. Mutually exclusive with local | github | git. */ + bitbucket?: BitbucketRepositoryConfig; /** Repository description */ description?: string; /** The repository on Git. Mutually exclusive with local | github | git. */ git?: GitRepositoryConfig; /** The repository on GitHub. Mutually exclusive with local | github | git. */ github?: GitHubRepositoryConfig; + /** The repository on GitLab. Mutually exclusive with local | github | git. */ + gitlab?: GitLabRepositoryConfig; /** The repository on the local file system. Mutually exclusive with local | github. */ local?: LocalRepositoryConfig; /** Sync settings -- how values are pulled from the repository into grafana */ @@ -896,10 +944,12 @@ export type RepositorySpec = { /** The repository type. When selected oneOf the values below should be non-nil Possible enum values: + - `"bitbucket"` - `"git"` - `"github"` + - `"gitlab"` - `"local"` */ - type: 'git' | 'github' | 'local'; + type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local'; /** UI driven Workflow that allow changes to the contends of the repository. The order is relevant for defining the precedence of the workflows. When empty, the repository does not support any edits (eg, readonly) */ workflows: ('branch' | 'write')[]; }; @@ -1031,10 +1081,12 @@ export type ResourceRepositoryInfo = { /** The repository type Possible enum values: + - `"bitbucket"` - `"git"` - `"github"` + - `"gitlab"` - `"local"` */ - type: 'git' | 'github' | 'local'; + type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local'; }; export type Unstructured = { [key: string]: any; @@ -1168,16 +1220,20 @@ export type RepositoryView = { /** The repository type Possible enum values: + - `"bitbucket"` - `"git"` - `"github"` + - `"gitlab"` - `"local"` */ - type: 'git' | 'github' | 'local'; + type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local'; /** The supported workflows */ workflows: ('branch' | 'write')[]; }; export type RepositoryViewList = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; + /** AvailableRepositoryTypes is the list of repository types supported in this instance (e.g. git, bitbucket, github, etc) */ + availableRepositoryTypes?: ('bitbucket' | 'git' | 'github' | 'gitlab' | 'local')[]; items: RepositoryView[]; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; @@ -1222,6 +1278,7 @@ export const { useGetRepositoryJobsQuery, useCreateRepositoryJobsMutation, useGetRepositoryJobsWithPathQuery, + useGetRepositoryRefsQuery, useGetRepositoryRenderWithPathQuery, useGetRepositoryResourcesQuery, useGetRepositoryStatusQuery, diff --git a/public/app/api/utils.ts b/public/app/api/utils.ts index 0815c34d288..1ad0ff690e5 100644 --- a/public/app/api/utils.ts +++ b/public/app/api/utils.ts @@ -1,8 +1,8 @@ import { config, isFetchError } from '@grafana/runtime'; +import { ThunkDispatch } from 'app/types/store'; import { notifyApp } from '../core/actions'; import { createErrorNotification } from '../core/copy/appNotification'; -import { ThunkDispatch } from '../types'; export const getAPINamespace = () => config.namespace; diff --git a/public/app/core/actions/cleanUp.ts b/public/app/core/actions/cleanUp.ts index 4322612bda0..854b10ec5c3 100644 --- a/public/app/core/actions/cleanUp.ts +++ b/public/app/core/actions/cleanUp.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { StoreState } from '../../types'; +import { StoreState } from 'app/types/store'; export type CleanUpAction = (state: StoreState) => void; diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index c7569ff09f6..f49988dcc1a 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,12 +1,12 @@ import { useEffect, useMemo, useState } from 'react'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, Select, Stack } from '@grafana/ui'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { ServiceAccountPicker } from 'app/core/components/Select/ServiceAccountPicker'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; import { UserPicker } from 'app/core/components/Select/UserPicker'; -import { OrgRole } from 'app/types/acl'; import { Assignments, PermissionTarget, SetPermission } from './types'; diff --git a/public/app/core/components/AccessControl/types.ts b/public/app/core/components/AccessControl/types.ts index c061bc77138..dcc69b58df4 100644 --- a/public/app/core/components/AccessControl/types.ts +++ b/public/app/core/components/AccessControl/types.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export type ResourcePermission = { id: number; diff --git a/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx b/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx index 9c1f0330ac7..16d651e72bd 100644 --- a/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx +++ b/public/app/core/components/AppChrome/AppChromeExtensionPoint.tsx @@ -2,7 +2,6 @@ import { useLocation } from 'react-router-dom'; import { PluginExtensionPoints } from '@grafana/data'; import { config, renderLimitedComponents, usePluginComponents } from '@grafana/runtime'; -import { ErrorBoundaryAlert } from '@grafana/ui'; const excludedRoutes: Record = { '/login': true, @@ -24,13 +23,10 @@ export function AppChromeExtensionPoint(): JSX.Element | null { return null; } - return ( - - - - ); + return ; } +// We have this "internal" component so we can prevent pre-loading the plugins associated with the extension-point if the feature is not enabled. function InternalAppChromeExtensionPoint(): JSX.Element | null { const { components, isLoading } = usePluginComponents({ extensionPointId: PluginExtensionPoints.AppChrome, diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 8c91066066b..fc6c062b782 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -7,7 +7,7 @@ import { config, locationService, reportInteraction } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import store from 'app/core/store'; import { isShallowEqual } from 'app/core/utils/isShallowEqual'; -import { KioskMode } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { RouteDescriptor } from '../../navigation/types'; import { buildBreadcrumbs } from '../Breadcrumbs/utils'; diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx new file mode 100644 index 00000000000..0582e5778fe --- /dev/null +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.test.tsx @@ -0,0 +1,167 @@ +import { render, screen } from '@testing-library/react'; + +import { ExtensionInfo, PluginExtensionTypes } from '@grafana/data'; +import { config, usePluginComponents } from '@grafana/runtime'; +import { AddedComponentRegistryItem } from 'app/features/plugins/extensions/registry/AddedComponentsRegistry'; +import { createComponentWithMeta } from 'app/features/plugins/extensions/usePluginComponents'; + +import { ExtensionSidebar } from './ExtensionSidebar'; +import { + ExtensionSidebarContextType, + getComponentIdFromComponentMeta, + useExtensionSidebarContext, +} from './ExtensionSidebarProvider'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + usePluginComponents: jest.fn(), +})); + +jest.mock('./ExtensionSidebarProvider', () => ({ + ...jest.requireActual('./ExtensionSidebarProvider'), + useExtensionSidebarContext: jest.fn(), +})); + +const mockUsePluginComponents = jest.mocked(usePluginComponents); +const mockUseExtensionSidebarContext = jest.mocked(useExtensionSidebarContext); + +const MockComponent = () =>
Mock Component
; +const pluginId = 'test-plugin'; +const extensionPointId = 'grafana/extension-sidebar/v0-alpha'; + +MockComponent.meta = { + pluginId, + title: 'Test Component', + id: 'test-component-id', + type: PluginExtensionTypes.component, + description: 'Test Component', +}; + +const addedComponentConfigMock: ExtensionInfo = { + targets: extensionPointId, + title: 'Test Component', +}; + +const extensionSidebarContextMock: ExtensionSidebarContextType = { + dockedComponentId: getComponentIdFromComponentMeta(pluginId, addedComponentConfigMock), + isEnabled: true, + props: {}, + isOpen: true, + setDockedComponentId: jest.fn(), + availableComponents: new Map(), + extensionSidebarWidth: 300, + setExtensionSidebarWidth: jest.fn(), +}; + +const addedComponentRegistryItemMock: AddedComponentRegistryItem = { + pluginId, + title: addedComponentConfigMock.title, + component: MockComponent, +}; + +describe('ExtensionSidebar', () => { + const originalEnv = config.buildInfo.env; + + beforeEach(() => { + jest.clearAllMocks(); + config.buildInfo.env = 'development'; + }); + + afterEach(() => { + config.buildInfo.env = originalEnv; + }); + + it('should render nothing when the extension sidebar is not enabled', () => { + mockUseExtensionSidebarContext.mockReturnValue({ + ...extensionSidebarContextMock, + isEnabled: false, + }); + + mockUsePluginComponents.mockReturnValue({ + components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)], + isLoading: false, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when the extension sidebar is enabled but no component is docked', () => { + mockUseExtensionSidebarContext.mockReturnValue({ + ...extensionSidebarContextMock, + isEnabled: true, + dockedComponentId: undefined, + }); + + mockUsePluginComponents.mockReturnValue({ + components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)], + isLoading: false, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when the extension sidebar is enabled but the component docked is not found in the available components', () => { + mockUseExtensionSidebarContext.mockReturnValue({ + ...extensionSidebarContextMock, + isEnabled: true, + dockedComponentId: 'test-component-id-not-found', + }); + + mockUsePluginComponents.mockReturnValue({ + components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)], + isLoading: false, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when the extension sidebar is enabled but the component docked is not found in the available components', () => { + mockUseExtensionSidebarContext.mockReturnValue({ + ...extensionSidebarContextMock, + isEnabled: true, + dockedComponentId: 'test-component-id-not-found', + }); + + mockUsePluginComponents.mockReturnValue({ + components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)], + isLoading: false, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when components are loading', () => { + mockUseExtensionSidebarContext.mockReturnValue({ + ...extensionSidebarContextMock, + isEnabled: true, + }); + + mockUsePluginComponents.mockReturnValue({ + components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)], + isLoading: true, + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should render the component when all conditions are met', () => { + mockUseExtensionSidebarContext.mockReturnValue({ + ...extensionSidebarContextMock, + isEnabled: true, + }); + + mockUsePluginComponents.mockReturnValue({ + components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)], + isLoading: false, + }); + + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + expect(screen.getByText('Mock Component')).toBeInTheDocument(); + }); +}); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx index 5f1e1b826ba..328ff2aa469 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx @@ -1,14 +1,10 @@ import { css } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, PluginExtensionPoints } from '@grafana/data'; import { usePluginComponents } from '@grafana/runtime'; -import { ErrorBoundaryAlert, useTheme2 } from '@grafana/ui'; +import { useTheme2 } from '@grafana/ui'; -import { - EXTENSION_SIDEBAR_EXTENSION_POINT_ID, - getComponentMetaFromComponentId, - useExtensionSidebarContext, -} from './ExtensionSidebarProvider'; +import { getComponentMetaFromComponentId, useExtensionSidebarContext } from './ExtensionSidebarProvider'; export const DEFAULT_EXTENSION_SIDEBAR_WIDTH = 300; export const MIN_EXTENSION_SIDEBAR_WIDTH = 100; @@ -22,7 +18,7 @@ export function ExtensionSidebar() { const styles = getStyles(useTheme2()); const { dockedComponentId, isEnabled, props = {} } = useExtensionSidebarContext(); const { components, isLoading } = usePluginComponents({ - extensionPointId: EXTENSION_SIDEBAR_EXTENSION_POINT_ID, + extensionPointId: PluginExtensionPoints.ExtensionSidebar, }); if (isLoading || !dockedComponentId || !isEnabled) { @@ -45,9 +41,7 @@ export function ExtensionSidebar() { return (
- - - +
); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx index b45be60bb2f..61651cabb84 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx @@ -3,7 +3,7 @@ import { render, screen, act } from '@testing-library/react'; import { store, EventBusSrv, EventBus } from '@grafana/data'; import { config, getAppEvents, setAppEvents, locationService } from '@grafana/runtime'; import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; -import { OpenExtensionSidebarEvent } from 'app/types/events'; +import { OpenExtensionSidebarEvent, CloseExtensionSidebarEvent } from 'app/types/events'; import { ExtensionSidebarContextProvider, @@ -249,7 +249,7 @@ describe('ExtensionSidebarProvider', () => { expect(screen.getByTestId('plugin-ids')).toHaveTextContent(permittedPluginMeta.pluginId); }); - it('should subscribe to OpenExtensionSidebarEvent when feature is enabled', async () => { + it('should subscribe to OpenExtensionSidebarEvent and CloseExtensionSidebarEvent when feature is enabled', async () => { render( @@ -257,9 +257,10 @@ describe('ExtensionSidebarProvider', () => { ); expect(subscribeSpy).toHaveBeenCalledWith(OpenExtensionSidebarEvent, expect.any(Function)); + expect(subscribeSpy).toHaveBeenCalledWith(CloseExtensionSidebarEvent, expect.any(Function)); }); - it('should not subscribe to OpenExtensionSidebarEvent when feature is disabled', () => { + it('should not subscribe to OpenExtensionSidebarEvent or CloseExtensionSidebarEvent when feature is disabled', () => { jest.replaceProperty(config.featureToggles, 'extensionSidebar', false); render( @@ -341,12 +342,58 @@ describe('ExtensionSidebarProvider', () => { expect(screen.getByTestId('is-open')).toHaveTextContent('false'); }); - it('should unsubscribe from OpenExtensionSidebarEvent on unmount', () => { - const unsubscribeMock = jest.fn(); - subscribeSpy.mockReturnValue({ - unsubscribe: unsubscribeMock, + it('should close sidebar when receiving a CloseExtensionSidebarEvent', () => { + const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent); + + const TestComponentWithProps = () => { + const context = useExtensionSidebarContext(); + return ( +
+
{context.isOpen.toString()}
+
{context.dockedComponentId || 'undefined'}
+ +
+ ); + }; + + render( + + + + ); + + // First open the sidebar manually + act(() => { + screen.getByText('Open Sidebar').click(); }); + expect(screen.getByTestId('is-open')).toHaveTextContent('true'); + expect(screen.getByTestId('docked-component-id')).toHaveTextContent(componentId); + + // Now test the close event + act(() => { + // Find the CloseExtensionSidebarEvent subscriber + const closeEventSubscriberCall = subscribeSpy.mock.calls.find((call) => call[0] === CloseExtensionSidebarEvent); + + expect(closeEventSubscriberCall).toBeDefined(); + const [, subscriberFn] = closeEventSubscriberCall!; + + // Call the close event handler + subscriberFn(new CloseExtensionSidebarEvent()); + }); + + expect(screen.getByTestId('is-open')).toHaveTextContent('false'); + expect(screen.getByTestId('docked-component-id')).toHaveTextContent('undefined'); + }); + + it('should unsubscribe from both OpenExtensionSidebarEvent and CloseExtensionSidebarEvent on unmount', () => { + const unsubscribeMocks = [jest.fn(), jest.fn()]; + let callIndex = 0; + + subscribeSpy.mockImplementation(() => ({ + unsubscribe: unsubscribeMocks[callIndex++], + })); + const { unmount } = render( @@ -354,7 +401,10 @@ describe('ExtensionSidebarProvider', () => { ); unmount(); - expect(unsubscribeMock).toHaveBeenCalled(); + + // Both event subscriptions should be unsubscribed + expect(unsubscribeMocks[0]).toHaveBeenCalled(); + expect(unsubscribeMocks[1]).toHaveBeenCalled(); }); it('should subscribe to location service observable', () => { diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx index 63b4812955e..84484eab290 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx @@ -1,14 +1,13 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useState, useMemo } from 'react'; import { useLocalStorage } from 'react-use'; -import { store, type ExtensionInfo } from '@grafana/data'; +import { PluginExtensionPoints, store, type ExtensionInfo } from '@grafana/data'; import { config, getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime'; import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; -import { OpenExtensionSidebarEvent } from 'app/types/events'; +import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent } from 'app/types/events'; import { DEFAULT_EXTENSION_SIDEBAR_WIDTH } from './ExtensionSidebar'; -export const EXTENSION_SIDEBAR_EXTENSION_POINT_ID = 'grafana/extension-sidebar/v0-alpha'; export const EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.extensionSidebarDocked'; export const EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY = 'grafana.navigation.extensionSidebarWidth'; const PERMITTED_EXTENSION_SIDEBAR_PLUGINS = [ @@ -18,7 +17,7 @@ const PERMITTED_EXTENSION_SIDEBAR_PLUGINS = [ 'grafana-grafanadocsplugin-app', ]; -type ExtensionSidebarContextType = { +export type ExtensionSidebarContextType = { /** * Whether the extension sidebar is enabled. */ @@ -94,7 +93,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo // `grafana/extension-sidebar/v0-alpha` and the link's `configure` method would control // whether the component is rendered or not const { links, isLoading } = usePluginLinks({ - extensionPointId: EXTENSION_SIDEBAR_EXTENSION_POINT_ID, + extensionPointId: PluginExtensionPoints.ExtensionSidebar, context: { path: currentPath, }, @@ -107,7 +106,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo () => isEnabled ? new Map( - Array.from(getExtensionPointPluginMeta(EXTENSION_SIDEBAR_EXTENSION_POINT_ID).entries()).filter( + Array.from(getExtensionPointPluginMeta(PluginExtensionPoints.ExtensionSidebar).entries()).filter( ([pluginId, pluginMeta]) => PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) && links.some( @@ -174,7 +173,10 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo if ( event.payload.pluginId && event.payload.componentTitle && - PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(event.payload.pluginId) + PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(event.payload.pluginId) && + availableComponents + .get(event.payload.pluginId) + ?.addedComponents.some((component) => component.title === event.payload.componentTitle) ) { setDockedComponentWithProps( JSON.stringify({ pluginId: event.payload.pluginId, componentTitle: event.payload.componentTitle }), @@ -183,11 +185,17 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo } }; - const subscription = getAppEvents().subscribe(OpenExtensionSidebarEvent, openSidebarHandler); - return () => { - subscription.unsubscribe(); + const closeSidebarHandler = () => { + setDockedComponentId(undefined); }; - }, [isEnabled, setDockedComponentWithProps]); + + const openSubscription = getAppEvents().subscribe(OpenExtensionSidebarEvent, openSidebarHandler); + const closeSubscription = getAppEvents().subscribe(CloseExtensionSidebarEvent, closeSidebarHandler); + return () => { + openSubscription.unsubscribe(); + closeSubscription.unsubscribe(); + }; + }, [isEnabled, setDockedComponentWithProps, availableComponents]); // update the stored docked component id when it changes useEffect(() => { diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx index 2b468d9f4c2..45b2c662cc7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx @@ -229,4 +229,41 @@ describe('ExtensionToolbarItem', () => { expect(screen.getByTestId('is-open')).toHaveTextContent('false'); }); + + it('should render individual buttons when multiple plugins are available', async () => { + const plugin1Meta = { + pluginId: 'grafana-investigations-app', + addedComponents: [{ ...mockComponent, title: 'Investigations' }], + }; + + const plugin2Meta = { + pluginId: 'grafana-assistant-app', + addedComponents: [{ ...mockComponent, title: 'Assistant' }], + }; + + (usePluginLinks as jest.Mock).mockReturnValue({ + links: [ + { pluginId: plugin1Meta.pluginId, title: plugin1Meta.addedComponents[0].title }, + { pluginId: plugin2Meta.pluginId, title: plugin2Meta.addedComponents[0].title }, + ], + isLoading: false, + }); + + (getExtensionPointPluginMeta as jest.Mock).mockReturnValue( + new Map([ + [plugin1Meta.pluginId, plugin1Meta], + [plugin2Meta.pluginId, plugin2Meta], + ]) + ); + + setup(); + + // Should render two separate buttons, not a dropdown + const buttons = screen.getAllByTestId(/extension-toolbar-button-open/); + expect(buttons).toHaveLength(2); + + // Each button should have the correct title + expect(buttons[0]).toHaveAttribute('aria-label', 'Open Investigations'); + expect(buttons[1]).toHaveAttribute('aria-label', 'Open Assistant'); + }); }); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index 02cad31ea91..50fe56630af 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -1,3 +1,4 @@ +import { ExtensionInfo } from '@grafana/data'; import { Dropdown, Menu } from '@grafana/ui'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; @@ -9,91 +10,75 @@ import { } from './ExtensionSidebarProvider'; import { ExtensionToolbarItemButton } from './ExtensionToolbarItemButton'; -export function ExtensionToolbarItem() { - const { availableComponents, dockedComponentId, setDockedComponentId, isOpen, isEnabled } = - useExtensionSidebarContext(); +type ComponentWithPluginId = ExtensionInfo & { pluginId: string }; - let dockedComponentTitle = ''; - let dockedPluginId = ''; - if (dockedComponentId) { - const dockedComponent = getComponentMetaFromComponentId(dockedComponentId); - if (dockedComponent) { - dockedComponentTitle = dockedComponent.componentTitle; - dockedPluginId = dockedComponent.pluginId; - } - } +export function ExtensionToolbarItem() { + const { availableComponents, dockedComponentId, setDockedComponentId, isEnabled } = useExtensionSidebarContext(); if (!isEnabled || availableComponents.size === 0) { return null; } - // get a flat list of all components with their pluginId - const components = Array.from(availableComponents.entries()).flatMap(([pluginId, { addedComponents }]) => - addedComponents.map((c) => ({ ...c, pluginId })) - ); + const dockedMeta = dockedComponentId ? getComponentMetaFromComponentId(dockedComponentId) : null; - if (components.length === 0) { - return null; - } + const renderPluginButton = (pluginId: string, components: ComponentWithPluginId[]) => { + if (components.length === 1) { + const component = components[0]; + const componentId = getComponentIdFromComponentMeta(pluginId, component); + const isActive = dockedComponentId === componentId; - if (components.length === 1) { - return ( - <> + return ( { - if (isOpen) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); - } - }} - pluginId={components[0].pluginId} + key={pluginId} + isOpen={isActive} + title={component.title} + onClick={() => setDockedComponentId(isActive ? undefined : componentId)} + pluginId={pluginId} /> - - + ); + } + + const isPluginActive = dockedMeta?.pluginId === pluginId; + const MenuItems = ( + + {components.map((c) => { + const id = getComponentIdFromComponentMeta(pluginId, c); + return ( + setDockedComponentId(dockedComponentId === id ? undefined : id)} + /> + ); + })} + ); - } + + return isPluginActive ? ( + setDockedComponentId(undefined)} + pluginId={pluginId} + /> + ) : ( + + + + ); + }; - const MenuItems = ( - - {components.map((c) => { - const id = getComponentIdFromComponentMeta(c.pluginId, c); - return ( - { - if (isOpen && dockedComponentId === id) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(id); - } - }} - /> - ); - })} - - ); return ( <> - {isOpen ? ( - { - if (isOpen) { - setDockedComponentId(undefined); - } - }} - pluginId={dockedPluginId} - /> - ) : ( - - - + {/* renders a single `ExtensionToolbarItemButton` for each plugin; if a plugin has multiple components, it renders them inside a `Dropdown` */} + {Array.from(availableComponents.entries()).map( + ([pluginId, { addedComponents }]: [string, { addedComponents: ExtensionInfo[] }]) => + renderPluginButton( + pluginId, + addedComponents.map((c: ExtensionInfo) => ({ ...c, pluginId })) + ) )} diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx index 44e33526095..3dcc53730a7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItemButton.tsx @@ -16,6 +16,8 @@ function getPluginIcon(pluginId?: string): string { switch (pluginId) { case 'grafana-grafanadocsplugin-app': return 'book'; + case 'grafana-investigations-app': + return 'eye'; default: return 'ai-sparkle'; } diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 9f85383b4d8..1c8b3959230 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -11,7 +11,7 @@ import { ScrollContainer, useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { setBookmark } from 'app/core/reducers/navBarTree'; import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { InviteUserButton } from '../../InviteUserButton/InviteUserButton'; import { shouldRenderInviteUserButton } from '../../InviteUserButton/utils'; diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx index 2a33cf03123..e2e1d35a962 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx @@ -5,7 +5,7 @@ import { SelectableValue, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Icon, Select, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { UserOrg } from 'app/types'; +import { UserOrg } from 'app/types/user'; import { OrganizationBaseProps } from './types'; diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx index d7897786e3e..79b9ab19f02 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.test.tsx @@ -4,7 +4,7 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { OrgRole } from '@grafana/data'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; import { getUserOrganizations } from 'app/features/org/state/actions'; -import * as appTypes from 'app/types'; +import { StoreState } from 'app/types/store'; import { OrganizationSwitcher } from './OrganizationSwitcher'; @@ -14,12 +14,12 @@ jest.mock('app/features/org/state/actions', () => ({ setUserOrganization: jest.fn(), })); -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), })); -const renderWithProvider = ({ initialState }: { initialState?: Partial }) => { +const renderWithProvider = ({ initialState }: { initialState?: Partial }) => { render( diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx index 4138e69733b..67ad47cded0 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -5,7 +5,8 @@ import { locationService } from '@grafana/runtime'; import { Space, Text } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { getUserOrganizations, setUserOrganization } from 'app/features/org/state/actions'; -import { useDispatch, useSelector, UserOrg } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; import { Branding } from '../../Branding/Branding'; diff --git a/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts b/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts index 3b2afef009b..0687bf2ca6c 100644 --- a/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts +++ b/public/app/core/components/AppChrome/OrganizationSwitcher/types.ts @@ -1,5 +1,5 @@ import { SelectableValue } from '@grafana/data'; -import { UserOrg } from 'app/types'; +import { UserOrg } from 'app/types/user'; export interface OrganizationBaseProps { orgs: UserOrg[]; diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx index a528a0c8341..0a9a48ac6ed 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx @@ -4,7 +4,7 @@ import { t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { Menu, Dropdown, ToolbarButton } from '@grafana/ui'; import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { performInviteUserClick, shouldRenderInviteUserButton } from '../../InviteUserButton/utils'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 0f26fdeeba1..6766d757b49 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -13,7 +13,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext'; import { contextSrv } from 'app/core/core'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { Branding } from '../../Branding/Branding'; import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs'; diff --git a/public/app/core/components/AppNotifications/AppNotificationItem.tsx b/public/app/core/components/AppNotifications/AppNotificationItem.tsx index 2d3ad8ea0f7..13575340b6a 100644 --- a/public/app/core/components/AppNotifications/AppNotificationItem.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationItem.tsx @@ -4,7 +4,7 @@ import { useEffectOnce } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Alert, useStyles2 } from '@grafana/ui'; -import { AppNotification, timeoutMap } from 'app/types'; +import { AppNotification, timeoutMap } from 'app/types/appNotifications'; interface Props { appNotification: AppNotification; diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index edb686f0d55..bb473dd0905 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -6,7 +6,7 @@ import { useStyles2, Stack } from '@grafana/ui'; import { notifyApp, hideAppNotification } from 'app/core/actions'; import appEvents from 'app/core/app_events'; import { selectVisible } from 'app/core/reducers/appNotification'; -import { useSelector, useDispatch } from 'app/types'; +import { useSelector, useDispatch } from 'app/types/store'; import { createErrorNotification, diff --git a/public/app/core/components/Branding/CloudBadge.tsx b/public/app/core/components/Branding/CloudBadge.tsx new file mode 100644 index 00000000000..e98ac79b913 --- /dev/null +++ b/public/app/core/components/Branding/CloudBadge.tsx @@ -0,0 +1,7 @@ +import { t } from '@grafana/i18n'; + +import { OrangeBadge } from './OrangeBadge'; + +export function CloudBadge() { + return ; +} diff --git a/public/app/core/components/Branding/CloudEnterpriseBadge.tsx b/public/app/core/components/Branding/CloudEnterpriseBadge.tsx index a904417053c..ce95d147581 100644 --- a/public/app/core/components/Branding/CloudEnterpriseBadge.tsx +++ b/public/app/core/components/Branding/CloudEnterpriseBadge.tsx @@ -1,32 +1,7 @@ -import { css } from '@emotion/css'; +import { t } from '@grafana/i18n'; -import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; -import { Icon, useStyles2 } from '@grafana/ui'; +import { OrangeBadge } from './OrangeBadge'; export function CloudEnterpriseBadge() { - const styles = useStyles2(getStyles); - return ( -
- - Cloud & Enterprise -
- ); + return ; } - -const getStyles = (theme: GrafanaTheme2) => { - return { - wrapper: css({ - display: 'inline-flex', - padding: theme.spacing(0.5, 1), - borderRadius: theme.shape.radius.pill, - background: theme.colors.gradients.brandHorizontal, - color: theme.colors.primary.contrastText, - fontWeight: theme.typography.fontWeightMedium, - gap: theme.spacing(0.5), - fontSize: theme.typography.bodySmall.fontSize, - lineHeight: theme.typography.bodySmall.lineHeight, - alignItems: 'center', - }), - }; -}; diff --git a/public/app/core/components/Branding/OrangeBadge.tsx b/public/app/core/components/Branding/OrangeBadge.tsx new file mode 100644 index 00000000000..be9d080df79 --- /dev/null +++ b/public/app/core/components/Branding/OrangeBadge.tsx @@ -0,0 +1,31 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Icon, useStyles2 } from '@grafana/ui'; + +export function OrangeBadge({ text }: { text: string }) { + const styles = useStyles2(getStyles); + return ( +
+ + {text} +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + wrapper: css({ + display: 'inline-flex', + padding: theme.spacing(0.5, 1), + borderRadius: theme.shape.radius.pill, + background: theme.colors.gradients.brandHorizontal, + color: theme.colors.primary.contrastText, + fontWeight: theme.typography.fontWeightMedium, + gap: theme.spacing(0.5), + fontSize: theme.typography.bodySmall.fontSize, + lineHeight: theme.typography.bodySmall.lineHeight, + alignItems: 'center', + }), + }; +}; diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index cbd4e574235..2d310a6609b 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -9,7 +9,8 @@ import { config } from 'app/core/config'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { FolderInfo, PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; +import { FolderInfo } from 'app/types/folders'; export interface FolderFilterProps { onChange: (folder: FolderInfo[]) => void; @@ -46,7 +47,7 @@ export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JS loadOptions={debouncedLoadOptions} maxMenuHeight={maxMenuHeight} placeholder={t('folder-filter.select-placeholder', 'Filter by folder')} - noOptionsMessage="No folders found" + noOptionsMessage={t('folder-filter.noOptionsMessage-no-folders-found', 'No folders found')} prefix={} aria-label={t('folder-filter.select-aria-label', 'Folder filter')} defaultOptions diff --git a/public/app/core/components/InviteUserButton/utils.ts b/public/app/core/components/InviteUserButton/utils.ts index 708e5ff88d5..6fb774a059b 100644 --- a/public/app/core/components/InviteUserButton/utils.ts +++ b/public/app/core/components/InviteUserButton/utils.ts @@ -2,7 +2,7 @@ import { reportInteraction } from '@grafana/runtime'; import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export const shouldRenderInviteUserButton = config.featureToggles.inviteUserExperimental && diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx index 02f2f108e30..e149f84c75d 100644 --- a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx @@ -2,7 +2,7 @@ import { t } from '@grafana/i18n'; import { Badge } from '@grafana/ui'; import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { NestedFolderDTO } from 'app/features/search/service/types'; -import { FolderDTO, FolderListItemDTO } from 'app/types'; +import { FolderDTO, FolderListItemDTO } from 'app/types/folders'; export interface Props { folder?: FolderListItemDTO | NestedFolderDTO | FolderDTO; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index 910bc761858..46ef137c53f 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -9,10 +9,10 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { IconButton, useStyles2, Text } from '@grafana/ui'; import { Indent } from 'app/core/components/Indent/Indent'; -import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state'; +import { childrenByParentUIDSelector, rootItemsSelector } from 'app/features/browse-dashboards/state/hooks'; import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { DashboardViewItem } from 'app/features/search/types'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { FolderRepo } from './FolderRepo'; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 28305640a2a..38d241c61e2 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -15,7 +15,7 @@ import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { QueryResponse } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; -import { PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; import { FolderRepo } from './FolderRepo'; import { getDOMId, NestedFolderList } from './NestedFolderList'; diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts index bd3b6ba9098..6fe4c0ef440 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts @@ -1,6 +1,5 @@ import { config } from '@grafana/runtime'; - -import { PermissionLevelString } from '../../../types'; +import { PermissionLevelString } from 'app/types/acl'; import { useFoldersQueryAppPlatform } from './useFoldersQueryAppPlatform'; import { useFoldersQueryLegacy } from './useFoldersQueryLegacy'; diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts index 6b1286895bd..8381c61d1f2 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryLegacy.ts @@ -7,7 +7,8 @@ import { ListFolderQueryArgs, browseDashboardsAPI } from 'app/features/browse-da import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services'; import { getPaginationPlaceholders } from 'app/features/browse-dashboards/state/utils'; import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; -import { FolderListItemDTO, PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; +import { FolderListItemDTO } from 'app/types/folders'; import { useDispatch, useSelector } from 'app/types/store'; import { ROOT_FOLDER_ITEM } from './utils'; diff --git a/public/app/core/components/Page/usePageNav.ts b/public/app/core/components/Page/usePageNav.ts index da6bd0da218..66899e442da 100644 --- a/public/app/core/components/Page/usePageNav.ts +++ b/public/app/core/components/Page/usePageNav.ts @@ -3,7 +3,7 @@ import { createSelector } from 'reselect'; import { NavModel } from '@grafana/data'; import { getNavModel } from 'app/core/selectors/navModel'; import { store } from 'app/store/store'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; export function usePageNav(navId?: string, oldProp?: NavModel): NavModel | undefined { if (oldProp) { diff --git a/public/app/core/components/Page/usePageTitle.ts b/public/app/core/components/Page/usePageTitle.ts index c2e53b23251..4c338a77ef0 100644 --- a/public/app/core/components/Page/usePageTitle.ts +++ b/public/app/core/components/Page/usePageTitle.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { NavModel, NavModelItem } from '@grafana/data'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { Branding } from '../Branding/Branding'; import { buildBreadcrumbs } from '../Breadcrumbs/utils'; diff --git a/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx b/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx index c0fa457d8c2..70e18b2a9c8 100644 --- a/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx +++ b/public/app/core/components/RolePicker/BuiltinRoleSelector.tsx @@ -1,8 +1,7 @@ -import { SelectableValue } from '@grafana/data'; +import { OrgRole, SelectableValue } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Icon, RadioButtonList, Tooltip, useStyles2, useTheme2, PopoverContent } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { OrgRole } from 'app/types'; import { getStyles } from './styles'; diff --git a/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx b/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx index 79e44be57e6..b4b3e30d738 100644 --- a/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx +++ b/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx @@ -1,7 +1,7 @@ import { forwardRef, useCallback, useState } from 'react'; import { useStyles2, getSelectStyles, useTheme2 } from '@grafana/ui'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { RoleMenuGroupOption } from './RoleMenuGroupOption'; import { RoleMenuOption } from './RoleMenuOption'; diff --git a/public/app/core/components/RolePicker/RoleMenuOption.tsx b/public/app/core/components/RolePicker/RoleMenuOption.tsx index 7700b00bdae..6e020ad6018 100644 --- a/public/app/core/components/RolePicker/RoleMenuOption.tsx +++ b/public/app/core/components/RolePicker/RoleMenuOption.tsx @@ -4,7 +4,7 @@ import { forwardRef, FormEvent } from 'react'; import { t } from '@grafana/i18n'; import { Checkbox, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { getStyles } from './styles'; diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index 6cbf2e0450d..25cff0889a1 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,7 +1,8 @@ import { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; +import { OrgRole } from '@grafana/data'; import { ClickOutsideWrapper, Portal, useTheme2 } from '@grafana/ui'; -import { Role, OrgRole } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { RolePickerInput } from './RolePickerInput'; import { RolePickerMenu } from './RolePickerMenu'; diff --git a/public/app/core/components/RolePicker/RolePickerInput.tsx b/public/app/core/components/RolePicker/RolePickerInput.tsx index 9acb2630745..9aaa4b447f5 100644 --- a/public/app/core/components/RolePicker/RolePickerInput.tsx +++ b/public/app/core/components/RolePicker/RolePickerInput.tsx @@ -6,8 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { useStyles2, getInputStyles, sharedInputStyle, Tooltip, Icon, Spinner } from '@grafana/ui'; import { getFocusStyles } from '@grafana/ui/internal'; - -import { Role } from '../../../types'; +import { Role } from 'app/types/accessControl'; import { ValueContainer } from './ValueContainer'; import { ROLE_PICKER_WIDTH } from './constants'; diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 23cf52de471..81c38519ae6 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -1,10 +1,11 @@ import { css, cx } from '@emotion/css'; import { useEffect, useRef, useState } from 'react'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, ScrollContainer, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; -import { OrgRole, Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { BuiltinRoleSelector } from './BuiltinRoleSelector'; import { RoleMenuGroupsSection } from './RoleMenuGroupsSection'; diff --git a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx index fdcd22224b9..3dff3fc88be 100644 --- a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx @@ -3,7 +3,7 @@ import { cx } from '@emotion/css'; import { Trans, t } from '@grafana/i18n'; import { Button, ScrollContainer, Stack, useStyles2, useTheme2 } from '@grafana/ui'; import { getSelectStyles } from '@grafana/ui/internal'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { RoleMenuOption } from './RoleMenuOption'; import { MENU_MAX_HEIGHT } from './constants'; diff --git a/public/app/core/components/RolePicker/TeamRolePicker.tsx b/public/app/core/components/RolePicker/TeamRolePicker.tsx index 83552236334..78e3b0f2c82 100644 --- a/public/app/core/components/RolePicker/TeamRolePicker.tsx +++ b/public/app/core/components/RolePicker/TeamRolePicker.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useAsyncFn } from 'react-use'; import { contextSrv } from 'app/core/core'; -import { Role, AccessControlAction } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; import { RolePicker } from './RolePicker'; import { fetchTeamRoles, updateTeamRoles } from './api'; diff --git a/public/app/core/components/RolePicker/UserRolePicker.tsx b/public/app/core/components/RolePicker/UserRolePicker.tsx index 5fa3c94286f..a4232f4b61a 100644 --- a/public/app/core/components/RolePicker/UserRolePicker.tsx +++ b/public/app/core/components/RolePicker/UserRolePicker.tsx @@ -1,8 +1,9 @@ import { useEffect } from 'react'; import { useAsyncFn } from 'react-use'; +import { OrgRole } from '@grafana/data'; import { contextSrv } from 'app/core/core'; -import { Role, OrgRole, AccessControlAction } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; import { RolePicker } from './RolePicker'; import { fetchUserRoles, updateUserRoles } from './api'; diff --git a/public/app/core/components/RolePicker/api.ts b/public/app/core/components/RolePicker/api.ts index 00e1926eea5..596463b4a9d 100644 --- a/public/app/core/components/RolePicker/api.ts +++ b/public/app/core/components/RolePicker/api.ts @@ -1,5 +1,5 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; import { addDisplayNameForFixedRole, addFilteredDisplayName } from './utils'; diff --git a/public/app/core/components/RolePicker/hooks.ts b/public/app/core/components/RolePicker/hooks.ts index 071f31eddaa..0b66a36c94f 100644 --- a/public/app/core/components/RolePicker/hooks.ts +++ b/public/app/core/components/RolePicker/hooks.ts @@ -4,7 +4,7 @@ import { useDeepCompareEffect } from 'react-use'; import useAsync from 'react-use/lib/useAsync'; import { contextSrv } from 'app/core/core'; -import { Role, AccessControlAction } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; import { fetchRoleOptions } from './api'; diff --git a/public/app/core/components/RolePicker/utils.ts b/public/app/core/components/RolePicker/utils.ts index ed1021f2d8c..413671f0090 100644 --- a/public/app/core/components/RolePicker/utils.ts +++ b/public/app/core/components/RolePicker/utils.ts @@ -1,4 +1,4 @@ -import { Role } from 'app/types'; +import { Role } from 'app/types/accessControl'; export const isNotDelegatable = (role: Role) => { return role.delegatable !== undefined && !role.delegatable; diff --git a/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx b/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx index 57789ebb518..522f752b9a6 100644 --- a/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx +++ b/public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx @@ -5,7 +5,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, Badge, Stack } from '@grafana/ui'; -import { OrgUser } from 'app/types'; +import { OrgUser } from 'app/types/user'; import { RolePickerDrawer } from './RolePickerDrawer'; diff --git a/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx b/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx index 9b327449519..75afc684eab 100644 --- a/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx +++ b/public/app/core/components/RolePickerDrawer/RolePickerDrawer.tsx @@ -1,9 +1,8 @@ import { Controller, useFormContext } from 'react-hook-form'; -import { toOption } from '@grafana/data'; +import { OrgRole, toOption } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Drawer, Field, RadioButtonGroup, TextLink } from '@grafana/ui'; -import { OrgRole } from 'app/types'; const roleOptions = Object.keys(OrgRole).map(toOption); diff --git a/public/app/core/components/Select/DashboardPicker.test.tsx b/public/app/core/components/Select/DashboardPicker.test.tsx index 2f662fe586b..b46c1f9d2df 100644 --- a/public/app/core/components/Select/DashboardPicker.test.tsx +++ b/public/app/core/components/Select/DashboardPicker.test.tsx @@ -10,7 +10,7 @@ import { import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { DashboardPicker } from './DashboardPicker'; diff --git a/public/app/core/components/Select/DashboardPicker.tsx b/public/app/core/components/Select/DashboardPicker.tsx index dfbb2458ade..bfb965a73bd 100644 --- a/public/app/core/components/Select/DashboardPicker.tsx +++ b/public/app/core/components/Select/DashboardPicker.tsx @@ -8,7 +8,7 @@ import { AnnoKeyFolder, AnnoKeyFolderTitle } from 'app/features/apiserver/types' import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { isDashboardV2Resource } from 'app/features/dashboard/api/utils'; import { DashboardSearchItem } from 'app/features/search/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; interface Props extends Omit, 'value' | 'onChange' | 'loadOptions' | ''> { value?: DashboardPickerDTO['uid']; diff --git a/public/app/core/components/Select/MetricSelect.tsx b/public/app/core/components/Select/MetricSelect.tsx index 771cf1aa6d2..d1655efb3f9 100644 --- a/public/app/core/components/Select/MetricSelect.tsx +++ b/public/app/core/components/Select/MetricSelect.tsx @@ -2,6 +2,7 @@ import { flatten } from 'lodash'; import { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; import { Variable } from 'app/types/templates'; @@ -32,7 +33,7 @@ export const MetricSelect = (props: Props) => { isSearchable={isSearchable} maxMenuHeight={500} placeholder={placeholder} - noOptionsMessage="No options found" + noOptionsMessage={t('metric-select.noOptionsMessage-no-options-found', 'No options found')} value={selected} /> ); diff --git a/public/app/core/components/Select/OldFolderPicker.tsx b/public/app/core/components/Select/OldFolderPicker.tsx index e5e684b7713..13ddca85986 100644 --- a/public/app/core/components/Select/OldFolderPicker.tsx +++ b/public/app/core/components/Select/OldFolderPicker.tsx @@ -13,7 +13,8 @@ import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/services/context_srv'; import { createFolder, getFolderByUid, searchFolders } from 'app/features/manage-dashboards/state/actions'; import { DashboardSearchHit } from 'app/features/search/types'; -import { AccessControlAction, PermissionLevelString, SearchQueryType } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { PermissionLevelString, SearchQueryType } from 'app/types/acl'; export type FolderPickerFilter = (hits: DashboardSearchHit[]) => DashboardSearchHit[]; diff --git a/public/app/core/components/Select/OrgPicker.tsx b/public/app/core/components/Select/OrgPicker.tsx index af7759901b5..81ed7132d02 100644 --- a/public/app/core/components/Select/OrgPicker.tsx +++ b/public/app/core/components/Select/OrgPicker.tsx @@ -5,7 +5,8 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { Organization, UserOrg } from 'app/types'; +import { Organization } from 'app/types/organization'; +import { UserOrg } from 'app/types/user'; export type OrgSelectItem = SelectableValue; @@ -67,7 +68,7 @@ export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOr }} value={selected} placeholder={t('org-picker.select-placeholder', 'Select organization')} - noOptionsMessage="No organizations found" + noOptionsMessage={t('org-picker.noOptionsMessage-no-organizations-found', 'No organizations found')} /> ); } diff --git a/public/app/core/components/Select/ServiceAccountPicker.tsx b/public/app/core/components/Select/ServiceAccountPicker.tsx index 09f045c544f..037404dcb2b 100644 --- a/public/app/core/components/Select/ServiceAccountPicker.tsx +++ b/public/app/core/components/Select/ServiceAccountPicker.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { ServiceAccountDTO, ServiceAccountsState } from 'app/types'; +import { ServiceAccountDTO, ServiceAccountsState } from 'app/types/serviceaccount'; export interface Props { onSelected: (user: SelectableValue) => void; @@ -67,7 +67,10 @@ export class ServiceAccountPicker extends Component { loadOptions={this.search} onChange={onSelected} placeholder={t('service-account-picker.select-placeholder', 'Start typing to search for service accounts')} - noOptionsMessage="No service accounts found" + noOptionsMessage={t( + 'service-account-picker.noOptionsMessage-no-service-accounts-found', + 'No service accounts found' + )} aria-label={t('service-account-picker.select-aria-label', 'Service account picker')} />
diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx index d189e46877c..3f374fc8930 100644 --- a/public/app/core/components/Select/TeamPicker.tsx +++ b/public/app/core/components/Select/TeamPicker.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { Team } from 'app/types'; +import { Team } from 'app/types/teams'; export interface Props { onSelected: (team: SelectableValue) => void; @@ -84,7 +84,7 @@ export class TeamPicker extends Component { onChange={onSelected} className={className} placeholder={t('team-picker.select-placeholder', 'Select a team')} - noOptionsMessage="No teams found" + noOptionsMessage={t('team-picker.noOptionsMessage-no-teams-found', 'No teams found')} aria-label={t('team-picker.select-aria-label', 'Team picker')} />
diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 91158b6901f..5284cbeca5f 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { AsyncSelect } from '@grafana/ui'; -import { OrgUser } from 'app/types'; +import { OrgUser } from 'app/types/user'; export interface Props { onSelected: (user: SelectableValue) => void; @@ -67,7 +67,7 @@ export class UserPicker extends Component { loadOptions={this.search} onChange={onSelected} placeholder={t('user-picker.select-placeholder', 'Start typing to search for user')} - noOptionsMessage="No users found" + noOptionsMessage={t('user-picker.noOptionsMessage-no-users-found', 'No users found')} aria-label={t('user-picker.select-aria-label', 'User picker')} /> diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index d71e3246dab..407bd92ed66 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -394,13 +394,13 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ else { // scan right let leftIdx = 0; - while (yData[leftIdx] == null) { + while (yData[leftIdx] === null) { leftIdx++; } // scan left let rightIdx = yData.length - 1; - while (rightIdx >= leftIdx && yData[rightIdx] == null) { + while (rightIdx >= leftIdx && yData[rightIdx] === null) { rightIdx--; } diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts index 9f2fff8641f..3f1dd8f484f 100644 --- a/public/app/core/copy/appNotification.ts +++ b/public/app/core/copy/appNotification.ts @@ -3,7 +3,8 @@ import { v4 as uuidv4 } from 'uuid'; import { getMessageFromError } from 'app/core/utils/errors'; import { dispatch as storeDispatch } from 'app/store/store'; -import { AppNotification, AppNotificationSeverity, useDispatch } from 'app/types'; +import { AppNotificationSeverity, AppNotification } from 'app/types/appNotifications'; +import { useDispatch } from 'app/types/store'; import { notifyApp } from '../actions'; diff --git a/public/app/core/history/RichHistoryLocalStorage.test.ts b/public/app/core/history/RichHistoryLocalStorage.test.ts index 8899a7cc509..8cf739514e2 100644 --- a/public/app/core/history/RichHistoryLocalStorage.test.ts +++ b/public/app/core/history/RichHistoryLocalStorage.test.ts @@ -1,8 +1,8 @@ import { DataQuery } from '@grafana/data'; import store from 'app/core/store'; +import { RichHistoryQuery } from 'app/types/explore'; import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; diff --git a/public/app/core/history/RichHistoryLocalStorage.ts b/public/app/core/history/RichHistoryLocalStorage.ts index 4396b73a1fa..165c2f06eac 100644 --- a/public/app/core/history/RichHistoryLocalStorage.ts +++ b/public/app/core/history/RichHistoryLocalStorage.ts @@ -2,8 +2,8 @@ import { find, isEqual, omit } from 'lodash'; import { DataQuery, SelectableValue } from '@grafana/data'; import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import store from '../store'; import RichHistoryStorage, { RichHistoryServiceError, RichHistoryStorageWarning } from './RichHistoryStorage'; diff --git a/public/app/core/history/RichHistoryRemoteStorage.test.ts b/public/app/core/history/RichHistoryRemoteStorage.test.ts index 766712d1c0a..b99967b09b7 100644 --- a/public/app/core/history/RichHistoryRemoteStorage.test.ts +++ b/public/app/core/history/RichHistoryRemoteStorage.test.ts @@ -1,9 +1,9 @@ import { of } from 'rxjs'; import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen'; +import { RichHistoryQuery } from 'app/types/explore'; import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; import { SortOrder } from '../utils/richHistoryTypes'; import RichHistoryRemoteStorage, { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; @@ -45,7 +45,7 @@ jest.mock('../services/PreferencesService', () => ({ })); // FIXME: Tests break unless plugin loader is mocked. This is likely due to a circular dependency -jest.mock('app/features/plugins/plugin_loader', () => ({})); +jest.mock('app/features/plugins/pluginLoader', () => ({})); describe('RichHistoryRemoteStorage', () => { let storage: RichHistoryRemoteStorage; diff --git a/public/app/core/history/RichHistoryStorage.ts b/public/app/core/history/RichHistoryStorage.ts index 13d89a05172..158a5e7d6cb 100644 --- a/public/app/core/history/RichHistoryStorage.ts +++ b/public/app/core/history/RichHistoryStorage.ts @@ -1,6 +1,5 @@ import { RichHistorySearchBackendFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; /** * Errors are used when the operation on Rich History was not successful. diff --git a/public/app/core/history/localStorageConverter.test.ts b/public/app/core/history/localStorageConverter.test.ts index 20b8701a258..ffefb2ce8d4 100644 --- a/public/app/core/history/localStorageConverter.test.ts +++ b/public/app/core/history/localStorageConverter.test.ts @@ -1,6 +1,6 @@ import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; diff --git a/public/app/core/history/localStorageConverter.ts b/public/app/core/history/localStorageConverter.ts index 62be110bcdd..485ebf3a1a3 100644 --- a/public/app/core/history/localStorageConverter.ts +++ b/public/app/core/history/localStorageConverter.ts @@ -1,6 +1,5 @@ import { getDataSourceSrv } from '@grafana/runtime'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; import { RichHistoryLocalStorageDTO } from './RichHistoryLocalStorage'; diff --git a/public/app/core/history/remoteStorageConverter.test.ts b/public/app/core/history/remoteStorageConverter.test.ts index 6e42cc507ac..dd8f927ec66 100644 --- a/public/app/core/history/remoteStorageConverter.test.ts +++ b/public/app/core/history/remoteStorageConverter.test.ts @@ -1,5 +1,6 @@ +import { RichHistoryQuery } from 'app/types/explore'; + import { DatasourceSrv } from '../../features/plugins/datasource_srv'; -import { RichHistoryQuery } from '../../types'; import { backendSrv } from '../services/backend_srv'; import { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; diff --git a/public/app/core/history/remoteStorageConverter.ts b/public/app/core/history/remoteStorageConverter.ts index 6a743016e64..a4d15a677c3 100644 --- a/public/app/core/history/remoteStorageConverter.ts +++ b/public/app/core/history/remoteStorageConverter.ts @@ -1,6 +1,5 @@ import { getDataSourceSrv } from '@grafana/runtime'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; import { RichHistoryRemoteStorageDTO } from './RichHistoryRemoteStorage'; diff --git a/public/app/core/history/richHistoryLocalStorageUtils.test.ts b/public/app/core/history/richHistoryLocalStorageUtils.test.ts index 7687f3f1bb5..29a4b7d79d2 100644 --- a/public/app/core/history/richHistoryLocalStorageUtils.test.ts +++ b/public/app/core/history/richHistoryLocalStorageUtils.test.ts @@ -1,7 +1,6 @@ import { DataQuery } from '@grafana/data'; import { SortOrder } from 'app/core/utils/richHistory'; - -import { RichHistoryQuery } from '../../types'; +import { RichHistoryQuery } from 'app/types/explore'; import { filterAndSortQueries } from './richHistoryLocalStorageUtils'; diff --git a/public/app/core/history/richHistoryLocalStorageUtils.ts b/public/app/core/history/richHistoryLocalStorageUtils.ts index d22b4816781..caefdd7f45d 100644 --- a/public/app/core/history/richHistoryLocalStorageUtils.ts +++ b/public/app/core/history/richHistoryLocalStorageUtils.ts @@ -1,8 +1,8 @@ import { omit } from 'lodash'; import { DateTime, dateTime, dateTimeForTimeZone } from '@grafana/data'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import { SortOrder } from '../utils/richHistoryTypes'; /** diff --git a/public/app/core/hooks/useCleanup.ts b/public/app/core/hooks/useCleanup.ts index 86696b829d2..6cec29424d1 100644 --- a/public/app/core/hooks/useCleanup.ts +++ b/public/app/core/hooks/useCleanup.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { cleanUpAction, CleanUpAction } from '../actions/cleanUp'; diff --git a/public/app/core/hooks/useNavModel.ts b/public/app/core/hooks/useNavModel.ts index 047654109cc..a872f1a59f1 100644 --- a/public/app/core/hooks/useNavModel.ts +++ b/public/app/core/hooks/useNavModel.ts @@ -1,5 +1,5 @@ import { NavModel } from '@grafana/data'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getNavModel } from '../selectors/navModel'; diff --git a/public/app/core/navigation/kiosk.ts b/public/app/core/navigation/kiosk.ts index 36c66c7eeeb..0e7ddfb3f3c 100644 --- a/public/app/core/navigation/kiosk.ts +++ b/public/app/core/navigation/kiosk.ts @@ -1,6 +1,5 @@ import { UrlQueryMap } from '@grafana/data'; - -import { KioskMode } from '../../types'; +import { KioskMode } from 'app/types/dashboard'; // TODO Remove after topnav feature toggle is permanent and old NavBar is removed export function getKioskMode(queryParams: UrlQueryMap): KioskMode | null { diff --git a/public/app/core/reducers/appNotification.test.ts b/public/app/core/reducers/appNotification.test.ts index 6094e1ac7bc..f9bb79baab0 100644 --- a/public/app/core/reducers/appNotification.test.ts +++ b/public/app/core/reducers/appNotification.test.ts @@ -1,4 +1,4 @@ -import { AppNotificationSeverity, AppNotificationsState } from 'app/types/'; +import { AppNotificationSeverity, AppNotificationsState } from 'app/types/appNotifications'; import { appNotificationsReducer, clearNotification, notifyApp } from './appNotification'; diff --git a/public/app/core/reducers/appNotification.ts b/public/app/core/reducers/appNotification.ts index d9f563d364e..aedb2d0a0fc 100644 --- a/public/app/core/reducers/appNotification.ts +++ b/public/app/core/reducers/appNotification.ts @@ -1,6 +1,6 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { AppNotification, AppNotificationSeverity, AppNotificationsState } from 'app/types/'; +import { AppNotification, AppNotificationSeverity, AppNotificationsState } from 'app/types/appNotifications'; const MAX_STORED_NOTIFICATIONS = 25; export const STORAGE_KEY = 'notifications'; diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts index 6b584776233..c9f5d5068bf 100644 --- a/public/app/core/reducers/root.test.ts +++ b/public/app/core/reducers/root.test.ts @@ -1,6 +1,7 @@ +import { Team } from 'app/types/teams'; + import { reducerTester } from '../../../test/core/redux/reducerTester'; import { initialTeamsState, teamsLoaded } from '../../features/teams/state/reducers'; -import { Team } from '../../types'; import { StoreState } from '../../types/store'; import { cleanUpAction } from '../actions/cleanUp'; diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index d28d2f9caba..67078fdbd27 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -1,7 +1,7 @@ import { ReducersMapObject } from '@reduxjs/toolkit'; import { AnyAction, combineReducers } from 'redux'; -import { alertingAPIv0alpha1 } from '@grafana/alerting/unstable'; +import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable'; import sharedReducers from 'app/core/reducers'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; @@ -61,7 +61,7 @@ const rootReducers = { ...authConfigReducers, plugins: pluginsReducer, [alertingApi.reducerPath]: alertingApi.reducer, - [alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer, + [alertingPackageAPI.reducerPath]: alertingPackageAPI.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, [browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer, [cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer, diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index ac0d13d1051..f518dc83df7 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -25,7 +25,8 @@ import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardSearchItem } from 'app/features/search/types'; import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal'; -import { DashboardDTO, FolderDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; +import { FolderDTO } from 'app/types/folders'; import { ShowModalReactEvent } from '../../types/events'; import { isContentTypeJson, parseInitFromOptions, parseResponseBody, parseUrlFromOptions } from '../utils/fetch'; diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 513b5a97699..8374d4b7bf1 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { featureEnabled, getBackendSrv } from '@grafana/runtime'; import { getSessionExpiry } from 'app/core/utils/auth'; -import { AccessControlAction, UserPermission } from 'app/types'; +import { UserPermission, AccessControlAction } from 'app/types/accessControl'; import { CurrentUserInternal } from 'app/types/config'; import config from '../../core/config'; diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index 0c6a95b981d..0fba9ac1352 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -1,7 +1,7 @@ import { dateTime, DataQuery } from '@grafana/data'; import store from 'app/core/store'; +import { RichHistoryQuery } from 'app/types/explore'; -import { RichHistoryQuery } from '../../types'; import RichHistoryStorage, { RichHistoryStorageWarning } from '../history/RichHistoryStorage'; import { diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts index 38d459b906a..406aa73e99f 100644 --- a/public/app/core/utils/shortLinks.test.ts +++ b/public/app/core/utils/shortLinks.test.ts @@ -15,6 +15,17 @@ jest.mock('@grafana/runtime', () => ({ }, })); +beforeEach(() => { + Object.assign(navigator, { + clipboard: { + write: jest.fn().mockResolvedValue(undefined), + writeText: jest.fn().mockResolvedValue(undefined), + }, + }); + + document.execCommand = jest.fn(); +}); + describe('createShortLink', () => { it('creates short link', async () => { const shortUrl = await createShortLink('www.verylonglinkwehavehere.com'); @@ -23,11 +34,34 @@ describe('createShortLink', () => { }); describe('createAndCopyShortLink', () => { - it('copies short link to clipboard', async () => { + it('copies short link to clipboard via document.execCommand when navigator.clipboard is undefined', async () => { + Object.assign(navigator, { + clipboard: { + write: undefined, + }, + }); document.execCommand = jest.fn(); await createAndCopyShortLink('www.verylonglinkwehavehere.com'); expect(document.execCommand).toHaveBeenCalledWith('copy'); }); + + it('copies short link to clipboard via navigator.clipboard.writeText when ClipboardItem is undefined', async () => { + window.isSecureContext = true; + await createAndCopyShortLink('www.verylonglinkwehavehere.com'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('www.short.com'); + }); + + it('copies short link to clipboard via navigator.clipboard.write and ClipboardItem when it is defined', async () => { + global.ClipboardItem = jest.fn().mockImplementation(() => ({ + type: 'text/plain', + size: 0, + slice: jest.fn(), + supports: jest.fn().mockReturnValue(true), + // eslint-disable-next-line + })) as any; + await createAndCopyShortLink('www.verylonglinkwehavehere.com'); + expect(navigator.clipboard.write).toHaveBeenCalled(); + }); }); describe('getLogsPermalinkRange', () => { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 3ab6556aa67..b6bfc95b006 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -35,13 +35,30 @@ export const createShortLink = memoizeOne(async function (path: string) { } }); +/** + * Creates a ClipboardItem for the shortened link. This is used due to clipboard issues in Safari after making async calls. + * See https://github.com/grafana/grafana/issues/106889 + * @param path - The long path to share. + * @returns A ClipboardItem for the shortened link. + */ +const createShortLinkClipboardItem = (path: string) => { + return new ClipboardItem({ + 'text/plain': createShortLink(path), + }); +}; + export const createAndCopyShortLink = async (path: string) => { - const shortLink = await createShortLink(path); - if (shortLink) { - copyStringToClipboard(shortLink); + if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write) { + navigator.clipboard.write([createShortLinkClipboardItem(path)]); dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard'))); } else { - dispatch(notifyApp(createErrorNotification('Error generating shortened link'))); + const shortLink = await createShortLink(path); + if (shortLink) { + copyStringToClipboard(shortLink); + dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard'))); + } else { + dispatch(notifyApp(createErrorNotification('Error generating shortened link'))); + } } }; diff --git a/public/app/dev-utils.ts b/public/app/dev-utils.ts index 478af4f8f64..07a656735c1 100644 --- a/public/app/dev-utils.ts +++ b/public/app/dev-utils.ts @@ -3,7 +3,7 @@ import store from 'app/core/store'; import { sendAppNotification } from './core/copy/appNotification'; import { PreferencesService } from './core/services/PreferencesService'; -import { AppNotificationSeverity } from './types'; +import { AppNotificationSeverity } from './types/appNotifications'; export const STORAGE_MOCK_API_KEY = 'grafana.dev.mockApi'; diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 34b0d477523..fb6039792dc 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -3,12 +3,13 @@ import { useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom-v5-compat'; import { useAsyncFn } from 'react-use'; -import { NavModelItem } from '@grafana/data'; +import { NavModelItem, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Field, Input, Button, Legend, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { OrgUser, AccessControlAction, OrgRole } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { OrgUser } from 'app/types/user'; import { OrgUsersTable } from './Users/OrgUsersTable'; import { getOrg, getOrgUsers, getUsersRoles, removeOrgUser, updateOrgName, updateOrgUserRole } from './api'; diff --git a/public/app/features/admin/AdminFeatureTogglesTable.tsx b/public/app/features/admin/AdminFeatureTogglesTable.tsx index 3f97bb526c3..65b6112652c 100644 --- a/public/app/features/admin/AdminFeatureTogglesTable.tsx +++ b/public/app/features/admin/AdminFeatureTogglesTable.tsx @@ -186,7 +186,7 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat

} - confirmText="Save changes" + confirmText={t('admin.admin-feature-toggles-table.confirmText-save-changes', 'Save changes')} onConfirm={async () => { showSaveChangesModal(false)(); handleSaveChanges(); diff --git a/public/app/features/admin/AdminListOrgsPage.tsx b/public/app/features/admin/AdminListOrgsPage.tsx index be3ef8f2624..48da226edaf 100644 --- a/public/app/features/admin/AdminListOrgsPage.tsx +++ b/public/app/features/admin/AdminListOrgsPage.tsx @@ -6,7 +6,8 @@ import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Organization } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Organization } from 'app/types/organization'; import { AdminOrgsTable } from './AdminOrgsTable'; diff --git a/public/app/features/admin/AdminOrgsTable.tsx b/public/app/features/admin/AdminOrgsTable.tsx index e11fde93557..d85dcccfea5 100644 --- a/public/app/features/admin/AdminOrgsTable.tsx +++ b/public/app/features/admin/AdminOrgsTable.tsx @@ -7,7 +7,8 @@ import { Trans, t } from '@grafana/i18n'; import { Button, ConfirmModal, useStyles2 } from '@grafana/ui'; import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Organization } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Organization } from 'app/types/organization'; interface Props { orgs: Organization[]; @@ -72,7 +73,7 @@ function AdminOrgsTableComponent({ orgs, onDelete }: Props) { } - confirmText="Delete" + confirmText={t('admin.admin-orgs-table.confirmText-delete', 'Delete')} onDismiss={() => setDeleteOrg(undefined)} onConfirm={() => { onDelete(deleteOrg.id); diff --git a/public/app/features/admin/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx index a7f62ec72c1..f3d8e4d0a66 100644 --- a/public/app/features/admin/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { config, GrafanaBootConfig } from '@grafana/runtime'; import { LinkButton, Stack, useStyles2 } from '@grafana/ui'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { contextSrv } from '../../core/services/context_srv'; diff --git a/public/app/features/admin/UpgradePage.tsx b/public/app/features/admin/UpgradePage.tsx index 3a8b8ed7f02..61925407cbd 100644 --- a/public/app/features/admin/UpgradePage.tsx +++ b/public/app/features/admin/UpgradePage.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2, NavModel } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types/store'; import checkmarkSvg from 'img/licensing/checkmark.svg'; import customerSupportSvg from 'img/licensing/customer_support.svg'; import handinhandSupportSvg from 'img/licensing/handinhand_support.svg'; @@ -13,7 +14,6 @@ import pluginEnterpriseSvg from 'img/licensing/plugin_enterprise.svg'; import slaSvg from 'img/licensing/sla.svg'; import { getNavModel } from '../../core/selectors/navModel'; -import { StoreState } from '../../types'; import { LicenseChrome } from './LicenseChrome'; import { ServerStats } from './ServerStats'; diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 8a2f2c84aa8..1642c3259e8 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -8,7 +8,10 @@ import { featureEnabled } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { StoreState, UserDTO, UserOrg, UserSession, SyncInfo, UserAdminError, AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { SyncInfo } from 'app/types/ldap'; +import { StoreState } from 'app/types/store'; +import { UserDTO, UserOrg, UserSession, UserAdminError } from 'app/types/user'; import { UserLdapSyncInfo } from './UserLdapSyncInfo'; import { UserOrgs } from './UserOrgs'; diff --git a/public/app/features/admin/UserLdapSyncInfo.tsx b/public/app/features/admin/UserLdapSyncInfo.tsx index 387cc3eb9fa..ca1d0ec148a 100644 --- a/public/app/features/admin/UserLdapSyncInfo.tsx +++ b/public/app/features/admin/UserLdapSyncInfo.tsx @@ -4,7 +4,9 @@ import { dateTimeFormat } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, LinkButton } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, SyncInfo, UserDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { SyncInfo } from 'app/types/ldap'; +import { UserDTO } from 'app/types/user'; import { TagBadge } from '../../core/components/TagFilter/TagBadge'; diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index d4b485c3ada..215fd281f3e 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -8,8 +8,9 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, RadioButtonGroup, useStyles2, FilterInput, EmptyState } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; - -import { AccessControlAction, StoreState, UserFilter } from '../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; +import { UserFilter } from 'app/types/user'; import { EnterpriseAuthFeaturesCard } from './EnterpriseAuthFeaturesCard'; import { UsersTable } from './Users/UsersTable'; diff --git a/public/app/features/admin/UserListAnonymousPage.tsx b/public/app/features/admin/UserListAnonymousPage.tsx index c2cd0404dd8..eec26483830 100644 --- a/public/app/features/admin/UserListAnonymousPage.tsx +++ b/public/app/features/admin/UserListAnonymousPage.tsx @@ -7,8 +7,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { t } from '@grafana/i18n'; import { RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; - -import { StoreState } from '../../types'; +import { StoreState } from 'app/types/store'; import { AnonUsersDevicesTable } from './Users/AnonUsersTable'; import { fetchUsersAnonymousDevices, changeAnonUserSort, changeAnonPage, changeAnonQuery } from './state/actions'; diff --git a/public/app/features/admin/UserListPage.tsx b/public/app/features/admin/UserListPage.tsx index 97184596de4..a2357b4aecf 100644 --- a/public/app/features/admin/UserListPage.tsx +++ b/public/app/features/admin/UserListPage.tsx @@ -9,9 +9,9 @@ import { config } from '@grafana/runtime'; import { useStyles2, TabsBar, Tab } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { isEmailSharingEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; +import { AccessControlAction } from 'app/types/accessControl'; import { Page } from '../../core/components/Page/Page'; -import { AccessControlAction } from '../../types'; import { UsersListPageContent } from '../users/UsersListPage'; import { UserListAdminPageContent } from './UserListAdminPage'; diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index f17cb6290d9..201df124ec8 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import { createRef, PureComponent, ReactElement } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, @@ -20,7 +20,9 @@ import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Organization, OrgRole, Role, UserDTO, UserOrg } from 'app/types'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { Organization } from 'app/types/organization'; +import { UserOrg, UserDTO } from 'app/types/user'; import { OrgRolePicker } from './OrgRolePicker'; @@ -240,7 +242,7 @@ class UnThemedOrgRow extends PureComponent {
{canRemoveFromOrg && ( ) : ( {t('admin.user-permissions.change-button', 'Change')} diff --git a/public/app/features/admin/UserProfile.tsx b/public/app/features/admin/UserProfile.tsx index bed64e97a3e..1fb9da50d77 100644 --- a/public/app/features/admin/UserProfile.tsx +++ b/public/app/features/admin/UserProfile.tsx @@ -5,7 +5,8 @@ import * as React from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, ConfirmButton, ConfirmModal, Input, LegacyInputStatus, Stack } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, UserDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { UserDTO } from 'app/types/user'; interface Props { user: UserDTO; @@ -142,8 +143,8 @@ export function UserProfile({ @@ -162,8 +163,8 @@ export function UserProfile({ @@ -290,7 +291,7 @@ export class UserProfileRow extends PureComponent { {canLogout && ( @@ -115,8 +116,11 @@ class BaseUserSessions extends PureComponent { diff --git a/public/app/features/admin/Users/AnonUsersTable.tsx b/public/app/features/admin/Users/AnonUsersTable.tsx index b4a4882314f..ba540e280a6 100644 --- a/public/app/features/admin/Users/AnonUsersTable.tsx +++ b/public/app/features/admin/Users/AnonUsersTable.tsx @@ -13,7 +13,7 @@ import { FetchDataFunc, } from '@grafana/ui'; import { EmptyArea } from 'app/features/alerting/unified/components/EmptyArea'; -import { UserAnonymousDeviceDTO } from 'app/types'; +import { UserAnonymousDeviceDTO } from 'app/types/user'; type Cell = CellProps< UserAnonymousDeviceDTO, diff --git a/public/app/features/admin/Users/OrgUnits.tsx b/public/app/features/admin/Users/OrgUnits.tsx index 5dc25622fed..b029df9e19e 100644 --- a/public/app/features/admin/Users/OrgUnits.tsx +++ b/public/app/features/admin/Users/OrgUnits.tsx @@ -2,7 +2,7 @@ import { forwardRef, PropsWithChildren } from 'react'; import { IconName } from '@grafana/data'; import { Icon, Tooltip, Box, Stack } from '@grafana/ui'; -import { Unit } from 'app/types'; +import { Unit } from 'app/types/user'; type OrgUnitProps = { units?: Unit[]; icon: IconName }; diff --git a/public/app/features/admin/Users/OrgUsersTable.test.tsx b/public/app/features/admin/Users/OrgUsersTable.test.tsx index ea64232081e..8fa183441db 100644 --- a/public/app/features/admin/Users/OrgUsersTable.test.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.test.tsx @@ -1,7 +1,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { OrgUser } from '../../../types'; +import { OrgUser } from 'app/types/user'; + import { getMockUsers } from '../../users/mocks/userMocks'; import { OrgUsersTable, Props } from './OrgUsersTable'; diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx index 9e2ba45de63..42f69b89cf2 100644 --- a/public/app/features/admin/Users/OrgUsersTable.tsx +++ b/public/app/features/admin/Users/OrgUsersTable.tsx @@ -25,7 +25,8 @@ import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicke import { RolePickerBadges } from 'app/core/components/RolePickerDrawer/RolePickerBadges'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, OrgUser, Role } from 'app/types'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { OrgUser } from 'app/types/user'; import { OrgRolePicker } from '../OrgRolePicker'; @@ -261,8 +262,10 @@ export const OrgUsersTable = ({ {Boolean(userToRemove) && ( { setUserToRemove(null); diff --git a/public/app/features/admin/Users/UsersTable.test.tsx b/public/app/features/admin/Users/UsersTable.test.tsx index 667aca52b89..61c8210a56f 100644 --- a/public/app/features/admin/Users/UsersTable.test.tsx +++ b/public/app/features/admin/Users/UsersTable.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom-v5-compat'; -import { UserDTO } from '../../../types'; +import { UserDTO } from 'app/types/user'; import { UsersTable, UsersTableProps } from './UsersTable'; diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx index 7deb4480fd4..a3a1f6be297 100644 --- a/public/app/features/admin/Users/UsersTable.tsx +++ b/public/app/features/admin/Users/UsersTable.tsx @@ -17,7 +17,7 @@ import { Tooltip, } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; -import { UserDTO } from 'app/types'; +import { UserDTO } from 'app/types/user'; import { OrgUnits } from './OrgUnits'; diff --git a/public/app/features/admin/api.ts b/public/app/features/admin/api.ts index 0db8de7ed13..f1e4f5419d4 100644 --- a/public/app/features/admin/api.ts +++ b/public/app/features/admin/api.ts @@ -2,7 +2,8 @@ import { UrlQueryValue } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { OrgUser, AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { OrgUser } from 'app/types/user'; const perPage = 30; diff --git a/public/app/features/admin/ldap/LdapConnectionStatus.tsx b/public/app/features/admin/ldap/LdapConnectionStatus.tsx index 59dc679d197..268ac3ea1b5 100644 --- a/public/app/features/admin/ldap/LdapConnectionStatus.tsx +++ b/public/app/features/admin/ldap/LdapConnectionStatus.tsx @@ -2,7 +2,8 @@ import { useMemo } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Alert, CellProps, Column, Icon, InteractiveTable, Stack, Text, Tooltip } from '@grafana/ui'; -import { AppNotificationSeverity, LdapConnectionInfo, LdapServerInfo } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { LdapConnectionInfo, LdapServerInfo } from 'app/types/ldap'; interface Props { ldapConnectionInfo: LdapConnectionInfo; diff --git a/public/app/features/admin/ldap/LdapDrawer.tsx b/public/app/features/admin/ldap/LdapDrawer.tsx index cd3e0a1342c..159a66a9ea5 100644 --- a/public/app/features/admin/ldap/LdapDrawer.tsx +++ b/public/app/features/admin/ldap/LdapDrawer.tsx @@ -23,7 +23,7 @@ import { RadioButtonGroup, SecretInput, } from '@grafana/ui'; -import { LdapPayload, MapKeyCertConfigured } from 'app/types'; +import { MapKeyCertConfigured, LdapPayload } from 'app/types/ldap'; import { GroupMappingComponent } from './LdapGroupMapping'; diff --git a/public/app/features/admin/ldap/LdapGroupMapping.tsx b/public/app/features/admin/ldap/LdapGroupMapping.tsx index 6680294e578..1a7b8d9450a 100644 --- a/public/app/features/admin/ldap/LdapGroupMapping.tsx +++ b/public/app/features/admin/ldap/LdapGroupMapping.tsx @@ -1,10 +1,10 @@ import { useFormContext } from 'react-hook-form'; -import { SelectableValue } from '@grafana/data'; +import { OrgRole, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Box, Button, Field, Input, RadioButtonGroup, Switch } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { LdapPayload, OrgRole } from 'app/types'; +import { LdapPayload } from 'app/types/ldap'; const roleOptions: Array> = Object.keys(OrgRole).map((key) => { return { label: key, value: key }; diff --git a/public/app/features/admin/ldap/LdapPage.tsx b/public/app/features/admin/ldap/LdapPage.tsx index ce1ba188936..4c6bc32f0e7 100644 --- a/public/app/features/admin/ldap/LdapPage.tsx +++ b/public/app/features/admin/ldap/LdapPage.tsx @@ -9,15 +9,10 @@ import { Alert, Button, Field, Input, 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'; -import { - AppNotificationSeverity, - LdapError, - LdapUser, - StoreState, - SyncInfo, - LdapConnectionInfo, - AccessControlAction, -} from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { LdapConnectionInfo, LdapUser, SyncInfo, LdapError } from 'app/types/ldap'; +import { StoreState } from 'app/types/store'; import { loadLdapState, diff --git a/public/app/features/admin/ldap/LdapSettingsPage.tsx b/public/app/features/admin/ldap/LdapSettingsPage.tsx index 7ba327c1ce0..b100c9386fb 100644 --- a/public/app/features/admin/ldap/LdapSettingsPage.tsx +++ b/public/app/features/admin/ldap/LdapSettingsPage.tsx @@ -27,7 +27,8 @@ import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { Page } from 'app/core/components/Page/Page'; import config from 'app/core/config'; import { Loader } from 'app/features/plugins/admin/components/Loader'; -import { LdapPayload, MapKeyCertConfigured, StoreState } from 'app/types'; +import { LdapPayload, MapKeyCertConfigured } from 'app/types/ldap'; +import { StoreState } from 'app/types/store'; import { LdapDrawerComponent } from './LdapDrawer'; diff --git a/public/app/features/admin/ldap/LdapSyncInfo.tsx b/public/app/features/admin/ldap/LdapSyncInfo.tsx index 11a1db488b5..c4aacb5a7ad 100644 --- a/public/app/features/admin/ldap/LdapSyncInfo.tsx +++ b/public/app/features/admin/ldap/LdapSyncInfo.tsx @@ -1,7 +1,7 @@ import { dateTimeFormat } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { InteractiveTable, Text } from '@grafana/ui'; -import { SyncInfo } from 'app/types'; +import { SyncInfo } from 'app/types/ldap'; interface Props { ldapSyncInfo: SyncInfo; diff --git a/public/app/features/admin/ldap/LdapUserGroups.tsx b/public/app/features/admin/ldap/LdapUserGroups.tsx index d2e82b83e3e..7f0ff635a44 100644 --- a/public/app/features/admin/ldap/LdapUserGroups.tsx +++ b/public/app/features/admin/ldap/LdapUserGroups.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Tooltip, Icon, InteractiveTable, type CellProps, Column, Stack } from '@grafana/ui'; -import { LdapRole } from 'app/types'; +import { LdapRole } from 'app/types/ldap'; interface Props { groups: LdapRole[]; diff --git a/public/app/features/admin/ldap/LdapUserInfo.tsx b/public/app/features/admin/ldap/LdapUserInfo.tsx index 0877216ca6f..73d6a79a87b 100644 --- a/public/app/features/admin/ldap/LdapUserInfo.tsx +++ b/public/app/features/admin/ldap/LdapUserInfo.tsx @@ -1,6 +1,6 @@ import { Trans } from '@grafana/i18n'; import { Box, Stack, Text } from '@grafana/ui'; -import { LdapUser } from 'app/types'; +import { LdapUser } from 'app/types/ldap'; import { LdapUserGroups } from './LdapUserGroups'; import { LdapUserMappingInfo } from './LdapUserMappingInfo'; diff --git a/public/app/features/admin/ldap/LdapUserMappingInfo.tsx b/public/app/features/admin/ldap/LdapUserMappingInfo.tsx index 534ea3cf0a7..e4485a9d3c2 100644 --- a/public/app/features/admin/ldap/LdapUserMappingInfo.tsx +++ b/public/app/features/admin/ldap/LdapUserMappingInfo.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { InteractiveTable } from '@grafana/ui'; -import { LdapUserInfo } from 'app/types'; +import { LdapUserInfo } from 'app/types/ldap'; interface Props { info: LdapUserInfo; diff --git a/public/app/features/admin/ldap/LdapUserPermissions.tsx b/public/app/features/admin/ldap/LdapUserPermissions.tsx index e426a4e090b..57dc65f4c96 100644 --- a/public/app/features/admin/ldap/LdapUserPermissions.tsx +++ b/public/app/features/admin/ldap/LdapUserPermissions.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Trans } from '@grafana/i18n'; import { Column, Icon, InteractiveTable } from '@grafana/ui'; -import { LdapPermissions } from 'app/types'; +import { LdapPermissions } from 'app/types/ldap'; interface Props { permissions: LdapPermissions; diff --git a/public/app/features/admin/ldap/LdapUserTeams.tsx b/public/app/features/admin/ldap/LdapUserTeams.tsx index 39f9f779710..b37cad73034 100644 --- a/public/app/features/admin/ldap/LdapUserTeams.tsx +++ b/public/app/features/admin/ldap/LdapUserTeams.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { Column, InteractiveTable, CellProps } from '@grafana/ui'; -import { LdapTeam } from 'app/types'; +import { LdapTeam } from 'app/types/ldap'; interface Props { teams: LdapTeam[]; diff --git a/public/app/features/admin/state/actions.ts b/public/app/features/admin/state/actions.ts index ede0ed15dd6..7dd57ea1e72 100644 --- a/public/app/features/admin/state/actions.ts +++ b/public/app/features/admin/state/actions.ts @@ -6,15 +6,10 @@ import { FetchDataArgs } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { - ThunkResult, - LdapUser, - UserSession, - UserDTO, - AccessControlAction, - UserFilter, - AnonUserFilter, -} from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { LdapUser } from 'app/types/ldap'; +import { ThunkResult } from 'app/types/store'; +import { UserDTO, UserSession, UserFilter, AnonUserFilter } from 'app/types/user'; import { userAdminPageLoadedAction, diff --git a/public/app/features/admin/state/reducers.test.ts b/public/app/features/admin/state/reducers.test.ts index 8b504a62935..67652835859 100644 --- a/public/app/features/admin/state/reducers.test.ts +++ b/public/app/features/admin/state/reducers.test.ts @@ -1,6 +1,7 @@ import { reducerTester } from 'test/core/redux/reducerTester'; -import { LdapState, LdapUser, UserAdminState, UserDTO, UserListAdminState } from 'app/types'; +import { LdapState, LdapUser } from 'app/types/ldap'; +import { UserAdminState, UserListAdminState, UserDTO } from 'app/types/user'; import { clearUserMappingInfoAction, diff --git a/public/app/features/admin/state/reducers.ts b/public/app/features/admin/state/reducers.ts index 550e3de57b6..67f71dec9f3 100644 --- a/public/app/features/admin/state/reducers.ts +++ b/public/app/features/admin/state/reducers.ts @@ -1,22 +1,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { LdapState, LdapConnectionInfo, LdapError, SyncInfo, LdapUser } from 'app/types/ldap'; import { - LdapConnectionInfo, - LdapError, - LdapState, - LdapUser, - SyncInfo, - UserAdminError, UserAdminState, UserDTO, UserOrg, UserSession, + UserAdminError, UserListAdminState, UserFilter, UserListAnonymousDevicesState, UserAnonymousDeviceDTO, AnonUserFilter, -} from 'app/types'; +} from 'app/types/user'; const initialLdapState: LdapState = { connectionInfo: [], diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 12a8dbfbadf..e51fd7cc27e 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -3,7 +3,7 @@ import { Navigate } from 'react-router-dom-v5-compat'; import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { config } from 'app/core/config'; import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PERMISSIONS_CONTACT_POINTS } from './unified/components/contact-points/permissions'; import { @@ -314,8 +314,16 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { ), }, { + // This route is for backward compatibility + // Previously we had a single admin page containing only the alertmanager settings + // We now have a separate route for the alertmanager settings and other settings can be added as extensions path: '/alerting/admin', roles: () => ['Admin'], + component: () => , + }, + { + path: '/alerting/admin/alertmanager', + roles: () => ['Admin'], component: importAlertingComponent( () => import(/* webpackChunkName: "AlertingSettings" */ 'app/features/alerting/unified/Settings') ), diff --git a/public/app/features/alerting/state/reducers.test.ts b/public/app/features/alerting/state/reducers.test.ts index ac9b9acda67..7afd5eee5e7 100644 --- a/public/app/features/alerting/state/reducers.test.ts +++ b/public/app/features/alerting/state/reducers.test.ts @@ -1,5 +1,5 @@ import { dateTime } from '@grafana/data'; -import { AlertRuleDTO, AlertRulesState, NotificationChannelState, NotifierDTO } from 'app/types'; +import { AlertRuleDTO, AlertRulesState, NotificationChannelState, NotifierDTO } from 'app/types/alerting'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index f4c9b798bc2..ffba9aa8c89 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -8,7 +8,7 @@ import { NotificationChannelOption, NotificationChannelState, NotifierDTO, -} from 'app/types'; +} from 'app/types/alerting'; import unifiedAlertingReducer from '../unified/state/reducers'; diff --git a/public/app/features/alerting/state/selectors.test.ts b/public/app/features/alerting/state/selectors.test.ts index b279c1ebd41..0463a99be8e 100644 --- a/public/app/features/alerting/state/selectors.test.ts +++ b/public/app/features/alerting/state/selectors.test.ts @@ -1,4 +1,5 @@ -import { AlertRulesState, StoreState } from 'app/types'; +import { AlertRulesState } from 'app/types/alerting'; +import { StoreState } from 'app/types/store'; import { getAlertRuleItems, getSearchQuery } from './selectors'; diff --git a/public/app/features/alerting/state/selectors.ts b/public/app/features/alerting/state/selectors.ts index f51e66ce8a4..0e2c897e7bb 100644 --- a/public/app/features/alerting/state/selectors.ts +++ b/public/app/features/alerting/state/selectors.ts @@ -1,4 +1,5 @@ -import { AlertRule, AlertRulesState, NotificationChannelState, StoreState } from 'app/types'; +import { AlertRule, AlertRulesState, NotificationChannelState } from 'app/types/alerting'; +import { StoreState } from 'app/types/store'; export const getSearchQuery = (state: AlertRulesState) => state.searchQuery; diff --git a/public/app/features/alerting/unified/AlertGroups.test.tsx b/public/app/features/alerting/unified/AlertGroups.test.tsx index a39b0348f15..dbf38615d33 100644 --- a/public/app/features/alerting/unified/AlertGroups.test.tsx +++ b/public/app/features/alerting/unified/AlertGroups.test.tsx @@ -3,7 +3,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import AlertGroups from './AlertGroups'; import { fetchAlertGroups } from './api/alertmanager'; diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index a72d973645b..0ca50b2c7c9 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -3,7 +3,7 @@ import { Fragment, useEffect } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Alert, Box, LoadingPlaceholder, Text } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types'; diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx index 8e13f93435a..ec39dcdde18 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui'; import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants'; import { getQueryParamValue } from 'app/core/utils/query'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { GrafanaRuleDefinition, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { usePagination } from './hooks/usePagination'; diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index 0b69e95653e..fc44b90a59f 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -14,7 +14,7 @@ import { } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import EditMuteTimingPage from './components/mute-timings/EditMuteTiming'; import NewMuteTimingPage from './components/mute-timings/NewMuteTiming'; diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx index f48f3eb16fe..90bfe88ad4b 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx @@ -29,7 +29,7 @@ import { MatcherOperator, RouteWithID, } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import NotificationPolicies from './NotificationPoliciesPage'; import { findRoutesMatchingFilters } from './components/notification-policies/NotificationPoliciesList'; diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 9e01d77d060..dfe3549eeee 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -7,7 +7,7 @@ import config from 'app/core/config'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto'; import { PanelAlertTabContent } from './PanelAlertTabContent'; diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index 0363e772509..f831a707bb3 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -10,7 +10,7 @@ import { import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import ContactPoints from './components/contact-points/ContactPoints'; import EditContactPoint from './components/contact-points/EditContactPoint'; diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx index 196ee8b0583..fcc74629ca1 100644 --- a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx +++ b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx @@ -9,7 +9,7 @@ import { FeatureToggles } from '@grafana/data'; import { contextSrv } from 'app/core/services/context_srv'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions, mockDataSource } from './mocks'; import { grafanaRulerGroup } from './mocks/grafanaRulerApi'; diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 5868e5b4c13..905920cbf1d 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -12,7 +12,7 @@ import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/serv import * as actions from 'app/features/alerting/unified/state/actions'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto'; import RuleList from './RuleList'; diff --git a/public/app/features/alerting/unified/Settings.test.tsx b/public/app/features/alerting/unified/Settings.test.tsx index 0d362c74b45..cba6f0a0308 100644 --- a/public/app/features/alerting/unified/Settings.test.tsx +++ b/public/app/features/alerting/unified/Settings.test.tsx @@ -7,6 +7,7 @@ import DataSourcesResponse from './components/settings/mocks/api/datasources.jso import { setupGrafanaManagedServer, withExternalOnlySetting } from './components/settings/mocks/server'; import { setupMswServer } from './mockApi'; import { grantUserRole } from './mocks'; +import { addSettingsSection, clearSettingsExtensions } from './settings/extensions'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -35,12 +36,23 @@ const ui = { versionsTab: byRole('tab', { name: /versions/i }), provisionedBadge: byText(/^Provisioned$/), + + // New selectors for extension tabs + alertmanagerTab: byRole('tab', { name: 'Alert managers' }), + enrichmentTab: byRole('tab', { name: 'Enrichment' }), + notificationsTab: byRole('tab', { name: 'Notifications' }), + customTab: (name: string) => byRole('tab', { name }), }; describe('Alerting settings', () => { beforeEach(() => { grantUserRole('ServerAdmin'); setupGrafanaManagedServer(server); + clearSettingsExtensions(); + }); + + afterEach(() => { + clearSettingsExtensions(); }); it('should render the page with Built-in only enabled, others disabled', async () => { @@ -131,4 +143,118 @@ describe('Alerting settings', () => { expect(enableButton).not.toBeInTheDocument(); expect(disableButton).not.toBeInTheDocument(); }); + + it('should display additional tabs when settings extensions are registered', async () => { + // Register extensions before rendering + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + addSettingsSection({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + icon: 'bell', + }); + + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that the extension tabs are visible + expect(ui.enrichmentTab.get()).toBeInTheDocument(); + expect(ui.notificationsTab.get()).toBeInTheDocument(); + }); + + it('should correctly show active state for extension tabs based on route', async () => { + // Register an extension + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + // Render with the extension route as active + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/enrichment'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that the extension tab is visible and active + const enrichmentTab = ui.enrichmentTab.get(); + expect(enrichmentTab).toBeInTheDocument(); + expect(enrichmentTab).toHaveAttribute('aria-selected', 'true'); + + // Check that the default alertmanager tab is not active + const alertmanagerTab = ui.alertmanagerTab.get(); + expect(alertmanagerTab).toHaveAttribute('aria-selected', 'false'); + }); + + it('should handle multiple extensions correctly', async () => { + // Register multiple extensions + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + addSettingsSection({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + icon: 'bell', + }); + + addSettingsSection({ + id: 'custom-settings', + text: 'Custom Settings', + url: '/alerting/admin/custom', + icon: 'cog', + }); + + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that all tabs are visible + expect(ui.alertmanagerTab.get()).toBeInTheDocument(); + expect(ui.enrichmentTab.get()).toBeInTheDocument(); + expect(ui.notificationsTab.get()).toBeInTheDocument(); + expect(ui.customTab('Custom Settings').get()).toBeInTheDocument(); + }); + + it('should not show extension tabs when no extensions are registered', async () => { + render(, { + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + // Wait for the page to load + await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + + // Check that only the default alertmanager tab is visible + expect(ui.alertmanagerTab.get()).toBeInTheDocument(); + expect(ui.enrichmentTab.query()).not.toBeInTheDocument(); + expect(ui.notificationsTab.query()).not.toBeInTheDocument(); + }); }); diff --git a/public/app/features/alerting/unified/Settings.tsx b/public/app/features/alerting/unified/Settings.tsx index e1a1eea1679..f09e9036fd3 100644 --- a/public/app/features/alerting/unified/Settings.tsx +++ b/public/app/features/alerting/unified/Settings.tsx @@ -7,24 +7,28 @@ import { useEditConfigurationDrawer } from './components/settings/ConfigurationD import { ExternalAlertmanagers } from './components/settings/ExternalAlertmanagers'; import InternalAlertmanager from './components/settings/InternalAlertmanager'; import { SettingsProvider, useSettings } from './components/settings/SettingsContext'; +import { useSettingsPageNav } from './settings/navigation'; import { withPageErrorBoundary } from './withPageErrorBoundary'; -function SettingsPage() { +function AlertmanagerSettingsPage() { return ( - + ); } -function SettingsContent() { +function AlertmanagerSettingsContent() { const [configurationDrawer, showConfiguration] = useEditConfigurationDrawer(); const { isLoading } = useSettings(); + const { navId, pageNav } = useSettingsPageNav(); + return ( { + const populateGroupsResponseCache = useCallback( + (groups: GrafanaPromRuleGroupDTO[]) => { dispatch( - prometheusApi.util.upsertQueryData( - 'getGrafanaGroups', - { folderUid: group.folderUid, groupName: group.name }, - { data: { groups: [group] }, status: 'success' } + prometheusApi.util.upsertQueryEntries( + groups.map((group) => ({ + endpointName: 'getGrafanaGroups', + arg: { folderUid: group.folderUid, groupName: group.name, limitAlerts: 0 }, + value: { data: { groups: [group] }, status: 'success' }, + })) ) ); }, [dispatch] ); - return { populateGroupResponseCache }; + return { populateGroupsResponseCache }; } diff --git a/public/app/features/alerting/unified/api/receiversApi.ts b/public/app/features/alerting/unified/api/receiversApi.ts index 3f94a38f26e..7b22a1507e3 100644 --- a/public/app/features/alerting/unified/api/receiversApi.ts +++ b/public/app/features/alerting/unified/api/receiversApi.ts @@ -1,6 +1,6 @@ /** @deprecated To be deleted - use alertingApiServer API instead */ -import { ContactPointsState } from 'app/types'; +import { ContactPointsState } from 'app/types/alerting'; import { CONTACT_POINTS_STATE_INTERVAL_MS } from '../utils/constants'; diff --git a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx index 90cbb059d9f..5a2a7ddc83e 100644 --- a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx +++ b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { ComponentProps, useMemo } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { InlineField, Select, SelectMenuOptions, useStyles2 } from '@grafana/ui'; import { useAlertmanager } from '../state/AlertmanagerContext'; @@ -44,7 +45,10 @@ export const AlertManagerPicker = ({ disabled = false }: Props) => { } }} options={options} - noOptionsMessage="No datasources found" + noOptionsMessage={t( + 'alerting.alert-manager-picker.noOptionsMessage-no-datasources-found', + 'No datasources found' + )} value={selectedAlertmanager} getOptionLabel={(o) => o.label} components={{ Option: CustomOption }} diff --git a/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx b/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx index 0424566f79d..1610330aad0 100644 --- a/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx +++ b/public/app/features/alerting/unified/components/MenuItemPauseRule.tsx @@ -1,15 +1,14 @@ import { Menu } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; -import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { usePauseRuleInGroup } from '../hooks/ruleGroup/usePauseAlertRule'; import { isLoading } from '../hooks/useAsync'; import { stringifyErrorLike } from '../utils/misc'; -import { isPausedRule } from '../utils/rules'; interface Props { - rule: RulerGrafanaRuleDTO; + uid: string; + isPaused: boolean; groupIdentifier: GrafanaRuleGroupIdentifier; /** * Method invoked after the request to change the paused state has completed @@ -21,20 +20,18 @@ interface Props { * Menu item to display correct text for pausing/resuming an alert, * and triggering API call to do so */ -const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => { +const MenuItemPauseRule = ({ uid, isPaused, groupIdentifier, onPauseChange }: Props) => { const notifyApp = useAppNotification(); const [pauseRule, updateState] = usePauseRuleInGroup(); - const [icon, title] = isPausedRule(rule) - ? ['play' as const, 'Resume evaluation'] - : ['pause' as const, 'Pause evaluation']; + const [icon, title] = isPaused ? ['play' as const, 'Resume evaluation'] : ['pause' as const, 'Pause evaluation']; /** * Triggers API call to update the current rule to the new `is_paused` state */ const setRulePause = async (newIsPaused: boolean) => { try { - await pauseRule.execute(groupIdentifier, rule.grafana_alert.uid, newIsPaused); + await pauseRule.execute(groupIdentifier, uid, newIsPaused); } catch (error) { notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`); return; @@ -49,7 +46,7 @@ const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => { icon={icon} disabled={isLoading(updateState)} onClick={() => { - setRulePause(!rule.grafana_alert.is_paused); + setRulePause(!isPaused); }} /> ); diff --git a/public/app/features/alerting/unified/components/MetaText.tsx b/public/app/features/alerting/unified/components/MetaText.tsx index 7de90ce772f..54c06a96699 100644 --- a/public/app/features/alerting/unified/components/MetaText.tsx +++ b/public/app/features/alerting/unified/components/MetaText.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { ComponentProps, HTMLAttributes } from 'react'; +import { ComponentProps, HTMLAttributes, forwardRef } from 'react'; import { Icon, IconName, Stack, Text, useStyles2 } from '@grafana/ui'; @@ -9,31 +9,36 @@ interface Props extends HTMLAttributes { color?: ComponentProps['color']; } -const MetaText = ({ children, icon, color = 'secondary', direction = 'row', ...rest }: Props) => { - const styles = useStyles2(getStyles); - const interactive = typeof rest.onClick === 'function'; +const MetaText = forwardRef( + ({ children, icon, color = 'secondary', direction = 'row', ...rest }, ref) => { + const styles = useStyles2(getStyles); + const interactive = typeof rest.onClick === 'function'; - const rowDirection = direction === 'row'; - const alignItems = rowDirection ? 'center' : 'flex-start'; - const gap = rowDirection ? 0.5 : 0; + const rowDirection = direction === 'row'; + const alignItems = rowDirection ? 'center' : 'flex-start'; + const gap = rowDirection ? 0.5 : 0; - return ( -
- - - {icon && } - {children} - - -
- ); -}; + return ( +
+ + + {icon && } + {children} + + +
+ ); + } +); + +MetaText.displayName = 'MetaText'; const getStyles = () => ({ interactive: css({ diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx index 3177a8aa6e7..642c47a3f9d 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx @@ -5,7 +5,7 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertState, AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertmanagerAction } from '../../hooks/useAbilities'; import { isGrafanaRulesSource } from '../../utils/datasource'; diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx index 57a7a4f48a6..40a7afeddeb 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx @@ -100,7 +100,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade openExportDrawer(name)} @@ -159,7 +159,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade > { await deleteMuteTiming.execute({ diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index a60d1902d7b..d9d611d7a00 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -7,7 +7,7 @@ import { } from 'app/features/alerting/unified/mocks/server/configure'; import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions } from '../../mocks'; import { TIME_INTERVAL_UID_HAPPY_PATH } from '../../mocks/server/handlers/k8s/timeIntervals.k8s'; diff --git a/public/app/features/alerting/unified/components/mute-timings/permissions.ts b/public/app/features/alerting/unified/components/mute-timings/permissions.ts index 5cbf687c636..b2891d4b251 100644 --- a/public/app/features/alerting/unified/components/mute-timings/permissions.ts +++ b/public/app/features/alerting/unified/components/mute-timings/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of granular permissions that allow viewing contact points diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx index e7ed08151a5..f426b2ad737 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.test.tsx @@ -6,7 +6,7 @@ import { Button } from '@grafana/ui'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RouteWithID } from '../../../../../plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../../types/amroutes'; diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx index c83c0813cf1..171e4349bf3 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.test.tsx @@ -6,7 +6,7 @@ import { Button } from '@grafana/ui'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RouteWithID } from '../../../../../plugins/datasource/alertmanager/types'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index 346a9de008e..ff037ca21f2 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -32,7 +32,7 @@ import { Receiver, RouteWithID, } from 'app/plugins/datasource/alertmanager/types'; -import { ReceiversState } from 'app/types'; +import { ReceiversState } from 'app/types/alerting'; import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { getAmMatcherFormatter } from '../../utils/alertmanager'; diff --git a/public/app/features/alerting/unified/components/notification-policies/PolicyUpdateErrorAlert.tsx b/public/app/features/alerting/unified/components/notification-policies/PolicyUpdateErrorAlert.tsx index 842b4840a47..48c573c6f5e 100644 --- a/public/app/features/alerting/unified/components/notification-policies/PolicyUpdateErrorAlert.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/PolicyUpdateErrorAlert.tsx @@ -4,23 +4,12 @@ import { Alert } from '@grafana/ui'; import { stringifyErrorLike } from '../../utils/misc'; export const NotificationPoliciesErrorAlert = ({ error }: { error: unknown }) => { - const title = t('alerting.policies.update-errors.title', 'Error saving notification policy'); + const title = t('alerting.policies.update-errors.title', 'Failed to add or update notification policy'); const errMessage = stringifyErrorLike(error); return ( -
- - Something went wrong when updating your notification policies. - -
-
- {errMessage || ( - - Error message: "{{ error }}" - - )} -
+
{errMessage}
Please refresh the page and try again.
diff --git a/public/app/features/alerting/unified/components/notification-policies/permissions.ts b/public/app/features/alerting/unified/components/notification-policies/permissions.ts index c685357df3e..bcbcae681ab 100644 --- a/public/app/features/alerting/unified/components/notification-policies/permissions.ts +++ b/public/app/features/alerting/unified/components/notification-policies/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of granular permissions that allow viewing notification policies diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx index 08fb0f52c8e..89b8886fb4e 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.test.tsx @@ -9,8 +9,8 @@ import * as analytics from '../../Analytics'; import { NewRuleFromPanelButton } from './NewRuleFromPanelButton'; -jest.mock('app/types', () => { - const original = jest.requireActual('app/types'); +jest.mock('app/types/store', () => { + const original = jest.requireActual('app/types/store'); return { ...original, useSelector: jest.fn(), diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx index 714f42a9a8a..ccd057e4526 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, Button, LinkButton } from '@grafana/ui'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { LogMessages, logInfo } from '../../Analytics'; import { panelToRuleFormValues } from '../../utils/rule-form'; diff --git a/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx b/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx index 12fefb5a4d3..9ec9d6f9b03 100644 --- a/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx @@ -4,7 +4,7 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, Button, LinkButton, Stack } from '@grafana/ui'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { updateAlertManagerConfigAction } from '../../state/actions'; diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx index 3929c478053..e1a1aa5aeba 100644 --- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index 963ee48e6e0..ea00e22b280 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -102,8 +102,12 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => { setTemplateToDelete(undefined)} /> diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx index e402c4f8875..e65dd9f0f38 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { DeepMap, FieldError, FieldErrors, useFormContext } from 'react-hook-form'; import { Field, SecretInput } from '@grafana/ui'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { ChannelValues, diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 7e47f08b5a2..93dfa812d85 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -7,7 +7,7 @@ import { Controller, FieldErrors, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert, Button, Field, Select, Stack, Text, useStyles2 } from '@grafana/ui'; -import { NotificationChannelOption } from 'app/types'; +import { NotificationChannelOption } from 'app/types/alerting'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx index 937c64f4afb..af23e5dc423 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx @@ -13,7 +13,7 @@ import { import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi'; import { grantUserPermissions } from '../../../mocks'; diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index 5a7a4d72d4b..44da5c3588b 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -15,7 +15,7 @@ import { GrafanaManagedReceiverConfig, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { alertmanagerApi } from '../../../api/alertmanagerApi'; import { testReceiversAction } from '../../../state/actions'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx index 62db9294dc3..4bad7a8639f 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/OptionField.tsx @@ -14,7 +14,7 @@ import { TextArea, useStyles2, } from '@grafana/ui'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { KeyValueMapInput } from './KeyValueMapInput'; import { StringArrayInput } from './StringArrayInput'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx index 00533179299..02177df1bae 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/SubformArrayField.tsx @@ -3,7 +3,7 @@ import { DeepMap, FieldError, useFormContext } from 'react-hook-form'; import { Trans, t } from '@grafana/i18n'; import { Button, useStyles2 } from '@grafana/ui'; import { useControlledFieldArray } from 'app/features/alerting/unified/hooks/useControlledFieldArray'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { ActionIcon } from '../../../rules/ActionIcon'; import { CollapsibleSection } from '../CollapsibleSection'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx index e836f4522f8..32118e6bc4c 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/SubformField.tsx @@ -3,7 +3,7 @@ import { DeepMap, FieldError, useFormContext } from 'react-hook-form'; import { Trans, t } from '@grafana/i18n'; import { Button, useStyles2 } from '@grafana/ui'; -import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types'; +import { NotificationChannelOption, NotificationChannelSecureFields, OptionMeta } from 'app/types/alerting'; import { ActionIcon } from '../../../rules/ActionIcon'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx index e8a58495887..74f574ac549 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx @@ -9,7 +9,8 @@ import { AlertmanagerProvider } from 'app/features/alerting/unified/state/Alertm import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants'; -import { AccessControlAction, NotificationChannelOption } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { NotificationChannelOption } from 'app/types/alerting'; import { TemplatesPicker, getTemplateOptions } from './TemplateSelector'; import { parseTemplates } from './utils'; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx index e77a425b6d5..6cfea24cd83 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx @@ -28,7 +28,7 @@ import { useNotificationTemplates, } from 'app/features/alerting/unified/components/contact-points/useNotificationTemplates'; import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; -import { NotificationChannelOption } from 'app/types'; +import { NotificationChannelOption } from 'app/types/alerting'; import { defaultPayloadString } from '../../TemplateForm'; @@ -142,7 +142,10 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe 'alerting.template-selector.template-options.label.select-notification-template', 'Select notification template' ), - ariaLabel: 'Select notification template', + ariaLabel: t( + 'alerting.template-selector.template-options.ariaLabel.select-notification-template', + 'Select notification template' + ), value: 'Existing', description: `Select an existing notification template and preview it, or copy it to paste it in the custom tab. ${templateOption === 'Existing' ? 'Clicking Save saves your changes to the selected template.' : ''}`, }, diff --git a/public/app/features/alerting/unified/components/receivers/form/notifiers.ts b/public/app/features/alerting/unified/components/receivers/form/notifiers.ts index 15baf45aabe..d84e8bc2f9d 100644 --- a/public/app/features/alerting/unified/components/receivers/form/notifiers.ts +++ b/public/app/features/alerting/unified/components/receivers/form/notifiers.ts @@ -1,6 +1,6 @@ import { ReactElement } from 'react'; -import { NotifierDTO } from '../../../../../../types'; +import { NotifierDTO } from 'app/types/alerting'; export interface NotifierMetadata { enabled: boolean; diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx index b30545e604b..2f84d394b9f 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx @@ -6,10 +6,10 @@ import { t } from '@grafana/i18n'; import { isFetchError } from '@grafana/runtime'; import { Badge } from '@grafana/ui'; import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config'; +import { NotifierDTO } from 'app/types/alerting'; import { useAppNotification } from '../../../../../../../core/copy/appNotification'; import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types'; -import { NotifierDTO } from '../../../../../../../types'; import { ONCALL_INTEGRATION_V2_FEATURE, onCallApi } from '../../../../api/onCallApi'; import { usePluginBridge } from '../../../../hooks/usePluginBridge'; import { option } from '../../../../utils/notifier-types'; diff --git a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx index ee9992d4f1b..26d32af3104 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DashboardAnnotationField.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { Icon, Text, useStyles2 } from '@grafana/ui'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { makeDashboardLink, makePanelLink } from '../../utils/misc'; diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 38dda76f142..50163827bdb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -213,7 +213,10 @@ export function GrafanaEvaluationBehaviorStep({ isLoading={loadingGroups} invalid={Boolean(folder?.uid) && !group && Boolean(fieldState.error)} cacheOptions - loadingMessage={'Loading groups...'} + loadingMessage={t( + 'alerting.grafana-evaluation-behavior-step.loadingMessage-loading-groups', + 'Loading groups...' + )} defaultValue={defaultGroupValue} options={groupOptions} getOptionLabel={(option: SelectableValue) => ( 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 c2d7fd5b240..d56b5b5ea01 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 @@ -17,7 +17,7 @@ import { setupDataSources } from 'app/features/alerting/unified/testSetup/dataso import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; import { MANUAL_ROUTING_KEY, SIMPLIFIED_QUERY_EDITOR_KEY } from 'app/features/alerting/unified/utils/rule-form'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grafanaRulerGroup } from '../../../../mocks/grafanaRulerApi'; 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 a2e88321e4f..c512615ce78 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 @@ -3,10 +3,7 @@ import { isEmpty } from 'lodash'; import { useEffect } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { - ContactPointSelector as GrafanaManagedContactPointSelector, - alertingAPIv0alpha1, -} from '@grafana/alerting/unstable'; +import { ContactPointSelector as GrafanaManagedContactPointSelector, alertingAPI } from '@grafana/alerting/unstable'; import { Trans, t } from '@grafana/i18n'; import { Field, FieldValidationMessage, Stack, TextLink } from '@grafana/ui'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; @@ -24,7 +21,7 @@ export function ContactPointSelector({ alertManager }: ContactPointSelectorProps // check if the contact point still exists, we'll use listReceiver to check if the contact point exists because getReceiver doesn't work with // contact point titles but with UUIDs (which is not what we store on the alert rule definition) - const { currentData, status } = alertingAPIv0alpha1.endpoints.listReceiver.useQuery({ + const { currentData, status } = alertingAPI.endpoints.listReceiver.useQuery({ fieldSelector: `spec.title=${contactPointInForm}`, }); 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 8f60198e042..43579acbcc7 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 @@ -718,7 +718,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod
} - confirmText="Deactivate" + confirmText={t('alerting.query-and-expressions-step.confirmText-deactivate', 'Deactivate')} icon="exclamation-triangle" onConfirm={() => { setValue('editorSettings.simplifiedQueryEditor', true); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts index 6ad5dac7d73..138db1979bf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/utils.ts @@ -2,7 +2,7 @@ import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceJsonData } from '@grafana/schema/dist/esm/index'; import { contextSrv } from 'app/core/core'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { RuleFormType } from '../../../types/rule-form'; diff --git a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts index 27bfb98c6a8..bf84e8eb110 100644 --- a/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts +++ b/public/app/features/alerting/unified/components/rule-editor/useDashboardQuery.ts @@ -1,6 +1,7 @@ import memoizeOne from 'memoize-one'; -import { DashboardDTO } from '../../../../../types'; +import { DashboardDTO } from 'app/types/dashboard'; + import { DashboardModel } from '../../../../dashboard/state/DashboardModel'; import { dashboardApi } from '../../api/dashboardApi'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index 5b6a8943da4..0ad498ebc65 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -7,13 +7,24 @@ 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 { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting'; +import { EditableRuleIdentifier, Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting'; import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-dto'; -import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities'; +import { + AlertRuleAction, + skipToken, + useGrafanaPromRuleAbilities, + useRulerRuleAbilities, +} from '../../hooks/useAbilities'; import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; -import { prometheusRuleType, rulerRuleType } from '../../utils/rules'; +import { + getRuleUID, + isEditableRuleIdentifier, + isPausedRule, + prometheusRuleType, + rulerRuleType, +} from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton'; @@ -23,7 +34,7 @@ interface Props { identifier: RuleIdentifier; groupIdentifier: RuleGroupIdentifierV2; handleSilence: () => void; - handleDelete: (rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifierV2) => void; + handleDelete: (identifier: EditableRuleIdentifier, groupIdentifier: RuleGroupIdentifierV2) => void; handleDuplicateRule: (identifier: RuleIdentifier) => void; onPauseChange?: () => void; buttonSize?: ComponentSize; @@ -33,6 +44,8 @@ interface Props { /** * Get a list of menu items + divider elements for rendering in an alert rule's * dropdown menu + * If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions, + * as we have removed all requests to the ruler API in the list view. */ const AlertRuleMenu = ({ promRule, @@ -46,29 +59,51 @@ const AlertRuleMenu = ({ buttonSize, fill, }: Props) => { - // check all abilities and permissions - const [pauseSupported, pauseAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Pause); - const canPause = pauseSupported && pauseAllowed; + // check all abilities and permissions using rulerRule + const [rulerPauseAbility, rulerDeleteAbility, rulerDuplicateAbility, rulerSilenceAbility, rulerExportAbility] = + useRulerRuleAbilities(rulerRule, groupIdentifier, [ + AlertRuleAction.Pause, + AlertRuleAction.Delete, + AlertRuleAction.Duplicate, + AlertRuleAction.Silence, + AlertRuleAction.ModifyExport, + ]); - const [deleteSupported, deleteAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Delete); - const canDelete = deleteSupported && deleteAllowed; + // check all abilities and permissions using promRule + const [ + grafanaPauseAbility, + grafanaDeleteAbility, + grafanaDuplicateAbility, + grafanaSilenceAbility, + grafanaExportAbility, + ] = useGrafanaPromRuleAbilities(prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken, [ + AlertRuleAction.Pause, + AlertRuleAction.Delete, + AlertRuleAction.Duplicate, + AlertRuleAction.Silence, + AlertRuleAction.ModifyExport, + ]); - const [duplicateSupported, duplicateAllowed] = useRulerRuleAbility( - rulerRule, - groupIdentifier, - AlertRuleAction.Duplicate - ); - const canDuplicate = duplicateSupported && duplicateAllowed; + const [pauseSupported, pauseAllowed] = rulerPauseAbility; + const [grafanaPauseSupported, grafanaPauseAllowed] = grafanaPauseAbility; + const canPause = (pauseSupported && pauseAllowed) || (grafanaPauseSupported && grafanaPauseAllowed); - const [silenceSupported, silenceAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Silence); - const canSilence = silenceSupported && silenceAllowed; + const [deleteSupported, deleteAllowed] = rulerDeleteAbility; + const [grafanaDeleteSupported, grafanaDeleteAllowed] = grafanaDeleteAbility; + const canDelete = (deleteSupported && deleteAllowed) || (grafanaDeleteSupported && grafanaDeleteAllowed); - const [exportSupported, exportAllowed] = useRulerRuleAbility( - rulerRule, - groupIdentifier, - AlertRuleAction.ModifyExport - ); - const canExport = exportSupported && exportAllowed; + const [duplicateSupported, duplicateAllowed] = rulerDuplicateAbility; + const [grafanaDuplicateSupported, grafanaDuplicateAllowed] = grafanaDuplicateAbility; + const canDuplicate = + (duplicateSupported && duplicateAllowed) || (grafanaDuplicateSupported && grafanaDuplicateAllowed); + + const [silenceSupported, silenceAllowed] = rulerSilenceAbility; + const [grafanaSilenceSupported, grafanaSilenceAllowed] = grafanaSilenceAbility; + const canSilence = (silenceSupported && silenceAllowed) || (grafanaSilenceSupported && grafanaSilenceAllowed); + + const [exportSupported, exportAllowed] = rulerExportAbility; + const [grafanaExportSupported, grafanaExportAllowed] = grafanaExportAbility; + const canExport = (exportSupported && exportAllowed) || (grafanaExportSupported && grafanaExportAllowed); const ruleExtensionLinks = useRulePluginLinkExtension(promRule, groupIdentifier); @@ -89,10 +124,22 @@ const AlertRuleMenu = ({ const showDivider = [canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) && [canExport].some(Boolean); + // grab the UID from either rulerRule or promRule + const ruleUid = getRuleUID(rulerRule ?? promRule); + + const isPaused = + (rulerRuleType.grafana.rule(rulerRule) && isPausedRule(rulerRule)) || + (prometheusRuleType.grafana.rule(promRule) && promRule.isPaused); + const menuItems = ( <> - {canPause && rulerRuleType.grafana.rule(rulerRule) && groupIdentifier.groupOrigin === 'grafana' && ( - + {canPause && ruleUid && groupIdentifier.groupOrigin === 'grafana' && ( + )} {canSilence && ( )} - {canDelete && rulerRule && ( + {canDelete && ( <> handleDelete(rulerRule, groupIdentifier)} + onClick={() => { + // if the identifier is not for a editable rule I wonder how you even got here. + if (isEditableRuleIdentifier(identifier)) { + handleDelete(identifier, groupIdentifier); + } + }} /> )} diff --git a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx index 14b0a6b87cb..b3c9196e664 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/ContactPointLink.tsx @@ -1,7 +1,7 @@ import { ComponentProps } from 'react'; import Skeleton from 'react-loading-skeleton'; -import { alertingAPIv0alpha1 } from '@grafana/alerting/unstable'; +import { alertingAPI } from '@grafana/alerting/unstable'; import { TextLink } from '@grafana/ui'; import { makeEditContactPointLink } from '../../utils/misc'; @@ -12,7 +12,7 @@ interface ContactPointLinkProps extends Omit, 'h export const ContactPointLink = ({ name, ...props }: ContactPointLinkProps) => { // find receiver by name – since this is what we store in the alert rule definition - const { currentData, isLoading, isSuccess } = alertingAPIv0alpha1.endpoints.listReceiver.useQuery({ + const { currentData, isLoading, isSuccess } = alertingAPI.endpoints.listReceiver.useQuery({ fieldSelector: `spec.title=${name}`, }); diff --git a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx index e6b3f93bde4..a26a9709d38 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx @@ -82,7 +82,7 @@ export const useDeleteModal = (redirectToListView = false): DeleteModalHook => { 'Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?' ) } - confirmText="Yes, delete" + confirmText={t('alerting.use-delete-modal.modal.confirmText-yes-delete', 'Yes, delete')} icon="exclamation-triangle" onConfirm={deleteRule} onDismiss={dismissModal} 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 a3125ccca53..cf642620c14 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 @@ -5,7 +5,7 @@ import { byLabelText, byRole, byText } from 'testing-library-selector'; import { setPluginLinksHook } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; import { 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 5a4b4da2178..7a3f89a2526 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -40,10 +40,10 @@ import { useRuleGroupConsistencyCheck } from '../../hooks/usePrometheusConsisten import { useReturnTo } from '../../hooks/useReturnTo'; import { PluginOriginBadge } from '../../plugins/PluginOriginBadge'; import { Annotation } from '../../utils/constants'; -import { ruleIdentifierToRuleSourceIdentifier } from '../../utils/datasource'; +import { getRulesSourceUid, ruleIdentifierToRuleSourceIdentifier } from '../../utils/datasource'; import { labelsSize } from '../../utils/labels'; import { makeDashboardLink, makePanelLink, stringifyErrorLike } from '../../utils/misc'; -import { createListFilterLink } from '../../utils/navigation'; +import { createListFilterLink, groups } from '../../utils/navigation'; import { RulePluginOrigin, getRulePluginOrigin, @@ -422,6 +422,11 @@ function usePageNav(rule: CombinedRule) { const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(rulerRule); const isRecordingRuleType = prometheusRuleType.recordingRule(promRule); + const dataSourceUID = getRulesSourceUid(rule.namespace.rulesSource); + const namespaceString = getNamespaceString(rule); + + const groupDetailsUrl = groups.detailsPageLink(dataSourceUID, namespaceString, groupName); + const pageNav: NavModelItem = { ...defaultPageNav, text: rule.name, @@ -470,10 +475,7 @@ function usePageNav(rule: CombinedRule) { ], parentItem: { text: groupName, - url: createListFilterLink([ - ['namespace', namespaceName], - ['group', groupName], - ]), + url: groupDetailsUrl, // @TODO support nested folders here parentItem: { text: namespaceName, @@ -523,4 +525,33 @@ function isValidRunbookURL(url: string) { return isRelative || isAbsolute; } +function getNamespaceString(rule: CombinedRule): string { + // try rule.namespace.uid + if (rule.namespace.uid) { + return rule.namespace.uid; + } + + // if datasource managed, use rule.namespace.name; + const isDataSourceManagedRulerRule = rulerRuleType.dataSource.rule(rule.rulerRule); + const isDataSourceManagedPromRule = + prometheusRuleType.rule(rule.promRule) && !prometheusRuleType.grafana.rule(rule.promRule); + + if (isDataSourceManagedRulerRule || isDataSourceManagedPromRule) { + return rule.namespace.name; + } + + // try rulerRule definition if grafana ruler rule; + if (rulerRuleType.grafana.rule(rule.rulerRule)) { + return rule.rulerRule?.grafana_alert.namespace_uid; + } + + // try promRule definition if grafana prom rule; + if (prometheusRuleType.grafana.rule(rule.promRule)) { + return rule.promRule.folderUid; + } + + // fell back to whatever the name is of the namespace assigned to the combined rule + return rule.namespace.name; +} + export default RuleViewer; diff --git a/public/app/features/alerting/unified/components/rules/CloneRule.tsx b/public/app/features/alerting/unified/components/rules/CloneRule.tsx index 175612dde1b..a5be76cf102 100644 --- a/public/app/features/alerting/unified/components/rules/CloneRule.tsx +++ b/public/app/features/alerting/unified/components/rules/CloneRule.tsx @@ -56,7 +56,7 @@ export function RedirectToCloneRule({

} - confirmText="Copy" + confirmText={t('alerting.redirect-to-clone-rule.confirmText-copy', 'Copy')} onConfirm={() => setStage('redirect')} onDismiss={onDismiss} /> diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index 61dc5f119e7..ca70ddaa984 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; import { diff --git a/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx b/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx index a12cc9aae21..2d30594b2e2 100644 --- a/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx +++ b/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx @@ -177,7 +177,10 @@ export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) = openMenuOnFocus={openMenuOnFocus} maxMenuHeight={500} placeholder={placeholder} - noOptionsMessage="No datasources found" + noOptionsMessage={t( + 'alerting.multiple-data-source-picker.noOptionsMessage-no-datasources-found', + 'No datasources found' + )} value={value ?? []} invalid={Boolean(state?.error) || Boolean(props.invalid)} getOptionLabel={(o) => { diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx index 57725b1975b..5de673ccb14 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx @@ -15,7 +15,7 @@ import { mockPromAlertingRule, } from 'app/features/alerting/unified/mocks'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx index 9e4d8c25a46..c6bcd640657 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.tsx @@ -1,3 +1,4 @@ +import { isString } from 'lodash'; import { useState } from 'react'; import { Trans, t } from '@grafana/i18n'; @@ -7,7 +8,7 @@ import { useDeleteModal } from 'app/features/alerting/unified/components/rule-vi import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails'; import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer'; import { useRulesFilter } from 'app/features/alerting/unified/hooks/useFilteredRules'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting'; import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; @@ -16,7 +17,7 @@ import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datas import { groupIdentifier } from '../../utils/groupIdentifier'; import { createViewLink } from '../../utils/misc'; import * as ruleId from '../../utils/rule-id'; -import { rulerRuleType } from '../../utils/rules'; +import { getRuleUID, prometheusRuleType, rulerRuleType } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; import { RedirectToCloneRule } from './CloneRule'; @@ -104,6 +105,12 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource return null; } + // determine if this rule can be silenced by checking for Grafana Alert rule type and extracting the UID + const ruleUid = getRuleUID(rule.rulerRule ?? rule.promRule); + const silenceableRule = + isString(ruleUid) && + (rulerRuleType.grafana.alertingRule(rule.rulerRule) || prometheusRuleType.grafana.alertingRule(rule.promRule)); + return ( {buttons} @@ -132,8 +139,8 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource buttonSize={buttonSize} /> {deleteModal} - {rulerRuleType.grafana.alertingRule(rule.rulerRule) && showSilenceDrawer && ( - setShowSilenceDrawer(false)} /> + {silenceableRule && showSilenceDrawer && ( + setShowSilenceDrawer(false)} /> )} {redirectToClone?.identifier && ( { jest.clearAllMocks(); }); @@ -81,7 +84,7 @@ describe('RuleDetails RBAC', () => { }); describe('Cloud rules action buttons', () => { - const cloudRule = getCloudRule({ name: 'Cloud' }); + const cloudRule = getCloudRule({ name: 'Cloud' }, { rulesSource: mimirDs }); it('Should not render Edit button for users with the update permission', async () => { // Arrange 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 2d770e93fff..19100a72600 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 { byRole } from 'testing-library-selector'; import { setPluginLinksHook } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; import * as analytics from '../../Analytics'; diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx index 67e5486f9f8..fca35aa7dc4 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from 'test/test-utils'; import { byRole } from 'testing-library-selector'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRuleGroup, CombinedRuleNamespace, RulerDataSourceConfig } from 'app/types/unified-alerting'; import * as analytics from '../../Analytics'; 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 22cb1bb8b7c..a4be47bbf81 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -4,7 +4,14 @@ import { byRole } from 'testing-library-selector'; import { setPluginLinksHook } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { AlertRuleAction, useAlertRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities'; +import { + AlertRuleAction, + useAlertRuleAbility, + useGrafanaPromRuleAbilities, + useGrafanaPromRuleAbility, + useRulerRuleAbilities, + useRulerRuleAbility, +} from '../../hooks/useAbilities'; import { getCloudRule, getGrafanaRule } from '../../mocks'; import { mimirDataSource } from '../../mocks/server/configure'; @@ -13,11 +20,15 @@ import { RulesTable } from './RulesTable'; jest.mock('../../hooks/useAbilities'); const mocks = { - // This is a bit unfortunate, but we need to mock both abilities - // RuleActionButtons still needs to use the useAlertRuleAbility hook - // whereas AlertRuleMenu has already been refactored to use useRulerRuleAbility + // Mock the hooks that are actually used by the components: + // RuleActionsButtons uses: useAlertRuleAbility (singular) + // AlertRuleMenu uses: useRulerRuleAbilities and useGrafanaPromRuleAbilities (plural) + // We can also use useGrafanaPromRuleAbility (singular) for simpler mocking useRulerRuleAbility: jest.mocked(useRulerRuleAbility), useAlertRuleAbility: jest.mocked(useAlertRuleAbility), + useGrafanaPromRuleAbility: jest.mocked(useGrafanaPromRuleAbility), + useRulerRuleAbilities: jest.mocked(useRulerRuleAbilities), + useGrafanaPromRuleAbilities: jest.mocked(useGrafanaPromRuleAbilities), }; setPluginLinksHook(() => ({ @@ -46,18 +57,40 @@ describe('RulesTable RBAC', () => { jest.clearAllMocks(); jest.restoreAllMocks(); jest.resetAllMocks(); + + // Set up default neutral mocks for all hooks + // Singular hooks (used by RuleActionsButtons and can simplify mocking) + mocks.useAlertRuleAbility.mockReturnValue([false, false]); + mocks.useRulerRuleAbility.mockReturnValue([false, false]); + mocks.useGrafanaPromRuleAbility.mockReturnValue([false, false]); + + // Plural hooks (used by AlertRuleMenu) - need to return arrays based on input actions + mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { + return actions.map(() => [false, false]); + }); + mocks.useGrafanaPromRuleAbilities.mockImplementation((_rule, actions) => { + return actions.map(() => [false, false]); + }); }); describe('Grafana rules action buttons', () => { const grafanaRule = getGrafanaRule({ name: 'Grafana' }); it('Should not render Edit button for users without the update permission', async () => { - mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => { + // Mock the specific hooks needed for Grafana rules + // Using singular hook for simpler mocking + mocks.useAlertRuleAbility.mockImplementation((rule, action) => { return action === AlertRuleAction.Update ? [true, false] : [true, true]; }); - mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { + mocks.useGrafanaPromRuleAbility.mockImplementation((rule, action) => { return action === AlertRuleAction.Update ? [true, false] : [true, true]; }); + // Still need plural hook for AlertRuleMenu component + mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Update ? [true, false] : [true, true]; + }); + }); render(); @@ -65,11 +98,14 @@ describe('RulesTable RBAC', () => { }); it('Should not render Delete button for users without the delete permission', async () => { - mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => { + // Mock the specific hooks needed for Grafana rules + mocks.useAlertRuleAbility.mockImplementation((rule, action) => { return action === AlertRuleAction.Delete ? [true, false] : [true, true]; }); - mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { - return action === AlertRuleAction.Delete ? [true, false] : [true, true]; + mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Delete ? [true, false] : [true, true]; + }); }); render(); @@ -80,11 +116,14 @@ describe('RulesTable RBAC', () => { }); it('Should render Edit button for users with the update permission', async () => { - mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => { + // Mock the specific hooks needed for Grafana rules + mocks.useAlertRuleAbility.mockImplementation((rule, action) => { return action === AlertRuleAction.Update ? [true, true] : [false, false]; }); - mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { - return action === AlertRuleAction.Update ? [true, true] : [false, false]; + mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Update ? [true, true] : [false, false]; + }); }); render(); @@ -93,11 +132,14 @@ describe('RulesTable RBAC', () => { }); it('Should render Delete button for users with the delete permission', async () => { - mocks.useRulerRuleAbility.mockImplementation((_rule, _groupIdentifier, action) => { + // Mock the specific hooks needed for Grafana rules + mocks.useAlertRuleAbility.mockImplementation((rule, action) => { return action === AlertRuleAction.Delete ? [true, true] : [false, false]; }); - mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { - return action === AlertRuleAction.Delete ? [true, true] : [false, false]; + mocks.useGrafanaPromRuleAbilities.mockImplementation((rule, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Delete ? [true, true] : [false, false]; + }); }); render(); @@ -123,11 +165,15 @@ describe('RulesTable RBAC', () => { }; beforeEach(() => { - mocks.useRulerRuleAbility.mockImplementation(() => { - return [true, true]; + // Mock all hooks needed for the creating/deleting state tests + mocks.useRulerRuleAbility.mockImplementation(() => [true, true]); + mocks.useAlertRuleAbility.mockImplementation(() => [true, true]); + // Mock plural hooks for AlertRuleMenu + mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { + return actions.map(() => [true, true]); }); - mocks.useAlertRuleAbility.mockImplementation(() => { - return [true, true]; + mocks.useGrafanaPromRuleAbilities.mockImplementation((_rule, actions) => { + return actions.map(() => [true, true]); }); }); @@ -164,6 +210,12 @@ describe('RulesTable RBAC', () => { mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { return action === AlertRuleAction.Update ? [true, false] : [true, true]; }); + // Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken) + mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Update ? [true, false] : [true, true]; + }); + }); render(); @@ -177,6 +229,12 @@ describe('RulesTable RBAC', () => { mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { return action === AlertRuleAction.Delete ? [true, false] : [true, true]; }); + // Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken) + mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Delete ? [true, false] : [true, true]; + }); + }); render(); @@ -191,6 +249,12 @@ describe('RulesTable RBAC', () => { mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { return action === AlertRuleAction.Update ? [true, true] : [false, false]; }); + // Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken) + mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Update ? [true, true] : [false, false]; + }); + }); render(); @@ -204,6 +268,12 @@ describe('RulesTable RBAC', () => { mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { return action === AlertRuleAction.Delete ? [true, true] : [false, false]; }); + // Cloud rules only need useRulerRuleAbilities mock (useGrafanaPromRuleAbilities gets skipToken) + mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { + return actions.map((action) => { + return action === AlertRuleAction.Delete ? [true, true] : [false, false]; + }); + }); render(); diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx index 9a97530ac9a..d401413335e 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx @@ -4,7 +4,7 @@ import { render } from 'test/test-utils'; import { byRole } from 'testing-library-selector'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx index b70c903024f..c42566d7610 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx @@ -209,7 +209,7 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave 'Reset Alertmanager configuration' )} body={confirmationText} - confirmText="Yes, reset configuration" + confirmText={t('alerting.alertmanager-config.confirmText-yes-reset-configuration', 'Yes, reset configuration')} onConfirm={() => { onReset(alertmanagerName); setShowResetConfirmation(false); diff --git a/public/app/features/alerting/unified/components/settings/VersionManager.tsx b/public/app/features/alerting/unified/components/settings/VersionManager.tsx index 981af90cee4..10b09a4809d 100644 --- a/public/app/features/alerting/unified/components/settings/VersionManager.tsx +++ b/public/app/features/alerting/unified/components/settings/VersionManager.tsx @@ -242,8 +242,14 @@ const AlertmanagerConfigurationVersionManager = ({ { if (activeRestoreVersion) { restoreVersion(activeRestoreVersion); diff --git a/public/app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer.tsx b/public/app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer.tsx index 2d14088fe2b..9cceef11d22 100644 --- a/public/app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer.tsx +++ b/public/app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer.tsx @@ -4,12 +4,11 @@ import { t } from '@grafana/i18n'; import { Drawer, Stack } from '@grafana/ui'; import { SilencesEditor } from 'app/features/alerting/unified/components/silences/SilencesEditor'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; -import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; type Props = { - rulerRule: RulerGrafanaRuleDTO; + ruleUid: string; onClose: () => void; }; @@ -17,32 +16,26 @@ type Props = { * For a given Grafana managed rule, renders a drawer containing silences editor and Alertmanager selection */ const SilenceGrafanaRuleDrawer = React.memo( - ({ rulerRule, onClose }: Props) => { - const { uid } = rulerRule.grafana_alert; - - return ( - - - - - - - - ); - }, - (prevProps, nextProps) => { - return prevProps.rulerRule.grafana_alert.uid === nextProps.rulerRule.grafana_alert.uid; - } + ({ ruleUid, onClose }: Props) => ( + + + + + + + + ), + (prevProps, nextProps) => prevProps.ruleUid === nextProps.ruleUid ); export default SilenceGrafanaRuleDrawer; diff --git a/public/app/features/alerting/unified/components/templates/permissions.ts b/public/app/features/alerting/unified/components/templates/permissions.ts index a92758f1a1d..82bef452e94 100644 --- a/public/app/features/alerting/unified/components/templates/permissions.ts +++ b/public/app/features/alerting/unified/components/templates/permissions.ts @@ -1,4 +1,4 @@ -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; /** * List of all permissions that allow templates read/write functionality diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx index a1606965fd2..90485290325 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx @@ -1,13 +1,16 @@ -import { HttpResponse } from 'msw'; +import { uniqueId } from 'lodash'; +import { HttpResponse, http } from 'msw'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { Props } from 'react-virtualized-auto-sizer'; import { render, screen, waitFor, within } from 'test/test-utils'; import { byRole, byTestId } from 'testing-library-selector'; -import { AccessControlAction } from 'app/types'; +import { setPluginLinksHook } from '@grafana/runtime'; +import { AccessControlAction } from 'app/types/accessControl'; +import { GrafanaPromRuleGroupDTO, GrafanaPromRulesResponse } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../mockApi'; -import { grantUserPermissions, mockRulerGrafanaRule, mockRulerRuleGroup } from '../mocks'; +import { grantUserPermissions, mockGrafanaPromAlertingRule, mockRulerGrafanaRule, mockRulerRuleGroup } from '../mocks'; import { mimirDataSource, setFolderResponse, @@ -38,8 +41,7 @@ const ui = { header: byRole('heading', { level: 1 }), editLink: byRole('link', { name: 'Edit' }), exportButton: byRole('button', { name: 'Export' }), - tableRow: byTestId('row'), - rowsTable: byTestId('dynamic-table'), + ruleItem: byRole('treeitem'), export: { dialog: byRole('dialog', { name: /Drawer title Export .* rules/ }), jsonTab: byRole('tab', { name: /JSON/ }), @@ -50,10 +52,16 @@ const ui = { }, }; -setupMswServer(); +const server = setupMswServer(); describe('GroupDetailsPage', () => { beforeEach(() => { + // mock this... + setPluginLinksHook(() => ({ + links: [], + isLoading: false, + })); + grantUserPermissions([ AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleUpdate, @@ -63,25 +71,71 @@ describe('GroupDetailsPage', () => { }); describe('Grafana managed rules', () => { - const rule1 = mockRulerGrafanaRule({ for: '10m' }, { title: 'High CPU Usage' }); - const rule2 = mockRulerGrafanaRule({ for: '5m' }, { title: 'Memory Pressure' }); - const provisionedRule = mockRulerGrafanaRule({ for: '10m' }, { title: 'Provisioned Rule', provenance: 'api' }); + const folder = { uid: 'test-folder-uid', canSave: true, title: 'test-folder-title' } as const; + + const rule1 = mockRulerGrafanaRule({ for: '10m' }, { title: 'High CPU Usage', uid: uniqueId() }); + const rule2 = mockRulerGrafanaRule({ for: '5m' }, { title: 'Memory Pressure', uid: uniqueId() }); + + const provisionedRule = mockRulerGrafanaRule( + { for: '10m' }, + { uid: uniqueId(), title: 'Provisioned Rule', provenance: 'api' } + ); const group = mockRulerRuleGroup({ name: 'test-group-cpu', interval: '3m', rules: [rule1, rule2], }); + const promGroup: GrafanaPromRuleGroupDTO = { + name: group.name, + rules: [ + mockGrafanaPromAlertingRule({ + uid: rule1.grafana_alert.uid, + name: rule1.grafana_alert.title, + }), + mockGrafanaPromAlertingRule({ + uid: rule2.grafana_alert.uid, + name: rule2.grafana_alert.title, + }), + ], + interval: 180, + folderUid: folder.uid, + file: folder.title, + }; const provisionedGroup = mockRulerRuleGroup({ name: 'provisioned-group-cpu', interval: '15m', rules: [provisionedRule], }); + const promProvisionedGroup: GrafanaPromRuleGroupDTO = { + name: provisionedGroup.name, + interval: 900, + rules: [ + mockGrafanaPromAlertingRule({ + uid: provisionedRule.grafana_alert.uid, + name: provisionedRule.grafana_alert.title, + provenance: provisionedRule.grafana_alert.provenance, + }), + ], + folderUid: folder.uid, + file: folder.title, + }; beforeEach(() => { setRulerRuleGroupHandler({ response: HttpResponse.json(group) }); - setFolderResponse({ uid: 'test-folder-uid', canSave: true, title: 'test-folder-title' }); + server.use( + http.get('/api/prometheus/grafana/api/v1/rules', () => + HttpResponse.json({ + status: 'success', + data: { + groups: [promGroup], + }, + }) + ) + ); + + setFolderResponse(folder); setGrafanaRuleGroupExportResolver(({ request }) => { const url = new URL(request.url); return HttpResponse.text( @@ -106,22 +160,24 @@ describe('GroupDetailsPage', () => { '/alerting/grafana/namespaces/test-folder-uid/groups/test-group-cpu/edit?returnTo=%2Falerting%2Fgrafana%2Fnamespaces%2Ftest-folder-uid%2Fgroups%2Ftest-group-cpu%2Fview' ); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); - expect(tableRows).toHaveLength(2); + expect(await screen.findByRole('treeitem', { name: rule1.grafana_alert.title })).toBeInTheDocument(); - expect(within(tableRows[0]).getByRole('link', { name: rule1.grafana_alert.title })).toHaveAttribute( + const alertRuleItems = await ui.ruleItem.findAll(); + expect(alertRuleItems).toHaveLength(2); + + // assert rule 1 + expect(within(alertRuleItems[0]).getByRole('link', { name: rule1.grafana_alert.title })).toHaveAttribute( 'href', `/alerting/grafana/${rule1.grafana_alert.uid}/view` ); - expect(tableRows[0]).toHaveTextContent(String(rule1.for)); - expect(tableRows[0]).toHaveTextContent('5'); + expect(alertRuleItems[0]).toHaveTextContent(rule1.grafana_alert.title); - expect(within(tableRows[1]).getByRole('link', { name: rule2.grafana_alert.title })).toHaveAttribute( + // assert rule 2 + expect(within(alertRuleItems[1]).getByRole('link', { name: rule2.grafana_alert.title })).toHaveAttribute( 'href', `/alerting/grafana/${rule2.grafana_alert.uid}/view` ); - expect(tableRows[1]).toHaveTextContent(String(rule2.for)); - expect(tableRows[1]).toHaveTextContent('3'); + expect(alertRuleItems[1]).toHaveTextContent(rule2.grafana_alert.title); }); it('should render error alert when API returns an error', async () => { @@ -164,10 +220,12 @@ describe('GroupDetailsPage', () => { // Act renderGroupDetailsPage('grafana', 'test-folder-uid', group.name); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); + // Wait until rule items are rendered + expect(await screen.findByRole('treeitem', { name: rule1.grafana_alert.title })).toBeInTheDocument(); + const alertRuleItems = ui.ruleItem.getAll(); // Assert - expect(tableRows).toHaveLength(2); + expect(alertRuleItems).toHaveLength(2); expect(ui.editLink.query()).not.toBeInTheDocument(); // Edit button should not be present }); @@ -177,24 +235,39 @@ describe('GroupDetailsPage', () => { // Act renderGroupDetailsPage('grafana', 'test-folder-uid', group.name); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); + // wait for rule items to render + expect(await screen.findByRole('treeitem', { name: rule1.grafana_alert.title })).toBeInTheDocument(); + + const alertRuleItems = await ui.ruleItem.findAll(); // Assert - expect(tableRows).toHaveLength(2); + expect(alertRuleItems).toHaveLength(2); expect(ui.editLink.query()).not.toBeInTheDocument(); // Edit button should not be present }); it('should not allow editing if the group is provisioned', async () => { setRulerRuleGroupHandler({ response: HttpResponse.json(provisionedGroup) }); + server.use( + http.get('/api/prometheus/grafana/api/v1/rules', () => + HttpResponse.json({ + status: 'success', + data: { + groups: [promProvisionedGroup], + }, + }) + ) + ); // Act renderGroupDetailsPage('grafana', 'test-folder-uid', provisionedGroup.name); + // wait for rule items to render + expect(await screen.findByRole('treeitem', { name: provisionedRule.grafana_alert.title })).toBeInTheDocument(); - const tableRows = await ui.tableRow.findAll(await ui.rowsTable.find()); + const alertRuleItems = await ui.ruleItem.findAll(); // Assert - expect(tableRows).toHaveLength(1); - expect(tableRows[0]).toHaveTextContent('Provisioned Rule'); + expect(alertRuleItems).toHaveLength(1); + expect(alertRuleItems[0]).toHaveTextContent('Provisioned Rule'); expect(ui.editLink.query()).not.toBeInTheDocument(); expect(ui.exportButton.query()).toBeInTheDocument(); }); diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx index f05e9c1a19d..997b1bd44b7 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.tsx @@ -1,35 +1,28 @@ import { skipToken } from '@reduxjs/toolkit/query'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Badge, Button, LinkButton, Text, TextLink, withErrorBoundary } from '@grafana/ui'; +import { Alert, Button, Dropdown, Icon, LinkButton, Menu, TextLink, withErrorBoundary } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; -import { FolderDTO } from 'app/types'; -import { GrafanaRulesSourceSymbol, RuleGroup } from 'app/types/unified-alerting'; -import { PromRuleType, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; +import { FolderDTO } from 'app/types/folders'; +import { GrafanaRulesSourceSymbol } from 'app/types/unified-alerting'; +import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../api/alertRuleApi'; import { RulesSourceFeatures, featureDiscoveryApi } from '../api/featureDiscoveryApi'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; -import { DynamicTable, DynamicTableColumnProps } from '../components/DynamicTable'; import { GrafanaRuleGroupExporter } from '../components/export/GrafanaRuleGroupExporter'; import { useFolder } from '../hooks/useFolder'; import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../rule-editor/formDefaults'; -import { createViewLinkFromIdentifier } from '../rule-list/DataSourceRuleListItem'; +import { DataSourceGroupLoader } from '../rule-list/DataSourceGroupLoader'; +import { GrafanaGroupLoader } from '../rule-list/GrafanaGroupLoader'; import { useRulesAccess } from '../utils/accessControlHooks'; import { GRAFANA_RULES_SOURCE_NAME, getDataSourceByUid } from '../utils/datasource'; import { makeFolderLink, stringifyErrorLike } from '../utils/misc'; import { createListFilterLink, groups } from '../utils/navigation'; -import { fromRule, fromRulerRule } from '../utils/rule-id'; -import { - calcRuleEvalsToStartAlerting, - getRuleName, - isFederatedRuleGroup, - isProvisionedRuleGroup, - rulerRuleType, -} from '../utils/rules'; -import { formatPrometheusDuration, safeParsePrometheusDuration } from '../utils/time'; +import { isFederatedRuleGroup, isProvisionedRuleGroup } from '../utils/rules'; +import { formatPrometheusDuration } from '../utils/time'; import { Title } from './Title'; @@ -111,6 +104,7 @@ function GroupDetailsPage() { }, }} renderTitle={(title) => } + subTitle={t('alerting.titles.group-view.subtitle', 'Manage alert rules, recording rules and evaluation interval')} info={[ { label: namespaceLabel, value: namespaceValue }, { label: t('alerting.group-details.interval', 'Interval'), value: groupInterval }, @@ -150,13 +144,33 @@ function GroupDetailsPage() { <div>{stringifyErrorLike(ruleNamespacesError || ruleGroupError)}</div> </Alert> )} - {promGroup && ruleSourceName && ( - <GroupDetails group={promRuleGroupToRuleGroupDetails(ruleSourceName, namespaceName, promGroup)} /> - )} - {rulerGroup && ruleSourceName && ( - <GroupDetails group={rulerRuleGroupToRuleGroupDetails(ruleSourceName, namespaceName, rulerGroup)} /> - )} {!promGroup && !rulerGroup && <EntityNotFound entity={`${namespaceId}/${groupName}`} />} + + {ruleSourceName && ( + <ul role="tree"> + {isGrafanaRuleGroup ? ( + <GrafanaGroupLoader + groupIdentifier={{ groupName, groupOrigin: 'grafana', namespace: { uid: namespaceId } }} + namespaceName={namespaceName} + /> + ) : ( + <DataSourceGroupLoader + groupIdentifier={{ + groupName, + groupOrigin: 'datasource', + namespace: { + name: namespaceName, + }, + rulesSource: { + name: ruleSourceName, + uid: dataSourceUid, + ruleSourceType: 'datasource', + }, + }} + /> + )} + </ul> + )} </> </AlertingPageWrapper> ); @@ -195,13 +209,39 @@ function GroupActions({ dsFeatures, namespaceId, groupName, folder, rulerGroup } </Button> )} {canEdit && ( - <LinkButton - icon="pen" - href={groups.editPageLink(dsFeatures.uid, namespaceId, groupName, { includeReturnTo: true })} - variant="secondary" - > - <Trans i18nKey="alerting.group-details.edit">Edit</Trans> - </LinkButton> + <> + <LinkButton + icon="pen" + href={groups.editPageLink(dsFeatures.uid, namespaceId, groupName, { includeReturnTo: true })} + variant="secondary" + > + <Trans i18nKey="alerting.group-details.edit">Edit</Trans> + </LinkButton> + {/* Data source managed requires different URLs, a hassle to implement for now */} + {isGrafanaSource && ( + <Dropdown + overlay={ + <Menu> + <Menu.Item + icon="bell" + url={groups.newAlertRuleLink(folder?.title, folder?.uid, groupName)} + label={t('alerting.alert-rule.term', 'Alert rule')} + /> + <Menu.Item + icon="record-audio" + url={groups.newRecordingRuleLink(folder?.title, folder?.uid, groupName)} + label={t('alerting.recording-rule.term', 'Recording rule')} + /> + </Menu> + } + > + <Button variant="primary"> + {t('alerting.group-details.new', 'New')} + <Icon name="angle-down" /> + </Button> + </Dropdown> + )} + </> )} {folder && isExporting && ( <GrafanaRuleGroupExporter folderUid={folder.uid} groupName={groupName} onClose={() => setIsExporting(false)} /> @@ -210,158 +250,4 @@ function GroupActions({ dsFeatures, namespaceId, groupName, folder, rulerGroup } ); } -/** An common interface for both Prometheus and Ruler rule groups */ -interface RuleGroupDetails { - name: string; - interval: string; - rules: RuleDetails[]; -} - -interface AlertingRuleDetails { - name: string; - href?: string; - type: 'alerting'; - pendingPeriod: string; - evaluationsToFire: number; -} -interface RecordingRuleDetails { - name: string; - href?: string; - type: 'recording'; -} - -type RuleDetails = AlertingRuleDetails | RecordingRuleDetails; - -interface GroupDetailsProps { - group: RuleGroupDetails; -} - -function GroupDetails({ group }: GroupDetailsProps) { - return ( - <div> - <RulesTable rules={group.rules} /> - </div> - ); -} - -function RulesTable({ rules }: { rules: RuleDetails[] }) { - const rows = rules.map((rule: RuleDetails, index) => ({ - id: index, - data: rule, - })); - - const columns: Array<DynamicTableColumnProps<RuleDetails>> = useMemo(() => { - return [ - { - id: 'alertName', - label: t('alerting.group-details.rule-name', 'Rule name'), - renderCell: ({ data: { name, href } }) => { - if (href) { - return ( - <TextLink href={href} inline={false} color="primary"> - {name} - </TextLink> - ); - } - - return <Text truncate>{name}</Text>; - }, - size: 0.4, - }, - { - id: 'for', - label: t('alerting.group-details.pending-period', 'Pending period'), - renderCell: ({ data }) => { - switch (data.type) { - case 'alerting': - return <>{data.pendingPeriod}</>; - case 'recording': - return <Badge text={t('alerting.group-details.recording', 'Recording')} color="purple" />; - } - }, - size: 0.3, - }, - { - id: 'numberEvaluations', - label: t('alerting.group-details.evaluations-to-fire', 'Evaluation cycles to fire'), - renderCell: ({ data }) => { - switch (data.type) { - case 'alerting': - return <>{data.evaluationsToFire}</>; - case 'recording': - return null; - } - }, - size: 0.3, - }, - ]; - }, []); - - return <DynamicTable items={rows} cols={columns} />; -} - -function promRuleGroupToRuleGroupDetails( - ruleSourceName: string, - namespaceName: string, - group: RuleGroup -): RuleGroupDetails { - const groupIntervalMs = group.interval * 1000; - - return { - name: group.name, - interval: formatPrometheusDuration(group.interval * 1000), - rules: group.rules.map<RuleDetails>((rule) => { - const ruleIdentifier = fromRule(ruleSourceName, namespaceName, group.name, rule); - const href = ruleIdentifier ? createViewLinkFromIdentifier(ruleIdentifier) : undefined; - - switch (rule.type) { - case PromRuleType.Alerting: - return { - name: rule.name, - href, - type: 'alerting', - pendingPeriod: formatPrometheusDuration(rule.duration ? rule.duration * 1000 : 0), - evaluationsToFire: calcRuleEvalsToStartAlerting(rule.duration ? rule.duration * 1000 : 0, groupIntervalMs), - }; - case PromRuleType.Recording: - return { name: rule.name, href, type: 'recording' }; - } - }), - }; -} - -function rulerRuleGroupToRuleGroupDetails( - ruleSourceName: string, - namespaceName: string, - group: RulerRuleGroupDTO -): RuleGroupDetails { - const groupIntervalMs = safeParsePrometheusDuration(group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL); - - return { - name: group.name, - interval: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL, - rules: group.rules.map<RuleDetails>((rule) => { - const name = getRuleName(rule); - - const ruleIdentifier = fromRulerRule(ruleSourceName, namespaceName, group.name, rule); - const href = createViewLinkFromIdentifier(ruleIdentifier); - - if (rulerRuleType.any.alertingRule(rule)) { - return { - name, - href, - type: 'alerting', - pendingPeriod: rule.for ?? '0s', - evaluationsToFire: calcRuleEvalsToStartAlerting( - rule.for ? safeParsePrometheusDuration(rule.for) : 0, - groupIntervalMs - ), - }; - } - - return { name, href, type: 'recording' }; - }), - }; -} - export default withErrorBoundary(GroupDetailsPage, { style: 'page' }); diff --git a/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx index 2240d26dc55..bafd72ddedd 100644 --- a/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupEditPage.test.tsx @@ -5,7 +5,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { locationService } from '@grafana/runtime'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../mockApi'; diff --git a/public/app/features/alerting/unified/group-details/GroupEditPage.tsx b/public/app/features/alerting/unified/group-details/GroupEditPage.tsx index 1a2ece20bc4..a68f230d8cb 100644 --- a/public/app/features/alerting/unified/group-details/GroupEditPage.tsx +++ b/public/app/features/alerting/unified/group-details/GroupEditPage.tsx @@ -20,7 +20,7 @@ import { } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { GrafanaRulesSourceSymbol, RuleGroupIdentifierV2, RulerDataSourceConfig } from 'app/types/unified-alerting'; import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/home/AdCard.tsx b/public/app/features/alerting/unified/home/AdCard.tsx new file mode 100644 index 00000000000..01577b83150 --- /dev/null +++ b/public/app/features/alerting/unified/home/AdCard.tsx @@ -0,0 +1,146 @@ +import { css } from '@emotion/css'; +import { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { Button, Divider, Icon, IconButton, useStyles2 } from '@grafana/ui'; +import { CloudBadge } from 'app/core/components/Branding/CloudBadge'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { contextSrv } from 'app/core/services/context_srv'; +import { isOpenSourceBuildOrUnlicenced } from 'app/features/admin/EnterpriseAuthFeaturesCard'; + +type AdCardProps = { + title: string; + description: string; + href: string; + logoUrl: string; + items: string[]; + helpFlag: number; +}; + +export default function AdCard({ title, description, href, logoUrl, items, helpFlag }: AdCardProps) { + const styles = useStyles2(getAddCardStyles); + + const helpFlags = contextSrv.user.helpFlags1; + const [isDismissed, setDismissed] = useState<boolean>(Boolean(helpFlags & helpFlag)); + + const onDismiss = () => { + backendSrv.put(`/api/user/helpflags/${helpFlag}`, undefined, { showSuccessAlert: false }).then((res) => { + contextSrv.user.helpFlags1 = res.helpFlags1; + setDismissed(true); + }); + }; + + if (isDismissed || !isOpenSourceBuildOrUnlicenced()) { + return null; + } + + return ( + <div className={styles.cardBody} title={title}> + <div className={styles.preHeader}> + <CloudBadge /> + <IconButton name="times" size="sm" onClick={onDismiss} aria-label={t('alerting.ad.close', 'Close')} /> + </div> + <header className={styles.header}> + <img src={logoUrl} alt={title.concat(' logo')} className={styles.logo} /> + <div className={styles.contentColumn}> + <h3 className={styles.title}>{title}</h3> + <p className={styles.description}>{description}</p> + </div> + </header> + <Divider /> + <div className={styles.itemsList}> + {items.map((item) => ( + <div key={item} className={styles.listItem}> + <Icon className={styles.icon} name="check" /> + {item} + </div> + ))} + </div> + <Divider /> + <Button fill="solid" variant="secondary" onClick={() => window.open(href, '_blank')} className={styles.button}> + <Trans i18nKey="alerting.ad.learn-more">Learn more</Trans> + <Icon name="external-link-alt" className={styles.buttonIcon} /> + </Button> + </div> + ); +} + +const getAddCardStyles = (theme: GrafanaTheme2) => ({ + logo: css({ + objectFit: 'contain', + width: '47px', + height: '47px', + }), + + header: css({ + display: 'flex', + alignItems: 'flex-start', + gap: theme.spacing(2), + paddingTop: theme.spacing(2), + height: theme.spacing(8), + }), + + contentColumn: css({ + flex: 1, + }), + + title: css({ + marginBottom: theme.spacing(1), + fontSize: theme.typography.h4.fontSize, + fontWeight: theme.typography.h4.fontWeight, + color: theme.colors.text.primary, + }), + + description: css({ + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + lineHeight: theme.typography.bodySmall.lineHeight, + }), + + itemsList: css({ + display: 'grid', + gridTemplateColumns: '1fr', + gap: theme.spacing(0.5), + [theme.breakpoints.up('xl')]: { + gridTemplateColumns: '1fr 1fr', + gap: theme.spacing(1), + }, + }), + + listItem: css({ + display: 'flex', + alignItems: 'flex-start', + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + lineHeight: theme.typography.bodySmall.lineHeight, + marginBottom: theme.spacing(0.5), + }), + + icon: css({ + marginRight: theme.spacing(1), + color: theme.colors.success.main, + }), + + button: css({ + padding: `0 ${theme.spacing(2)}`, + }), + + buttonIcon: css({ + marginLeft: theme.spacing(1), + }), + + cardBody: css({ + padding: `${theme.spacing(3)} ${theme.spacing(4)} ${theme.spacing(2.25)} ${theme.spacing(4)}`, + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + border: `1px solid ${theme.colors.border.weak}`, + flex: 1, + }), + + preHeader: css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }), +}); diff --git a/public/app/features/alerting/unified/home/Home.tsx b/public/app/features/alerting/unified/home/Home.tsx index d62f6735531..449f956255c 100644 --- a/public/app/features/alerting/unified/home/Home.tsx +++ b/public/app/features/alerting/unified/home/Home.tsx @@ -8,8 +8,10 @@ import { isLocalDevEnv } from '../utils/misc'; import { withPageErrorBoundary } from '../withPageErrorBoundary'; import GettingStarted, { WelcomeHeader } from './GettingStarted'; +import IRMCard from './IRMCard'; import { getInsightsScenes, insightsIsAvailable } from './Insights'; import { PluginIntegrations } from './PluginIntegrations'; +import SyntheticMonitoringCard from './SyntheticMonitoringCard'; function Home() { const insightsEnabled = insightsIsAvailable() || isLocalDevEnv(); @@ -27,6 +29,12 @@ function Home() { <WelcomeHeader /> <PluginIntegrations /> </Stack> + <Box marginTop={{ lg: 2, md: 2, xs: 2 }}> + <Stack direction="row" gap={2}> + <SyntheticMonitoringCard /> + <IRMCard /> + </Stack> + </Box> <Box marginTop={{ lg: 2, md: 0, xs: 0 }}> <TabsBar> {insightsEnabled && ( diff --git a/public/app/features/alerting/unified/home/IRMCard.tsx b/public/app/features/alerting/unified/home/IRMCard.tsx new file mode 100644 index 00000000000..ff22c94943d --- /dev/null +++ b/public/app/features/alerting/unified/home/IRMCard.tsx @@ -0,0 +1,28 @@ +import { t } from '@grafana/i18n'; +import irmSvg from 'img/irm_logo.svg'; + +import AdCard from './AdCard'; + +const LINK = 'https://grafana.com/auth/sign-up/create-user?redirectPath=irm&src=oss-grafana&cnt=alerting-irm'; +const HELP_FLAG_IRM = 0x0010; + +export default function IRMCard() { + return ( + <AdCard + title={t('alerting.home.irm-card-title', 'Incident response and management')} + description={t( + 'alerting.home.irm-card-description', + 'Unify on-call, alerting, and incident response with Grafana Cloud IRM.' + )} + href={LINK} + logoUrl={irmSvg} + items={[ + t('alerting.home.irm-card-item-1', 'Manage on-call schedules with your calendar or Terraform.'), + t('alerting.home.irm-card-item-2', 'Respond to incidents via web, app, Slack, or other channels.'), + t('alerting.home.irm-card-item-3', 'Pinpoint root causes with AI-powered Grafana SIFT.'), + t('alerting.home.irm-card-item-4', 'Analyze past incidents to improve response and resilience.'), + ]} + helpFlag={HELP_FLAG_IRM} + /> + ); +} diff --git a/public/app/features/alerting/unified/home/SyntheticMonitoringCard.tsx b/public/app/features/alerting/unified/home/SyntheticMonitoringCard.tsx new file mode 100644 index 00000000000..c98b7066c46 --- /dev/null +++ b/public/app/features/alerting/unified/home/SyntheticMonitoringCard.tsx @@ -0,0 +1,38 @@ +import { t } from '@grafana/i18n'; +import syntheticMonitoringSvg from 'img/synthetic_monitoring_logo.svg'; + +import AdCard from './AdCard'; + +const LINK = + 'https://grafana.com/auth/sign-up/create-user?redirectPath=synthetic-monitoring&src=oss-grafana&cnt=alerting-synthetic-monitoring'; +const HELP_FLAG_SYNTHETIC_MONITORING = 0x0008; + +export default function SyntheticMonitoringCard() { + return ( + <AdCard + title={t('alerting.home.synthetic-monitoring-card-title', 'Synthetic Monitoring')} + description={t( + 'alerting.home.synthetic-monitoring-card-description', + 'Monitor critical user flows, websites, and APIs externally, from global locations.' + )} + href={LINK} + logoUrl={syntheticMonitoringSvg} + items={[ + t('alerting.home.synthetic-monitoring-card-item-1', 'Simulate end-to-end user journeys with browser checks.'), + t( + 'alerting.home.synthetic-monitoring-card-item-2', + 'Run ping, DNS, HTTP/S, and TCP checks at every network layer.' + ), + t( + 'alerting.home.synthetic-monitoring-card-item-3', + 'Use 20+ global probes or private probes behind your firewall.' + ), + t( + 'alerting.home.synthetic-monitoring-card-item-4', + 'Track SLOs with built-in Prometheus-style alerts — right from the UI.' + ), + ]} + helpFlag={HELP_FLAG_SYNTHETIC_MONITORING} + /> + ); +} diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx index 538d02d1a38..552c401b6d0 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useDeleteRuleFromGroup.test.tsx @@ -3,7 +3,7 @@ import { render } from 'test/test-utils'; import { byRole, byText } from 'testing-library-selector'; import server from '@grafana/test-utils/server'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule } from 'app/types/unified-alerting'; import { setupMswServer } from '../../mockApi'; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx index 1b7f1f89db3..df57efb9ac5 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx @@ -3,7 +3,7 @@ import { HttpResponse } from 'msw'; import { render } from 'test/test-utils'; import { byRole, byText } from 'testing-library-selector'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../../mockApi'; diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx index 4d5fa6d556f..e74178297fb 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/useUpdateRuleGroup.test.tsx @@ -1,7 +1,7 @@ import { render } from 'test/test-utils'; import { byRole, byText } from 'testing-library-selector'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleGroupIdentifier } from 'app/types/unified-alerting'; import { setupMswServer } from '../../mockApi'; diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx index a7b550e0b68..3fdd05e6c63 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx @@ -5,7 +5,7 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule } from 'app/types/unified-alerting'; import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks'; diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index c2e96434d89..5dd43d6db63 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -12,17 +12,22 @@ import { } from 'app/features/alerting/unified/components/notification-policies/permissions'; import { useFolder } from 'app/features/alerting/unified/hooks/useFolder'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule, RuleGroupIdentifierV2 } from 'app/types/unified-alerting'; -import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import { GrafanaPromRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { alertmanagerApi } from '../api/alertmanagerApi'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control'; -import { getRulesSourceName } from '../utils/datasource'; -import { getGroupOriginName } from '../utils/groupIdentifier'; +import { getGroupOriginName, groupIdentifier } from '../utils/groupIdentifier'; import { isAdmin } from '../utils/misc'; -import { isFederatedRuleGroup, isPluginProvidedRule, rulerRuleType } from '../utils/rules'; +import { + isPluginProvidedRule, + isProvisionedPromRule, + isProvisionedRule, + prometheusRuleType, + rulerRuleType, +} from '../utils/rules'; import { useIsRuleEditable } from './useIsRuleEditable'; @@ -200,7 +205,7 @@ export function useRulerRuleAbility( } export function useRulerRuleAbilities( - rule: RulerRuleDTO, + rule: RulerRuleDTO | undefined, groupIdentifier: RuleGroupIdentifierV2, actions: AlertRuleAction[] ): Ability[] { @@ -211,61 +216,16 @@ export function useRulerRuleAbilities( }, [abilities, actions]); } -// This hook is being called a lot in different places -// In some cases multiple times for ~80 rules (e.g. on the list page) -// We need to investigate further if some of these calls are redundant -// In the meantime, memoizing the result helps +/** + * @deprecated Use {@link useAllRulerRuleAbilities} instead + */ export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRuleAction> { - const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource); - - const { - isEditable, - isRemovable, - isRulerAvailable = false, - loading, - } = useIsRuleEditable(rulesSourceName, rule.rulerRule); - const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules); - const canSilence = useCanSilence(rule.rulerRule); - - const abilities = useMemo<Abilities<AlertRuleAction>>(() => { - const isProvisioned = - rulerRuleType.grafana.rule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); - const isFederated = isFederatedRuleGroup(rule.group); - const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule.rulerRule); - const isPluginProvided = isPluginProvidedRule(rule.rulerRule); - - // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited - const immutableRule = isProvisioned || isFederated || isPluginProvided; - - // while we gather info, pretend it's not supported - const MaybeSupported = loading ? NotSupported : isRulerAvailable; - const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported; - - // Creating duplicates of plugin-provided rules does not seem to make a lot of sense - const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported; - - const rulesPermissions = getRulesPermissions(rulesSourceName); - - const abilities: Abilities<AlertRuleAction> = { - [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], - [AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], - [AlertRuleAction.DeletePermanently]: [ - MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, - (isRemovable && isAdmin()) ?? false, - ], - }; - - return abilities; - }, [rule, loading, isRulerAvailable, isEditable, isRemovable, rulesSourceName, exportAllowed, canSilence]); - - return abilities; + // This hook is being called a lot in different places + // In some cases multiple times for ~80 rules (e.g. on the list page) + // We need to investigate further if some of these calls are redundant + // In the meantime, memoizing the result helps + const groupIdentifierV2 = useMemo(() => groupIdentifier.fromCombinedRule(rule), [rule]); + return useAllRulerRuleAbilities(rule.rulerRule, groupIdentifierV2); } export function useAllRulerRuleAbilities( @@ -279,7 +239,8 @@ export function useAllRulerRuleAbilities( const canSilence = useCanSilence(rule); const abilities = useMemo<Abilities<AlertRuleAction>>(() => { - const isProvisioned = rulerRuleType.grafana.rule(rule) && Boolean(rule.grafana_alert.provenance); + const isProvisioned = rule ? isProvisionedRule(rule) : false; + // TODO: Add support for federated rules // const isFederated = isFederatedRuleGroup(); const isFederated = false; const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule); @@ -319,6 +280,131 @@ export function useAllRulerRuleAbilities( return abilities; } +/** + * Hook for checking abilities on Grafana Prometheus rules (GrafanaPromRuleDTO) + * This is the next version of useAllRulerRuleAbilities designed to work with GrafanaPromRuleDTO + */ +export function useAllGrafanaPromRuleAbilities(rule: GrafanaPromRuleDTO | undefined): Abilities<AlertRuleAction> { + // For GrafanaPromRuleDTO, we use useIsGrafanaPromRuleEditable instead + const { isEditable, isRemovable, loading } = useIsGrafanaPromRuleEditable(rule); // duplicate + const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules); + + const silenceSupported = useGrafanaRulesSilenceSupport(); + const canSilenceInFolder = useCanSilenceInFolder(rule?.folderUid); + + const abilities = useMemo<Abilities<AlertRuleAction>>(() => { + const isProvisioned = rule ? isProvisionedPromRule(rule) : false; + + // Note: Grafana managed rules can't be federated - this is strictly a Mimir feature + // See: https://grafana.com/docs/mimir/latest/references/architecture/components/ruler/#federated-rule-groups + const isFederated = false; + // All GrafanaPromRuleDTO rules are Grafana-managed by definition + const isAlertingRule = prometheusRuleType.grafana.alertingRule(rule); + const isPluginProvided = isPluginProvidedRule(rule); + + // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited + const immutableRule = isProvisioned || isFederated || isPluginProvided; + + // GrafanaPromRuleDTO rules are always supported (no loading state for ruler availability) + const MaybeSupported = loading ? NotSupported : AlwaysSupported; + const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported; + + // Creating duplicates of plugin-provided rules does not seem to make a lot of sense + const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported; + + const rulesPermissions = getRulesPermissions('grafana'); + + const abilities: Abilities<AlertRuleAction> = { + [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]: [silenceSupported, canSilenceInFolder && isAlertingRule], + [AlertRuleAction.ModifyExport]: [isAlertingRule, exportAllowed], + [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isAlertingRule, isEditable ?? false], + [AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isAlertingRule, isEditable ?? false], + [AlertRuleAction.DeletePermanently]: [ + MaybeSupportedUnlessImmutable && isAlertingRule, + (isRemovable && isAdmin()) ?? false, + ], + }; + + return abilities; + }, [rule, loading, isEditable, isRemovable, canSilenceInFolder, exportAllowed, silenceSupported]); + + return abilities; +} + +interface IsGrafanaPromRuleEditableResult { + isEditable: boolean; + isRemovable: boolean; + loading: boolean; +} + +/** + * Hook for checking if a GrafanaPromRuleDTO is editable + * Adapted version of useIsRuleEditable for GrafanaPromRuleDTO + */ +function useIsGrafanaPromRuleEditable(rule?: GrafanaPromRuleDTO): IsGrafanaPromRuleEditableResult { + const folderUID = rule?.folderUid; + const { folder, loading } = useFolder(folderUID); + + return useMemo(() => { + if (!rule || !folderUID) { + return { isEditable: false, isRemovable: false, loading: false }; + } + + if (!folder) { + // Loading or invalid folder UID + return { + isEditable: false, + isRemovable: false, + loading, + }; + } + + // For Grafana-managed rules, check folder permissions + const rulesPermissions = getRulesPermissions('grafana'); + const canEditGrafanaRules = ctx.hasPermissionInMetadata(rulesPermissions.update, folder); + const canRemoveGrafanaRules = ctx.hasPermissionInMetadata(rulesPermissions.delete, folder); + + return { + isEditable: canEditGrafanaRules, + isRemovable: canRemoveGrafanaRules, + loading, + }; + }, [rule, folderUID, folder, loading]); +} + +export const skipToken = Symbol('ability-skip-token'); +type SkipToken = typeof skipToken; + +/** + * Hook for checking a single ability on a GrafanaPromRuleDTO + */ +export function useGrafanaPromRuleAbility(rule: GrafanaPromRuleDTO | SkipToken, action: AlertRuleAction): Ability { + const abilities = useAllGrafanaPromRuleAbilities(rule === skipToken ? undefined : rule); + + return useMemo(() => { + return abilities[action]; + }, [abilities, action]); +} + +/** + * Hook for checking multiple abilities on a GrafanaPromRuleDTO + */ +export function useGrafanaPromRuleAbilities( + rule: GrafanaPromRuleDTO | SkipToken, + actions: AlertRuleAction[] +): Ability[] { + const abilities = useAllGrafanaPromRuleAbilities(rule === skipToken ? undefined : rule); + + return useMemo(() => { + return actions.map((action) => abilities[action]); + }, [abilities, actions]); +} + export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> { const { selectedAlertmanager, diff --git a/public/app/features/alerting/unified/hooks/useFolder.ts b/public/app/features/alerting/unified/hooks/useFolder.ts index 7b06bf92020..804c9c12b25 100644 --- a/public/app/features/alerting/unified/hooks/useFolder.ts +++ b/public/app/features/alerting/unified/hooks/useFolder.ts @@ -1,7 +1,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react'; import { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; interface ReturnBag { folder?: FolderDTO; diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx index 36d2d6f06d1..ff0c9b61a1c 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx @@ -5,7 +5,8 @@ import { Provider } from 'react-redux'; import { contextSrv } from 'app/core/services/context_srv'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, FolderDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { FolderDTO } from 'app/types/folders'; import { setupMswServer } from '../mockApi'; import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks'; diff --git a/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts b/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts index e7902fcf507..8230954063c 100644 --- a/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts +++ b/public/app/features/alerting/unified/hooks/useManagedAlertStateHistory.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { StateHistoryItem } from 'app/types/unified-alerting'; import { fetchGrafanaAnnotationsAction } from '../state/actions'; diff --git a/public/app/features/alerting/unified/hooks/useReturnTo.test.tsx b/public/app/features/alerting/unified/hooks/useReturnTo.test.tsx index 80adb1661cc..b5dadf701db 100644 --- a/public/app/features/alerting/unified/hooks/useReturnTo.test.tsx +++ b/public/app/features/alerting/unified/hooks/useReturnTo.test.tsx @@ -5,9 +5,10 @@ import { useReturnTo } from './useReturnTo'; describe('useReturnTo', () => { beforeAll(() => { + const win: typeof globalThis = window; // @ts-expect-error - delete window.location; - window.location = { origin: 'https://play.grafana.net' } as Location; + delete win.location; + win.location = { origin: 'https://play.grafana.net' } as Location; }); it('should return the fallback value when `returnTo` is not present in the query string', () => { diff --git a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts index 5e6daa81e8b..ed65c6ea4a9 100644 --- a/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts +++ b/public/app/features/alerting/unified/hooks/useUnifiedAlertingSelector.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import { UnifiedAlertingState } from '../state/reducers'; diff --git a/public/app/features/alerting/unified/mockApi.ts b/public/app/features/alerting/unified/mockApi.ts index ae0e9c385b0..614c8235dd0 100644 --- a/public/app/features/alerting/unified/mockApi.ts +++ b/public/app/features/alerting/unified/mockApi.ts @@ -9,7 +9,8 @@ import { setupAlertmanagerStatusMapDefaultState, } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { resetRoutingTreeMap } from 'app/features/alerting/unified/mocks/server/entities/k8s/routingtrees'; -import { DashboardDTO, FolderDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; +import { FolderDTO } from 'app/types/folders'; import { PromRulesResponse, RulerGrafanaRuleDTO, diff --git a/public/app/features/alerting/unified/mockGrafanaNotifiers.ts b/public/app/features/alerting/unified/mockGrafanaNotifiers.ts index 08bc5d4d90d..fd058176867 100644 --- a/public/app/features/alerting/unified/mockGrafanaNotifiers.ts +++ b/public/app/features/alerting/unified/mockGrafanaNotifiers.ts @@ -1,4 +1,4 @@ -import { GrafanaNotifierType, NotifierDTO } from 'app/types'; +import { GrafanaNotifierType, NotifierDTO } from 'app/types/alerting'; export const grafanaAlertNotifiers: Record<GrafanaNotifierType, NotifierDTO> = { dingding: { diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index e8d3bc2e2ab..0202406bf8e 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -26,7 +26,11 @@ import { SilenceState, } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, DashboardDTO, FolderDTO, NotifiersState, ReceiversState, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { NotifiersState, ReceiversState } from 'app/types/alerting'; +import { DashboardDTO } from 'app/types/dashboard'; +import { FolderDTO } from 'app/types/folders'; +import { StoreState } from 'app/types/store'; import { Alert, AlertingRule, diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index f21833e4866..3e3043d9331 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -22,7 +22,7 @@ import { import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { RulerDataSourceConfig } from 'app/types/unified-alerting'; import { GrafanaPromRuleGroupDTO, PromRuleGroupDTO, RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/mocks/server/db.ts b/public/app/features/alerting/unified/mocks/server/db.ts index a100519b4b4..05cbf225981 100644 --- a/public/app/features/alerting/unified/mocks/server/db.ts +++ b/public/app/features/alerting/unified/mocks/server/db.ts @@ -4,7 +4,7 @@ import { uniqueId } from 'lodash'; import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { GrafanaAlertStateDecision, GrafanaAlertingRuleDefinition, diff --git a/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts b/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts index 9e73c77f671..3a79aa4824d 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts @@ -1,7 +1,7 @@ import { HttpResponse, http } from 'msw'; import { Description, ResourcePermission } from 'app/core/components/AccessControl/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; // TODO: Expand this out to more realistic use cases as we work on RBAC for contact points const resourceDescriptionsMap: Record<string, Description> = { diff --git a/public/app/features/alerting/unified/mocks/server/handlers/folders.ts b/public/app/features/alerting/unified/mocks/server/handlers/folders.ts index a1599b815e1..7a295222077 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/folders.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/folders.ts @@ -2,7 +2,7 @@ import { HttpResponse, http } from 'msw'; import { mockFolder } from 'app/features/alerting/unified/mocks'; import { grafanaRulerRule } from 'app/features/alerting/unified/mocks/grafanaRulerApi'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; export const DEFAULT_FOLDERS: FolderDTO[] = [ mockFolder({ diff --git a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx index fd04f5c3b8e..8bd3b28e933 100644 --- a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx @@ -5,7 +5,7 @@ import { byRole, byTestId } from 'testing-library-selector'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { RulerAlertingRuleDTO, diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx index 86d92dac4fe..d21d660cc66 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx @@ -3,7 +3,7 @@ import { screen } from 'test/test-utils'; import { byText } from 'testing-library-selector'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromApiFeatures, PromApplication } from 'app/types/unified-alerting-dto'; import { discoverFeaturesByUid } from '../api/buildInfo'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx index ac0668721cb..7e823431f87 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudRules.test.tsx @@ -3,7 +3,7 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { screen } from 'test/test-utils'; import { selectors } from '@grafana/e2e-selectors'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; import { setupMswServer } from '../mockApi'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx index 9a99b587bcd..eab7a703499 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx @@ -6,7 +6,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx index fedb898f955..632904914b1 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorGrafanaRules.test.tsx @@ -8,7 +8,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { DashboardSearchItemType } from 'app/features/search/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; import { grafanaRulerGroup, grafanaRulerGroup2, grafanaRulerRule } from '../mocks/grafanaRulerApi'; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx index 19166c01ea4..cd1f40deb43 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx @@ -5,7 +5,7 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { byText } from 'testing-library-selector'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RecordingRuleEditorProps } from '../components/rule-editor/RecordingRuleEditor'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx index c1fe5eeec29..2c8411e09a9 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx @@ -3,7 +3,7 @@ import { byRole } from 'testing-library-selector'; import { DataSourceInstanceSettings } from '@grafana/data'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DataSourceRuleGroupIdentifier, DataSourceRulesSourceIdentifier } from 'app/types/unified-alerting'; import { PromRuleGroupDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index b9120e57eb1..b37e08dda81 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -2,7 +2,7 @@ import { mockIntersectionObserver } from 'jsdom-testing-mocks'; import { act, render, screen, waitForElementToBeRemoved } from 'test/test-utils'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index e29367da11a..08a8ecbce43 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -12,7 +12,7 @@ import { hashRule } from '../utils/rule-id'; import { DataSourceRuleLoader } from './DataSourceRuleLoader'; import { FilterProgressState, FilterStatus } from './FilterViewStatus'; -import { GrafanaRuleLoader } from './GrafanaRuleLoader'; +import { GrafanaRuleListItem } from './GrafanaRuleListItem'; import LoadMoreHelper from './LoadMoreHelper'; import { UnknownRuleListItem } from './components/AlertRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; @@ -154,11 +154,11 @@ function FilterViewResults({ filterState }: FilterViewProps) { switch (origin) { case 'grafana': return ( - <GrafanaRuleLoader - key={key} - ruleIdentifier={{ ruleSourceName: 'grafana', uid: rule.uid }} + <GrafanaRuleListItem + rule={rule} groupIdentifier={groupIdentifier} namespaceName={ruleWithOrigin.namespaceName} + showLocation={true} /> ); case 'datasource': diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx index 621cd6ecd2e..3169996b88c 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx @@ -2,6 +2,7 @@ import { render } from 'test/test-utils'; import { byRole, byTitle } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; +import { AccessControlAction } from 'app/types/accessControl'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, @@ -13,13 +14,13 @@ import { } from 'app/types/unified-alerting-dto'; import { setupMswServer } from '../mockApi'; -import { mockGrafanaPromAlertingRule, mockGrafanaRulerRule } from '../mocks'; +import { grantUserPermissions } from '../mocks'; import { grafanaRulerGroup, grafanaRulerNamespace } from '../mocks/grafanaRulerApi'; -import { setGrafanaPromRules } from '../mocks/server/configure'; +import { setFolderAccessControl, setGrafanaPromRules } from '../mocks/server/configure'; import { rulerRuleType } from '../utils/rules'; import { intervalToSeconds } from '../utils/time'; -import { GrafanaGroupLoader, matchRules } from './GrafanaGroupLoader'; +import { GrafanaGroupLoader } from './GrafanaGroupLoader'; setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); @@ -32,9 +33,36 @@ const ui = { ruleLink: (ruleName: string) => byRole('link', { name: ruleName }), editButton: () => byRole('link', { name: 'Edit' }), moreButton: () => byRole('button', { name: 'More' }), + // Menu items that appear when More button is clicked + menuItems: { + silence: () => byRole('menuitem', { name: /silence/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 }), + pause: () => byRole('menuitem', { name: /pause/i }), + }, }; describe('GrafanaGroupLoader', () => { + beforeEach(() => { + grantUserPermissions([ + AccessControlAction.AlertingRuleUpdate, + AccessControlAction.AlertingRuleDelete, + AccessControlAction.AlertingSilenceCreate, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + ]); + // Grant necessary permissions for editing rules + setFolderAccessControl({ + [AccessControlAction.AlertingRuleUpdate]: true, + [AccessControlAction.AlertingRuleDelete]: true, + [AccessControlAction.AlertingSilenceCreate]: true, + [AccessControlAction.AlertingRuleCreate]: true, // For duplicate action + [AccessControlAction.AlertingRuleRead]: true, // For export action + }); + }); + it('should render rule with url when ruler and prom rule exist', async () => { setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); @@ -55,8 +83,8 @@ describe('GrafanaGroupLoader', () => { ); }); - it('should render rule with url and creating state when only ruler rule exists', async () => { - setGrafanaPromRules([]); + it('should render More button with action menu options', async () => { + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); @@ -65,92 +93,125 @@ describe('GrafanaGroupLoader', () => { const [rule1] = grafanaRulerGroup.rules; const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); - const creatingIcon = ui.ruleStatus('Creating').get(ruleListItem); - expect(creatingIcon).toBeInTheDocument(); + // Check that More button is present + const moreButton = ui.moreButton().get(ruleListItem); + expect(moreButton).toBeInTheDocument(); - const ruleLink = ui.ruleLink(rule1.grafana_alert.title).get(ruleListItem); - expect(ruleLink).toHaveAttribute( - 'href', - expect.stringContaining(`/alerting/grafana/${rule1.grafana_alert.uid}/view`) - ); + // Verify More button accessibility + expect(moreButton).toHaveAttribute('aria-label', 'More'); + expect(moreButton).toHaveTextContent('More'); }); - it('should render delete rule operation list item when only prom rule exists', async () => { - const promOnlyGroup: GrafanaPromRuleGroupDTO = { - ...rulerGroupToPromGroup(grafanaRulerGroup), - name: 'prom-only-group', + it('should render multiple rules with their own action buttons', async () => { + // Create a group with multiple rules + const multiRuleGroup = { + ...grafanaRulerGroup, + rules: [ + grafanaRulerGroup.rules[0], + { + ...grafanaRulerGroup.rules[0], + grafana_alert: { + ...grafanaRulerGroup.rules[0].grafana_alert, + uid: 'second-rule-uid', + title: 'Second Rule', + }, + }, + ], }; - setGrafanaPromRules([promOnlyGroup]); + setGrafanaPromRules([rulerGroupToPromGroup(multiRuleGroup)]); - const groupIdentifier = getGroupIdentifier(promOnlyGroup); + const groupIdentifier = getGroupIdentifier(multiRuleGroup); render(<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />); - const [rule1] = promOnlyGroup.rules; - const promRule = await ui.ruleItem(rule1.name).find(); + // Check first rule + const [rule1, rule2] = multiRuleGroup.rules; + const ruleListItem1 = await ui.ruleItem(rule1.grafana_alert.title).find(); + const ruleListItem2 = await ui.ruleItem(rule2.grafana_alert.title).find(); - const deletingIcon = ui.ruleStatus('Deleting').get(promRule); - expect(deletingIcon).toBeInTheDocument(); + // Each rule should have its own More button + expect(ui.moreButton().get(ruleListItem1)).toBeInTheDocument(); + expect(ui.moreButton().get(ruleListItem2)).toBeInTheDocument(); - expect(ui.editButton().query(promRule)).not.toBeInTheDocument(); - expect(ui.moreButton().query(promRule)).not.toBeInTheDocument(); - }); -}); + // Check that edit buttons are present and have correct URLs + const editButton1 = ui.editButton().get(ruleListItem1); + const editButton2 = ui.editButton().get(ruleListItem2); -describe('matchRules', () => { - it('should return matches for all items and have empty promOnlyRules if all rules are matched by uid', () => { - const rulerRules = [ - mockGrafanaRulerRule({ uid: '1' }), - mockGrafanaRulerRule({ uid: '2' }), - mockGrafanaRulerRule({ uid: '3' }), - ]; + expect(editButton1).toBeInTheDocument(); + expect(editButton2).toBeInTheDocument(); - const promRules = rulerRules.map(rulerRuleToPromRule); - - const { matches, promOnlyRules } = matchRules(promRules, rulerRules); - - expect(matches.size).toBe(rulerRules.length); - expect(promOnlyRules).toHaveLength(0); - - for (const [rulerRule, promRule] of matches) { - expect(rulerRule.grafana_alert.uid).toBe(promRule.uid); - } + // Check that edit buttons have correct URLs (the actual format is simpler) + expect(editButton1).toHaveAttribute('href', expect.stringContaining(`/alerting/${rule1.grafana_alert.uid}/edit`)); + expect(editButton2).toHaveAttribute('href', expect.stringContaining(`/alerting/${rule2.grafana_alert.uid}/edit`)); }); - it('should return unmatched prometheus rules in promOnlyRules array', () => { - const rulerRules = [mockGrafanaRulerRule({ uid: '1' }), mockGrafanaRulerRule({ uid: '2' })]; + it('should not render edit button when user lacks edit permissions', async () => { + // Override permissions to deny editing + setFolderAccessControl({ + [AccessControlAction.AlertingRuleUpdate]: false, + [AccessControlAction.AlertingRuleDelete]: false, + }); - const matchingPromRules = rulerRules.map(rulerRuleToPromRule); - const unmatchedPromRules = [mockGrafanaPromAlertingRule({ uid: '3' }), mockGrafanaPromAlertingRule({ uid: '4' })]; + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); - const allPromRules = [...matchingPromRules, ...unmatchedPromRules]; - const { matches, promOnlyRules } = matchRules(allPromRules, rulerRules); + const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); - expect(matches.size).toBe(rulerRules.length); - expect(promOnlyRules).toHaveLength(unmatchedPromRules.length); - expect(promOnlyRules).toEqual(expect.arrayContaining(unmatchedPromRules)); + render(<GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} />); + + const [rule1] = grafanaRulerGroup.rules; + const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); + + // Edit button should not be present + expect(ui.editButton().query(ruleListItem)).not.toBeInTheDocument(); + + // More button should still be present (for other actions like viewing) + expect(ui.moreButton().get(ruleListItem)).toBeInTheDocument(); }); - it('should not include ruler rules in matches if they have no prometheus counterpart', () => { - const rulerRules = [ - mockGrafanaRulerRule({ uid: '1' }), - mockGrafanaRulerRule({ uid: '2' }), - mockGrafanaRulerRule({ uid: '3' }), - ]; + it('should render correct menu actions when More button is clicked', async () => { + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); - // Only create prom rule for the second ruler rule - const promRules = [rulerRuleToPromRule(rulerRules[1])]; + const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); - const { matches, promOnlyRules } = matchRules(promRules, rulerRules); + const { user } = render( + <GrafanaGroupLoader groupIdentifier={groupIdentifier} namespaceName={grafanaRulerNamespace.name} /> + ); - expect(matches.size).toBe(1); - expect(promOnlyRules).toHaveLength(0); + const [rule1] = grafanaRulerGroup.rules; + const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); - // Verify that only the second ruler rule is in matches - expect(matches.has(rulerRules[0])).toBe(false); - expect(matches.get(rulerRules[1])).toBe(promRules[0]); - expect(matches.has(rulerRules[2])).toBe(false); + // Find and click the More button + const moreButton = ui.moreButton().get(ruleListItem); + await user.click(moreButton); + + // Check that the dropdown menu appears + const menu = byRole('menu').get(); + expect(menu).toBeInTheDocument(); + + // With proper permissions, all 6 menu actions should be available: + + // 1. Silence notifications - available for alerting rules (AlertingSilenceCreate permission) + expect(ui.menuItems.silence().get()).toBeInTheDocument(); + + // 2. Copy link - always available + expect(ui.menuItems.copyLink().get()).toBeInTheDocument(); + + // 3. Duplicate - should be available with create permissions (AlertingRuleCreate permission) + expect(ui.menuItems.duplicate().get()).toBeInTheDocument(); + + // 4. Export - should be available for Grafana alerting rules (AlertingRuleRead permission) + expect(ui.menuItems.export().get()).toBeInTheDocument(); + + // 5. Delete - should be available for Grafana alerting rules (AlertingRuleDelete permission) + expect(ui.menuItems.delete().get()).toBeInTheDocument(); + + // 6. Pause - should be available for Grafana alerting rules (AlertingRuleUpdate permission) + expect(ui.menuItems.pause().get()).toBeInTheDocument(); + + // Verify that the menu contains all 6 expected menu items + const menuItems = byRole('menuitem').getAll(); + expect(menuItems.length).toBe(6); }); }); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx index c092a748388..2dee977835d 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx @@ -1,22 +1,13 @@ -import { useMemo } from 'react'; - import { t } from '@grafana/i18n'; import { Alert } from '@grafana/ui'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; -import { GrafanaPromRuleDTO, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; -import { logWarning } from '../Analytics'; -import { alertRuleApi } from '../api/alertRuleApi'; import { prometheusApi } from '../api/prometheusApi'; import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; -import { GrafanaRulesSource } from '../utils/datasource'; -import { GrafanaRuleListItem } from './GrafanaRuleLoader'; -import { RuleOperationListItem } from './components/AlertRuleListItem'; +import { GrafanaRuleListItem } from './GrafanaRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; -import { RuleOperation } from './components/RuleListIcon'; -const { useGetGrafanaRulerGroupQuery } = alertRuleApi; const { useGetGrafanaGroupsQuery } = prometheusApi; export interface GrafanaGroupLoaderProps { @@ -48,20 +39,8 @@ export function GrafanaGroupLoader({ }, { pollingInterval: RULE_LIST_POLL_INTERVAL_MS } ); - const { data: rulerResponse, isLoading: isRulerGroupLoading } = useGetGrafanaRulerGroupQuery({ - folderUid: groupIdentifier.namespace.uid, - groupName: groupIdentifier.groupName, - }); - const { matches, promOnlyRules } = useMemo(() => { - const promRules = promResponse?.data.groups.at(0)?.rules ?? []; - const rulerRules = rulerResponse?.rules ?? []; - - return matchRules(promRules, rulerRules); - }, [promResponse, rulerResponse]); - - const isLoading = isPromResponseLoading || isRulerGroupLoading; - if (isLoading) { + if (isPromResponseLoading) { return ( <> {Array.from({ length: expectedRulesCount }).map((_, index) => ( @@ -71,7 +50,7 @@ export function GrafanaGroupLoader({ ); } - if (!rulerResponse && !promResponse) { + if (!promResponse) { return ( <Alert title={t( @@ -86,28 +65,11 @@ export function GrafanaGroupLoader({ return ( <> - {rulerResponse?.rules.map((rulerRule) => { - const promRule = matches.get(rulerRule); - - if (!promRule) { - return ( - <GrafanaRuleListItem - key={rulerRule.grafana_alert.uid} - rule={promRule} - rulerRule={rulerRule} - groupIdentifier={groupIdentifier} - namespaceName={namespaceName} - operation={RuleOperation.Creating} - showLocation={false} - /> - ); - } - + {promResponse.data.groups.at(0)?.rules.map((promRule) => { return ( <GrafanaRuleListItem key={promRule.uid} rule={promRule} - rulerRule={rulerRule} groupIdentifier={groupIdentifier} namespaceName={namespaceName} // we don't show the location again for rules, it's redundant because they are shown in a folder > group hierarchy @@ -115,58 +77,6 @@ export function GrafanaGroupLoader({ /> ); })} - {promOnlyRules.map((rule) => ( - <RuleOperationListItem - key={rule.uid} - name={rule.name} - namespace={namespaceName} - group={groupIdentifier.groupName} - rulesSource={GrafanaRulesSource} - application="grafana" - operation={RuleOperation.Deleting} - showLocation={false} - /> - ))} </> ); } - -interface MatchingResult { - matches: Map<RulerGrafanaRuleDTO, GrafanaPromRuleDTO>; - /** - * Rules that were already removed from the Ruler but the changes has not been yet propagated to Prometheus - */ - promOnlyRules: GrafanaPromRuleDTO[]; -} - -export function matchRules( - promRules: GrafanaPromRuleDTO[], - rulerRules: RulerGrafanaRuleDTO[] -): Readonly<MatchingResult> { - const promRulesMap = new Map(promRules.map((rule) => [rule.uid, rule])); - - const matchingResult = rulerRules.reduce<MatchingResult>( - (acc, rulerRule) => { - const { matches } = acc; - const promRule = promRulesMap.get(rulerRule.grafana_alert.uid); - if (promRule) { - matches.set(rulerRule, promRule); - promRulesMap.delete(rulerRule.grafana_alert.uid); - } - return acc; - }, - { matches: new Map(), promOnlyRules: [] } - ); - - matchingResult.promOnlyRules.push(...promRulesMap.values()); - - if (matchingResult.promOnlyRules.length > 0) { - // Grafana Prometheus rules should be strongly consistent now so each Ruler rule should have a matching Prometheus rule - // If not, log it as a warning - logWarning('Grafana Managed Rules: No matching Prometheus rule found for Ruler rule', { - promOnlyRulesCount: matchingResult.promOnlyRules.length.toString(), - }); - } - - return matchingResult; -} diff --git a/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx new file mode 100644 index 00000000000..3113c991e09 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/GrafanaRuleListItem.tsx @@ -0,0 +1,71 @@ +import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; +import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto'; + +import { GrafanaRulesSource } from '../utils/datasource'; +import { totalFromStats } from '../utils/ruleStats'; +import { prometheusRuleType } from '../utils/rules'; +import { createRelativeUrl } from '../utils/url'; + +import { + AlertRuleListItem, + RecordingRuleListItem, + RuleListItemCommonProps, + UnknownRuleListItem, +} from './components/AlertRuleListItem'; +import { RuleActionsButtons } from './components/RuleActionsButtons.V2'; +import { RuleOperation } from './components/RuleListIcon'; + +interface GrafanaRuleListItemProps { + rule: GrafanaPromRuleDTO; + groupIdentifier: GrafanaRuleGroupIdentifier; + namespaceName: string; + operation?: RuleOperation; + showLocation?: boolean; +} + +export function GrafanaRuleListItem({ + rule, + groupIdentifier, + namespaceName, + operation, + showLocation = true, +}: GrafanaRuleListItemProps) { + const { name, uid, labels, provenance } = rule; + + const commonProps: RuleListItemCommonProps = { + name, + rulesSource: GrafanaRulesSource, + group: groupIdentifier.groupName, + namespace: namespaceName, + href: createRelativeUrl(`/alerting/grafana/${uid}/view`), + health: rule?.health, + error: rule?.lastError, + labels: labels, + isProvisioned: Boolean(provenance), + isPaused: rule?.isPaused, + application: 'grafana' as const, + actions: <RuleActionsButtons promRule={rule} groupIdentifier={groupIdentifier} compact />, + }; + + if (prometheusRuleType.grafana.alertingRule(rule)) { + const promAlertingRule = rule && rule.type === PromRuleType.Alerting ? rule : undefined; + const instancesCount = totalFromStats(promAlertingRule?.totals ?? {}); + + return ( + <AlertRuleListItem + {...commonProps} + summary={rule.annotations?.summary} + state={promAlertingRule?.state} + instancesCount={instancesCount} + operation={operation} + showLocation={showLocation} + /> + ); + } + + if (prometheusRuleType.grafana.recordingRule(rule)) { + return <RecordingRuleListItem {...commonProps} showLocation={showLocation} />; + } + + return <UnknownRuleListItem ruleName={name} groupIdentifier={groupIdentifier} ruleDefinition={rule} />; +} diff --git a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx deleted file mode 100644 index 3ca5e6a7496..00000000000 --- a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { Trans, t } from '@grafana/i18n'; -import { Alert } from '@grafana/ui'; -import { GrafanaRuleGroupIdentifier, GrafanaRuleIdentifier } from 'app/types/unified-alerting'; -import { GrafanaPromRuleDTO, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; - -import { alertRuleApi } from '../api/alertRuleApi'; -import { prometheusApi } from '../api/prometheusApi'; -import { createReturnTo } from '../hooks/useReturnTo'; -import { GrafanaRulesSource } from '../utils/datasource'; -import { totalFromStats } from '../utils/ruleStats'; -import { rulerRuleType } from '../utils/rules'; -import { createRelativeUrl } from '../utils/url'; - -import { - AlertRuleListItem, - RecordingRuleListItem, - RuleListItemCommonProps, - UnknownRuleListItem, -} from './components/AlertRuleListItem'; -import { AlertRuleListItemSkeleton, RulerRuleLoadingError } from './components/AlertRuleListItemLoader'; -import { RuleActionsButtons } from './components/RuleActionsButtons.V2'; -import { RuleOperation } from './components/RuleListIcon'; - -const { useGetGrafanaRulerGroupQuery } = alertRuleApi; -const { useGetGrafanaGroupsQuery } = prometheusApi; - -interface GrafanaRuleLoaderProps { - ruleIdentifier: GrafanaRuleIdentifier; - groupIdentifier: GrafanaRuleGroupIdentifier; - namespaceName: string; -} - -export function GrafanaRuleLoader({ ruleIdentifier, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) { - const { - data: rulerRuleGroup, - error: rulerRuleGroupError, - isLoading: isRulerRuleGroupLoading, - } = useGetGrafanaRulerGroupQuery({ - folderUid: groupIdentifier.namespace.uid, - groupName: groupIdentifier.groupName, - }); - const { - data: promRuleGroup, - error: promRuleGroupError, - isLoading: isPromRuleGroupLoading, - } = useGetGrafanaGroupsQuery({ - folderUid: groupIdentifier.namespace.uid, - groupName: groupIdentifier.groupName, - }); - - const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === ruleIdentifier.uid); - const promRule = promRuleGroup?.data.groups - .flatMap((group) => group.rules) - .find((promRule) => promRule.uid === ruleIdentifier.uid); - - if (rulerRuleGroupError || promRuleGroupError) { - return <RulerRuleLoadingError ruleIdentifier={ruleIdentifier} error={rulerRuleGroupError || promRuleGroupError} />; - } - - if (isRulerRuleGroupLoading || isPromRuleGroupLoading) { - return <AlertRuleListItemSkeleton />; - } - - if (!rulerRule) { - return ( - <Alert - title={t('alerting.rule-list.cannot-load-rule-details-for', 'Cannot load rule details for UID {{uid}}', { - uid: ruleIdentifier.uid, - })} - severity="error" - > - <Trans i18nKey="alerting.rule-list.cannot-find-rule-details-for"> - Cannot find rule details for UID {{ uid: ruleIdentifier.uid ?? '<empty uid>' }} - </Trans> - </Alert> - ); - } - - return ( - <GrafanaRuleListItem - rule={promRule} - rulerRule={rulerRule} - groupIdentifier={groupIdentifier} - namespaceName={namespaceName} - /> - ); -} - -interface GrafanaRuleListItemProps { - rule?: GrafanaPromRuleDTO; - rulerRule: RulerGrafanaRuleDTO; - groupIdentifier: GrafanaRuleGroupIdentifier; - namespaceName: string; - operation?: RuleOperation; - showLocation?: boolean; -} - -export function GrafanaRuleListItem({ - rule, - rulerRule, - groupIdentifier, - namespaceName, - operation, - showLocation = true, -}: GrafanaRuleListItemProps) { - const returnTo = createReturnTo(); - - const { - grafana_alert: { uid, title, provenance, is_paused }, - annotations = {}, - labels = {}, - } = rulerRule; - - const commonProps: RuleListItemCommonProps = { - name: title, - rulesSource: GrafanaRulesSource, - group: groupIdentifier.groupName, - namespace: namespaceName, - href: createRelativeUrl(`/alerting/grafana/${uid}/view`, { returnTo }), - health: rule?.health, - error: rule?.lastError, - labels: labels, - isProvisioned: Boolean(provenance), - isPaused: rule?.isPaused ?? is_paused, - application: 'grafana' as const, - actions: <RuleActionsButtons rule={rulerRule} promRule={rule} groupIdentifier={groupIdentifier} compact />, - showLocation, - }; - - if (rulerRuleType.grafana.alertingRule(rulerRule)) { - const promAlertingRule = rule && rule.type === PromRuleType.Alerting ? rule : undefined; - const instancesCount = totalFromStats(promAlertingRule?.totals ?? {}); - - return ( - <AlertRuleListItem - {...commonProps} - summary={annotations.summary} - state={promAlertingRule?.state} - instancesCount={instancesCount} - operation={operation} - /> - ); - } - - if (rulerRuleType.grafana.recordingRule(rulerRule)) { - return <RecordingRuleListItem {...commonProps} />; - } - - return <UnknownRuleListItem ruleName={title} groupIdentifier={groupIdentifier} ruleDefinition={rulerRule} />; -} diff --git a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx index 361d017b4d2..63723b06bdd 100644 --- a/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GroupedView.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor, within } from 'test/test-utils'; import { byRole } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook, setReturnToPreviousHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx index ec3fe298a91..76fc0c6bbec 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx @@ -51,9 +51,7 @@ function PaginatedGroupsLoader({ rulesSourceIdentifier, application, groupFilter const hasFilters = Boolean(groupFilter || namespaceFilter); const { uid, name } = rulesSourceIdentifier; - const prometheusGroupsGenerator = usePrometheusGroupsGenerator({ - populateCache: hasFilters ? false : true, - }); + const prometheusGroupsGenerator = usePrometheusGroupsGenerator(); // If there are no filters we can match one frontend page to one API page. // However, if there are filters, we need to fetch more groups from the API to populate one frontend page diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx index d9df8e007d4..8e80dd0eff3 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx @@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n'; import { logInfo } from '@grafana/runtime'; import { Button, LinkButton, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; import { LogMessages, trackRuleListNavigation } from '../Analytics'; diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx index e29d4c2f65b..1f2f4c0b02e 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx @@ -2,7 +2,7 @@ import { render } from 'test/test-utils'; import { byRole, byTestId } from 'testing-library-selector'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions } from '../mocks'; diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx index db56bb9af9f..e8abce41ed8 100644 --- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItemLoader.tsx @@ -17,6 +17,7 @@ export function AlertRuleListItemSkeleton() { description={<Skeleton width={256} />} actions={<RuleActionsSkeleton />} data-testid="alert-rule-list-item-loader" + aria-disabled={true} /> ); } diff --git a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx index 2c183bc6059..4570b3dd8e9 100644 --- a/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx +++ b/public/app/features/alerting/unified/rule-list/components/RuleActionsButtons.V2.tsx @@ -1,4 +1,6 @@ +import { isString } from 'lodash'; import { useState } from 'react'; +import { RequireAtLeastOne } from 'type-fest'; import { Trans, t } from '@grafana/i18n'; import { LinkButton, Stack } from '@grafana/ui'; @@ -6,24 +8,40 @@ import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/ import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal'; import { RedirectToCloneRule } from 'app/features/alerting/unified/components/rules/CloneRule'; import SilenceGrafanaRuleDrawer from 'app/features/alerting/unified/components/silences/SilenceGrafanaRuleDrawer'; -import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting'; +import { + EditableRuleIdentifier, + GrafanaRuleIdentifier, + Rule, + RuleGroupIdentifierV2, + RuleIdentifier, +} from 'app/types/unified-alerting'; import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; -import { AlertRuleAction, useRulerRuleAbility } from '../../hooks/useAbilities'; +import { logWarning } from '../../Analytics'; +import { AlertRuleAction, skipToken, useGrafanaPromRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities'; import * as ruleId from '../../utils/rule-id'; -import { isProvisionedRule, rulerRuleType } from '../../utils/rules'; +import { + getRuleUID, + isProvisionedPromRule, + isProvisionedRule, + prometheusRuleType, + rulerRuleType, +} from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; -interface Props { - rule: RulerRuleDTO; +type RuleProps = RequireAtLeastOne<{ + rule?: RulerRuleDTO; promRule?: Rule; +}>; + +type Props = RuleProps & { groupIdentifier: RuleGroupIdentifierV2; /** * Should we show the buttons in a "compact" state? * i.e. without text and using smaller button sizes */ compact?: boolean; -} +}; // For now this is just a copy of RuleActionsButtons.tsx but with the View button removed. // This is only done to keep the new list behind a feature flag and limit changes in the existing components @@ -37,16 +55,32 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: { identifier: RuleIdentifier; isProvisioned: boolean } | undefined >(undefined); - const isProvisioned = isProvisionedRule(rule); + const isProvisioned = getIsProvisioned(rule, promRule); const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rule, groupIdentifier, AlertRuleAction.Update); + // If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions, + // as we have removed all requests to the ruler API in the list view. + const [grafanaEditRuleSupported, grafanaEditRuleAllowed] = useGrafanaPromRuleAbility( + prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken, + AlertRuleAction.Update + ); - const canEditRule = editRuleSupported && editRuleAllowed; + const canEditRule = (editRuleSupported && editRuleAllowed) || (grafanaEditRuleSupported && grafanaEditRuleAllowed); const buttons: JSX.Element[] = []; const buttonSize = compact ? 'sm' : 'md'; - const identifier = ruleId.fromRulerRuleAndGroupIdentifierV2(groupIdentifier, rule); + const identifier = getEditableIdentifier(groupIdentifier, rule, promRule); + + if (!identifier) { + return null; + } + + // determine if this rule can be silenced by checking for Grafana Alert rule type and extracting the UID + const ruleUid = getRuleUID(rule ?? promRule); + const silenceableRule = + isString(ruleUid) && + (rulerRuleType.grafana.alertingRule(rule) || prometheusRuleType.grafana.alertingRule(promRule)); if (canEditRule) { const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`); @@ -75,13 +109,13 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: promRule={promRule} groupIdentifier={groupIdentifier} identifier={identifier} - handleDelete={() => showDeleteModal(identifier, groupIdentifier)} + handleDelete={(identifier, groupIdentifier) => showDeleteModal(identifier, groupIdentifier)} handleSilence={() => setShowSilenceDrawer(true)} handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })} /> {deleteModal} - {rulerRuleType.grafana.alertingRule(rule) && showSilenceDrawer && ( - <SilenceGrafanaRuleDrawer rulerRule={rule} onClose={() => setShowSilenceDrawer(false)} /> + {silenceableRule && showSilenceDrawer && ( + <SilenceGrafanaRuleDrawer ruleUid={ruleUid} onClose={() => setShowSilenceDrawer(false)} /> )} {redirectToClone?.identifier && ( <RedirectToCloneRule @@ -93,3 +127,38 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }: </Stack> ); } + +function getIsProvisioned(rule?: RulerRuleDTO, promRule?: Rule): boolean { + if (rule) { + return isProvisionedRule(rule); + } + + if (promRule) { + return isProvisionedPromRule(promRule); + } + + return false; +} + +function getEditableIdentifier( + groupIdentifier: RuleGroupIdentifierV2, + rule?: RulerRuleDTO, + promRule?: Rule +): EditableRuleIdentifier | undefined { + if (rule) { + return ruleId.fromRulerRuleAndGroupIdentifierV2(groupIdentifier, rule); + } + + if (prometheusRuleType.grafana.rule(promRule)) { + return { + ruleSourceName: 'grafana', + uid: promRule.uid, + } satisfies GrafanaRuleIdentifier; + } + + logWarning('Unable to construct an editable rule identifier'); + + // Returning undefined is safer than throwing here as it allows the component to gracefully handle + // the error by returning null instead of crashing the entire component tree + return undefined; +} diff --git a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts index 40d786f03f7..71ee5537465 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -1,11 +1,9 @@ import { useCallback } from 'react'; -import { useDispatch } from 'app/types/store'; import { DataSourceRulesSourceIdentifier, RuleHealth } from 'app/types/unified-alerting'; import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; -import { alertRuleApi } from '../../api/alertRuleApi'; -import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi'; +import { PromRulesResponse, prometheusApi, usePopulateGrafanaPrometheusApiCache } from '../../api/prometheusApi'; const { useLazyGetGroupsQuery, useLazyGetGrafanaGroupsQuery } = prometheusApi; @@ -23,8 +21,7 @@ interface FetchGroupsOptions { groupNextToken?: string; } -export function usePrometheusGroupsGenerator(hookOptions: UseGeneratorHookOptions = {}) { - const dispatch = useDispatch(); +export function usePrometheusGroupsGenerator() { const [getGroups] = useLazyGetGroupsQuery(); return useCallback( @@ -36,24 +33,12 @@ export function usePrometheusGroupsGenerator(hookOptions: UseGeneratorHookOption ...fetchOptions, }).unwrap(); - if (hookOptions.populateCache) { - response.data.groups.forEach((group) => { - dispatch( - prometheusApi.util.upsertQueryData( - 'getGroups', - { ruleSource: { uid: ruleSource.uid }, namespace: group.file, groupName: group.name }, - { data: { groups: [group] }, status: 'success' } - ) - ); - }); - } - return response; }; yield* genericGroupsGenerator(getRuleSourceGroupsWithCache, groupLimit); }, - [getGroups, dispatch, hookOptions.populateCache] + [getGroups] ); } @@ -68,8 +53,8 @@ interface GrafanaFetchGroupsOptions extends FetchGroupsOptions { } export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = {}) { - const dispatch = useDispatch(); const [getGrafanaGroups] = useLazyGetGrafanaGroupsQuery(); + const { populateGroupsResponseCache } = usePopulateGrafanaPrometheusApiCache(); const getGroupsAndProvideCache = useCallback( async (fetchOptions: GrafanaFetchGroupsOptions) => { @@ -79,32 +64,13 @@ export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = ...fetchOptions.filter, }).unwrap(); - // This is not mandatory to preload ruler rules, but it improves the UX - // Because the user waits a bit longer for the initial load but doesn't need to wait for each group to be loaded if (hookOptions.populateCache) { - const cacheAndRulerPreload = response.data.groups.map(async (group) => { - dispatch( - alertRuleApi.util.prefetch( - 'getGrafanaRulerGroup', - { folderUid: group.folderUid, groupName: group.name }, - { force: true } - ) - ); - await dispatch( - prometheusApi.util.upsertQueryData( - 'getGrafanaGroups', - { folderUid: group.folderUid, groupName: group.name, limitAlerts: hookOptions.limitAlerts }, - { data: { groups: [group] }, status: 'success' } - ) - ); - }); - - await Promise.allSettled(cacheAndRulerPreload); + populateGroupsResponseCache(response.data.groups); } return response; }, - [getGrafanaGroups, dispatch, hookOptions.populateCache, hookOptions.limitAlerts] + [getGrafanaGroups, hookOptions.limitAlerts, hookOptions.populateCache, populateGroupsResponseCache] ); return useCallback( diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 73095e0acff..216d59c10a7 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -15,7 +15,6 @@ import { PromRuleGroupDTO, } from 'app/types/unified-alerting-dto'; -import { usePopulateGrafanaPrometheusApiCache } from '../../api/prometheusApi'; import { RulesFilter } from '../../search/rulesSearchParser'; import { getDataSourceByUid, @@ -52,7 +51,6 @@ interface GetIteratorResult { } export function useFilteredRulesIteratorProvider() { - const { populateGroupResponseCache } = usePopulateGrafanaPrometheusApiCache(); const allExternalRulesSources = getExternalRulesSources(); const prometheusGroupsGenerator = usePrometheusGroupsGenerator(); @@ -76,10 +74,7 @@ export function useFilteredRulesIteratorProvider() { concatMap((groups) => groups .filter((group) => groupFilter(group, normalizedFilterState)) - .flatMap((group) => { - populateGroupResponseCache(group); - return group.rules.map((rule) => [group, rule] as const); - }) + .flatMap((group) => group.rules.map((rule) => [group, rule] as const)) .filter(([, rule]) => ruleFilter(rule, normalizedFilterState)) .map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule)) ), diff --git a/public/app/features/alerting/unified/settings/extensions.ts b/public/app/features/alerting/unified/settings/extensions.ts new file mode 100644 index 00000000000..c6cb202849f --- /dev/null +++ b/public/app/features/alerting/unified/settings/extensions.ts @@ -0,0 +1,50 @@ +import { useLocation } from 'react-router-dom-v5-compat'; + +import { NavModelItem } from '@grafana/data'; +import { useSelector } from 'app/types/store'; + +type SettingsSectionUrl = `/alerting/admin/${string}`; +type SettingsSectionNav = Pick<NavModelItem, 'id' | 'text' | 'icon'> & { + url: SettingsSectionUrl; +}; + +const settingsExtensions: Map<SettingsSectionUrl, { nav: SettingsSectionNav }> = new Map(); + +/** + * Registers a new settings section that will appear as a tab in the alerting settings page. + * @param pageNav - The navigation configuration for the settings section + */ +export function addSettingsSection(pageNav: SettingsSectionNav) { + if (settingsExtensions.has(pageNav.url)) { + console.warn('Unable to add settings page, PageNav must have an unique url'); + return; + } + settingsExtensions.set(pageNav.url, { nav: pageNav }); +} + +/** + * Returns the navigation configuration for all settings extensions. + */ +export function useSettingsExtensionsNav(): NavModelItem[] { + const location = useLocation(); + + const navIndex = useSelector((state) => state.navIndex); + const settingsNav = navIndex['alerting-admin']; + + // Build extension tabs from settingsExtensions + const extensionTabs: NavModelItem[] = Array.from(settingsExtensions.entries()).map(([url, { nav }]) => ({ + ...nav, + active: location.pathname === url, + url: url, + parentItem: settingsNav, + })); + + return extensionTabs; +} + +/** + * ONLY USE FOR TESTING. Clears all settings extensions. + */ +export function clearSettingsExtensions() { + settingsExtensions.clear(); +} diff --git a/public/app/features/alerting/unified/settings/navigation.test.ts b/public/app/features/alerting/unified/settings/navigation.test.ts new file mode 100644 index 00000000000..380d747a8a6 --- /dev/null +++ b/public/app/features/alerting/unified/settings/navigation.test.ts @@ -0,0 +1,111 @@ +import { renderHook } from '@testing-library/react'; +import { getWrapper } from 'test/test-utils'; + +import { addSettingsSection, clearSettingsExtensions } from './extensions'; +import { useSettingsPageNav } from './navigation'; + +describe('useSettingsPageNav', () => { + const mockNavIndex = { + 'alerting-admin': { + id: 'alerting-admin', + text: 'Settings', + url: '/alerting/admin', + }, + }; + + const defaultPreloadedState = { + navIndex: mockNavIndex, + }; + + it('should return settings page nav with alertmanager child when no extensions are present', () => { + const wrapper = getWrapper({ + preloadedState: defaultPreloadedState, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/admin/alertmanager'], + }, + }); + + const { result } = renderHook(() => useSettingsPageNav(), { wrapper }); + + expect(result.current.navId).toBe('alerting-admin'); + + // Check the structure + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toHaveLength(1); + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toEqual([ + expect.objectContaining({ + id: 'alertmanager', + text: 'Alert managers', + url: '/alerting/admin/alertmanager', + active: true, + icon: 'cloud', + parentItem: undefined, + }), + ]); + }); + + it('should include extensions when added via addSettingsSection', () => { + // Clear any existing extensions + clearSettingsExtensions(); + + // Add two extensions + addSettingsSection({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + icon: 'star', + }); + + addSettingsSection({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + icon: 'bell', + }); + + const wrapper = getWrapper({ + preloadedState: defaultPreloadedState, + renderWithRouter: true, + historyOptions: { + initialEntries: ['/alerting/admin/enrichment'], + }, + }); + + const { result } = renderHook(() => useSettingsPageNav(), { wrapper }); + + expect(result.current.navId).toBe('alerting-admin'); + + // Should have 3 children: alertmanager + 2 extensions + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toHaveLength(3); + // eslint-disable-next-line testing-library/no-node-access + expect(result.current.pageNav.children).toEqual([ + expect.objectContaining({ + id: 'alertmanager', + text: 'Alert managers', + url: '/alerting/admin/alertmanager', + active: false, + icon: 'cloud', + parentItem: undefined, + }), + expect.objectContaining({ + id: 'enrichment', + text: 'Enrichment', + url: '/alerting/admin/enrichment', + active: true, + icon: 'star', + parentItem: undefined, + }), + expect.objectContaining({ + id: 'notifications', + text: 'Notifications', + url: '/alerting/admin/notifications', + active: false, + icon: 'bell', + parentItem: undefined, + }), + ]); + }); +}); diff --git a/public/app/features/alerting/unified/settings/navigation.ts b/public/app/features/alerting/unified/settings/navigation.ts new file mode 100644 index 00000000000..83c4afb69f0 --- /dev/null +++ b/public/app/features/alerting/unified/settings/navigation.ts @@ -0,0 +1,40 @@ +import { useLocation } from 'react-router-dom-v5-compat'; + +import { NavModelItem } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { useSelector } from 'app/types/store'; + +import { useSettingsExtensionsNav } from './extensions'; + +export function useSettingsPageNav() { + const location = useLocation(); + + const navIndex = useSelector((state) => state.navIndex); + const settingsNav = navIndex['alerting-admin']; + + const extensionTabs = useSettingsExtensionsNav(); + + // All available tabs including the main alertmanager tab + const allTabs: NavModelItem[] = [ + { + id: 'alertmanager', + text: t('alerting.settings.tabs.alert-managers', 'Alert managers'), + url: '/alerting/admin/alertmanager', + active: location.pathname === '/alerting/admin/alertmanager', + icon: 'cloud', + parentItem: settingsNav, + }, + ...extensionTabs, + ]; + + // Create pageNav that represents the Settings page with tabs as children + const pageNav: NavModelItem = { + ...settingsNav, + children: allTabs, + }; + + return { + navId: 'alerting-admin', + pageNav, + }; +} diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index f45b7ff1dde..90ccc17db20 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -9,7 +9,7 @@ import { Receiver, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { RuleIdentifier, RuleNamespace, StateHistoryItem } from 'app/types/unified-alerting'; import { RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/alerting/unified/test/test-utils.ts b/public/app/features/alerting/unified/test/test-utils.ts index 66598dea083..0dea7f9eb58 100644 --- a/public/app/features/alerting/unified/test/test-utils.ts +++ b/public/app/features/alerting/unified/test/test-utils.ts @@ -2,7 +2,7 @@ import { act } from '@testing-library/react'; import { FeatureToggles } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { grantUserPermissions } from '../mocks'; import { setFolderAccessControl } from '../mocks/server/configure'; diff --git a/public/app/features/alerting/unified/types/receiver-form.ts b/public/app/features/alerting/unified/types/receiver-form.ts index e17aa9d05f0..d2045a2c2a6 100644 --- a/public/app/features/alerting/unified/types/receiver-form.ts +++ b/public/app/features/alerting/unified/types/receiver-form.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types'; -import { CloudNotifierType, NotifierType } from 'app/types'; +import { CloudNotifierType, NotifierType } from 'app/types/alerting'; import { ControlledField } from '../hooks/useControlledFieldArray'; diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index edda81712ae..0821c92963b 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -1,6 +1,6 @@ import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from './datasource'; 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 96bc19b4aff..2475248dc34 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 @@ -1,5 +1,5 @@ import { config } from '@grafana/runtime'; -import { CloudNotifierType, NotificationChannelOption, NotifierDTO } from 'app/types'; +import { CloudNotifierType, NotificationChannelOption, NotifierDTO } from 'app/types/alerting'; import { option } from './notifier-types'; diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index a50eddcfa50..4bcf3757d2c 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -8,7 +8,7 @@ import { AlertManagerImplementation, AlertmanagerChoice, } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DataSourceRulesSourceIdentifier as DataSourceRulesSourceIdentifier, GrafanaRulesSourceIdentifier, diff --git a/public/app/features/alerting/unified/utils/k8s/errors.test.ts b/public/app/features/alerting/unified/utils/k8s/errors.test.ts new file mode 100644 index 00000000000..ac7c03aa907 --- /dev/null +++ b/public/app/features/alerting/unified/utils/k8s/errors.test.ts @@ -0,0 +1,45 @@ +import { + ApiMachineryErrorResponse, + ERROR_ROUTES_MATCHER_CONFLICT, + getErrorMessageFromApiMachineryErrorResponse, +} from './errors'; + +describe('getErrorMessageFromCode', () => { + it(`should handle ${ERROR_ROUTES_MATCHER_CONFLICT}`, () => { + const error: ApiMachineryErrorResponse = { + status: 400, + config: { url: '' }, + data: { + kind: 'Status', + apiVersion: 'v1', + metadata: {}, + status: 'Failure', + message: 'this is ignored', + reason: 'BadRequest', + details: { + uid: 'alerting.notifications.routes.conflictingMatchers', + causes: [ + { + message: '"[foo=\\"bar\\" baz=\\"qux\\"]"', + field: 'Matchers', + }, + { + message: '"[foo2=\\"bar2\\" baz2=\\"qux2\\"]"', + field: 'Matchers', + }, + ], + }, + code: 400, + }, + }; + + expect(getErrorMessageFromApiMachineryErrorResponse(error)).toBe( + 'Cannot add or update route: matchers conflict with an external routing tree if we merged matchers \"[foo=\\\"bar\\\" baz=\\\"qux\\\"]\", \"[foo2=\\\"bar2\\\" baz2=\\\"qux2\\\"]\". This would make the route unreachable.' + ); + + delete error.data.details?.causes; + expect(getErrorMessageFromApiMachineryErrorResponse(error)).toBe( + 'Cannot add or update route: matchers conflict with an external routing tree if we merged matchers <unknown matchers>. This would make the route unreachable.' + ); + }); +}); diff --git a/public/app/features/alerting/unified/utils/k8s/errors.ts b/public/app/features/alerting/unified/utils/k8s/errors.ts index 3d315471321..0bb486710a1 100644 --- a/public/app/features/alerting/unified/utils/k8s/errors.ts +++ b/public/app/features/alerting/unified/utils/k8s/errors.ts @@ -3,19 +3,56 @@ import { get } from 'lodash'; import { t } from '@grafana/i18n'; import { FetchError, isFetchError } from '@grafana/runtime'; -export type SupportedErrors = 'alerting.notifications.conflict' | string; +import { getErrorCode } from '../misc'; -export const ERROR_NEWER_CONFIGURATION = 'alerting.notifications.conflict'; +export const ERROR_NEWER_CONFIGURATION = 'alerting.notifications.conflict' as const; +export const ERROR_ROUTES_MATCHER_CONFLICT = 'alerting.notifications.routes.conflictingMatchers' as const; -/** This function gives us the opportunity to translate or transform error codes that are returned from the Kubernetes APIs */ +export type ApiMachineryErrorResponse = FetchError<ApiMachineryError>; + +// these are known error IDs, used by both Kubernetes API and the front-end (using error `cause`). +export type KnownErrorCodes = typeof ERROR_NEWER_CONFIGURATION; +// Kubernetes API Machinery errors are a superset of supported errors codes. +export type KnownMachineryErrorCodes = KnownErrorCodes | typeof ERROR_ROUTES_MATCHER_CONFLICT; + +/** + * This function gives us the opportunity to translate or transform error codes that are returned from the Kubernetes APIs + */ +export function getErrorMessageFromApiMachineryErrorResponse(error: ApiMachineryErrorResponse): string | undefined { + const code = getErrorCode(error); + if (!code) { + return error.data.message; + } + + const errorMessageMap: Record<KnownMachineryErrorCodes, string | undefined> = { + [ERROR_NEWER_CONFIGURATION]: getErrorMessageFromCode(code), + [ERROR_ROUTES_MATCHER_CONFLICT]: t( + 'alerting.policies.update-errors.routes.conflictingMatchers', + 'Cannot add or update route: matchers conflict with an external routing tree if we merged matchers {{-matchers}}. This would make the route unreachable.', + { + matchers: + error.data.details?.causes?.map((cause) => cause.message).join(', ') ?? + t('alerting.policies.update-errors.routes.unknownMatchers', '<unknown matchers>'), + } + ), + }; + + // @ts-expect-error this allows the typechecker to warn us about forgetting to handle some KnownMachineryErrors and still return "undefined" for unknown codes; + return errorMessageMap[code]; +} + +/** + * This function gives us the opportunity to translate or transform error codes + */ export function getErrorMessageFromCode(code: string): string | undefined { - const errorMessageMap: Record<SupportedErrors, string> = { + const errorMessageMap: Record<KnownErrorCodes, string> = { [ERROR_NEWER_CONFIGURATION]: t( 'alerting.policies.update-errors.conflict', 'The notification policy tree has been updated by another user.' ), }; + // @ts-expect-error this allows the typechecker to warn us about forgetting to handle some KnownErrors and still return "undefined" for unknown codes; return errorMessageMap[code]; } @@ -29,7 +66,12 @@ export type ApiMachineryError = { group?: string; kind?: string; retryAfterSeconds?: number; - causes?: []; // @TODO type this, see apimachinery@v0.31.1/pkg/apis/meta/v1/types.go + // https://github.com/kubernetes/apimachinery/blob/v0.33.2/pkg/apis/meta/v1/types.go#L1020-L1040 + causes?: Array<{ + message?: string; + field?: string; + reason?: string; + }>; }; status: 'Failure'; metadata?: Record<string, unknown>; @@ -37,6 +79,6 @@ export type ApiMachineryError = { reason: string; }; -export function isApiMachineryError(error: unknown): error is FetchError<ApiMachineryError> { +export function isApiMachineryError(error: unknown): error is ApiMachineryErrorResponse { return isFetchError(error) && get(error.data, 'kind') === 'Status' && get(error.data, 'status') === 'Failure'; } diff --git a/public/app/features/alerting/unified/utils/misc.test.ts b/public/app/features/alerting/unified/utils/misc.test.ts index 8dae9443827..8916603f2ee 100644 --- a/public/app/features/alerting/unified/utils/misc.test.ts +++ b/public/app/features/alerting/unified/utils/misc.test.ts @@ -125,12 +125,12 @@ describe('create links', () => { describe('stringifyErrorLike', () => { it('should stringify error with cause', () => { - const error = new Error('Something went strong', { cause: new Error('database did not respond') }); - expect(stringifyErrorLike(error)).toBe('Something went strong, cause: database did not respond'); + const error = new Error('Something went wrong', { cause: new Error('database did not respond') }); + expect(stringifyErrorLike(error)).toBe('Something went wrong, cause: database did not respond'); }); it('should stringify error with cause being a code', () => { - const error = new Error('Something went strong', { cause: ERROR_NEWER_CONFIGURATION }); + const error = new Error('Something went wrong', { cause: ERROR_NEWER_CONFIGURATION }); expect(stringifyErrorLike(error)).toBe(getErrorMessageFromCode(ERROR_NEWER_CONFIGURATION)); }); diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index 561e5be930d..4501be47b69 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -1,4 +1,4 @@ -import { sortBy } from 'lodash'; +import { isString, sortBy } from 'lodash'; import { Labels, UrlQueryMap } from '@grafana/data'; import { GrafanaEdition } from '@grafana/data/internal'; @@ -34,7 +34,12 @@ import { import { ALERTMANAGER_NAME_QUERY_KEY } from './constants'; import { getRulesSourceName } from './datasource'; -import { SupportedErrors, getErrorMessageFromCode, isApiMachineryError } from './k8s/errors'; +import { + KnownErrorCodes, + getErrorMessageFromApiMachineryErrorResponse, + getErrorMessageFromCode, + isApiMachineryError, +} from './k8s/errors'; import { getMatcherQueryParams } from './matchers'; import { rulesNav } from './navigation'; import * as ruleId from './rule-id'; @@ -282,15 +287,20 @@ export function isErrorLike(error: unknown): error is Error { return Boolean(error && typeof error === 'object' && 'message' in error); } -export function getErrorCode(error: Error): unknown { +export function getErrorCode(error: unknown): string | undefined { if (isApiMachineryError(error) && error.data.details) { return error.data.details.uid; } - return error.cause; + + if (isErrorLike(error) && isString(error.cause)) { + return error.cause; + } + + return; } /* this function will check if the error passed as the first argument contains an error code */ -export function isErrorMatchingCode(error: Error | undefined, code: SupportedErrors): boolean { +export function isErrorMatchingCode(error: Error | undefined, code: KnownErrorCodes): boolean { if (!error) { return false; } @@ -301,8 +311,8 @@ export function isErrorMatchingCode(error: Error | undefined, code: SupportedErr export function stringifyErrorLike(error: unknown): string { const fetchError = isFetchError(error); if (fetchError) { - if (isApiMachineryError(error) && error.data.details) { - const message = getErrorMessageFromCode(error.data.details.uid); + if (isApiMachineryError(error)) { + const message = getErrorMessageFromApiMachineryErrorResponse(error); if (message) { return message; } diff --git a/public/app/features/alerting/unified/utils/navigation.ts b/public/app/features/alerting/unified/utils/navigation.ts index 15081fb8b4e..259207f41b7 100644 --- a/public/app/features/alerting/unified/utils/navigation.ts +++ b/public/app/features/alerting/unified/utils/navigation.ts @@ -45,6 +45,32 @@ export const groups = { { skipSubPath: options?.skipSubPath } ); }, + newAlertRuleLink: (folderName?: string, folderUid?: string, groupName?: string) => { + const returnTo = createReturnTo(); + + const defaults = JSON.stringify({ + folder: { + title: folderName, + uid: folderUid, + }, + group: groupName, + }); + + return createRelativeUrl('/alerting/new', { defaults, returnTo }); + }, + newRecordingRuleLink: (folderName?: string, folderUid?: string, groupName?: string) => { + const returnTo = createReturnTo(); + + const defaults = JSON.stringify({ + folder: { + title: folderName, + uid: folderUid, + }, + group: groupName, + }); + + return createRelativeUrl('/alerting/new/grafana-recording', { defaults, returnTo }); + }, }; export const rulesNav = { diff --git a/public/app/features/alerting/unified/utils/notifier-types.ts b/public/app/features/alerting/unified/utils/notifier-types.ts index f7c9e0054bc..eeb2f30dae9 100644 --- a/public/app/features/alerting/unified/utils/notifier-types.ts +++ b/public/app/features/alerting/unified/utils/notifier-types.ts @@ -1,4 +1,4 @@ -import { NotificationChannelOption } from '../../../../types'; +import { NotificationChannelOption } from 'app/types/alerting'; export function option( propertyName: string, diff --git a/public/app/features/alerting/unified/utils/receiver-form.ts b/public/app/features/alerting/unified/utils/receiver-form.ts index c5ae3b55263..689bfe07c26 100644 --- a/public/app/features/alerting/unified/utils/receiver-form.ts +++ b/public/app/features/alerting/unified/utils/receiver-form.ts @@ -7,7 +7,7 @@ import { GrafanaManagedReceiverSecureFields, Receiver, } from 'app/plugins/datasource/alertmanager/types'; -import { CloudNotifierType, NotificationChannelOption, NotifierDTO, NotifierType } from 'app/types'; +import { CloudNotifierType, NotificationChannelOption, NotifierDTO, NotifierType } from 'app/types/alerting'; import { ChannelValues, diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index bbd7a08db95..033be738e49 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -168,6 +168,10 @@ export function isProvisionedRule(rulerRule: RulerRuleDTO): boolean { return isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance); } +export function isProvisionedPromRule(promRule: PromRuleDTO): boolean { + return prometheusRuleType.grafana.rule(promRule) && Boolean(promRule.provenance); +} + export function isProvisionedRuleGroup(group: RulerRuleGroupDTO): boolean { return group.rules.some((rule) => isProvisionedRule(rule)); } @@ -529,3 +533,22 @@ export function isRecordingRuleByType(type?: RuleFormType) { export function isDataSourceManagedRuleByType(type?: RuleFormType) { return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type); } + +/* + * Grab the UID from either a rulerRule definition or a Prometheus rule definition, only Grafana-managed rules will have a UID. + */ +export function getRuleUID(rule?: RulerRuleDTO | Rule) { + if (!rule) { + return; + } + + let ruleUid: string | undefined; + + if ('grafana_alert' in rule && rulerRuleType.grafana.rule(rule)) { + ruleUid = rule.grafana_alert.uid; + } else if ('uid' in rule && prometheusRuleType.grafana.rule(rule)) { + ruleUid = rule.uid; + } + + return ruleUid; +} diff --git a/public/app/features/annotations/components/AnnotationResultMapper.tsx b/public/app/features/annotations/components/AnnotationResultMapper.tsx index a78711d465a..b572ab13e40 100644 --- a/public/app/features/annotations/components/AnnotationResultMapper.tsx +++ b/public/app/features/annotations/components/AnnotationResultMapper.tsx @@ -10,10 +10,10 @@ import { AnnotationEventFieldSource, getValueFormat, } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { Trans, t } from '@grafana/i18n'; import { Select, Tooltip, Icon } from '@grafana/ui'; -import { annotationEventNames, AnnotationFieldInfo } from '../standardAnnotationSupport'; +import { getAnnotationEventNames, AnnotationFieldInfo } from '../standardAnnotationSupport'; import { AnnotationQueryResponse } from '../types'; // const valueOptions: Array<SelectableValue<AnnotationEventFieldSource>> = [ @@ -171,7 +171,10 @@ export class AnnotationFieldMapper extends PureComponent<Props, State> { onChange={(v: SelectableValue<string>) => { this.onFieldNameChange(row.key, v); }} - noOptionsMessage="Unknown field names" + noOptionsMessage={t( + 'annotations.annotation-field-mapper.noOptionsMessage-unknown-field-names', + 'Unknown field names' + )} allowCustomValue={true} isClearable /> @@ -201,7 +204,7 @@ export class AnnotationFieldMapper extends PureComponent<Props, State> { </tr> </thead> <tbody> - {annotationEventNames.map((row) => { + {getAnnotationEventNames().map((row) => { return this.renderRow(row, mappings[row.key] || {}, first); })} </tbody> diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index a2caca1d583..90d28715c7f 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -17,6 +17,7 @@ import { KeyValue, standardTransformers, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { config } from 'app/core/config'; export const standardAnnotationSupport: AnnotationSupport = { @@ -97,22 +98,44 @@ export interface AnnotationFieldInfo { } // These fields get added to the standard UI -export const annotationEventNames: AnnotationFieldInfo[] = [ +export const getAnnotationEventNames: () => AnnotationFieldInfo[] = () => [ { key: 'time', field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.time), - placeholder: 'time, or the first time field', + placeholder: t( + 'annotations.get-annotation-event-names.placeholder.time-or-the-first-field', + '{{defaultField}}, or the first time field', + { defaultField: 'time' } + ), + }, + { + key: 'timeEnd', + // label: 'end time', + help: t( + 'annotations.get-annotation-event-names.help.annotation-treated-as-range', + 'When this field is defined, the annotation will be treated as a range' + ), }, - { key: 'timeEnd', label: 'end time', help: 'When this field is defined, the annotation will be treated as a range' }, { key: 'title', }, { key: 'text', field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.string), - placeholder: 'text, or the first text field', + placeholder: t( + 'annotations.get-annotation-event-names.placeholder.text-or-the-first-field', + '{{defaultField}}, or the first text field', + { defaultField: 'text' } + ), + }, + { + key: 'tags', + split: ',', + help: t( + 'annotations.get-annotation-event-names.help.results-split-on-comma', + 'The results will be split on comma (,)' + ), }, - { key: 'tags', split: ',', help: 'The results will be split on comma (,)' }, { key: 'id', }, @@ -134,7 +157,7 @@ export const publicDashboardEventNames: AnnotationFieldInfo[] = [ // pipeline, but include fields that should not be exposed generally const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [ ...(config.publicDashboardAccessToken ? publicDashboardEventNames : []), - ...annotationEventNames, + ...getAnnotationEventNames(), { key: 'userId' }, { key: 'login' }, { key: 'email' }, diff --git a/public/app/features/auth-config/AuthDrawer.tsx b/public/app/features/auth-config/AuthDrawer.tsx index 06d1ebc948c..e6a3892bc1c 100644 --- a/public/app/features/auth-config/AuthDrawer.tsx +++ b/public/app/features/auth-config/AuthDrawer.tsx @@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Button, Drawer, Text, TextLink, Switch, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { loadSettings, saveSettings } from './state/actions'; diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index d0ae5e73ab7..37fdf7c5cfe 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -7,7 +7,7 @@ import { reportInteraction } from '@grafana/runtime'; import { Grid, TextLink, ToolbarButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { config } from 'app/core/config'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import AuthDrawer from './AuthDrawer'; import ConfigureAuthCTA from './components/ConfigureAuthCTA'; diff --git a/public/app/features/auth-config/ErrorContainer.tsx b/public/app/features/auth-config/ErrorContainer.tsx index 79cf6514569..336dde9716a 100644 --- a/public/app/features/auth-config/ErrorContainer.tsx +++ b/public/app/features/auth-config/ErrorContainer.tsx @@ -1,7 +1,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { Alert } from '@grafana/ui'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { resetError, resetWarning } from './state/reducers'; diff --git a/public/app/features/auth-config/ProviderConfigForm.tsx b/public/app/features/auth-config/ProviderConfigForm.tsx index 8f1e3f80b91..861fe6acde9 100644 --- a/public/app/features/auth-config/ProviderConfigForm.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.tsx @@ -263,7 +263,7 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf </small> </Stack> } - confirmText="Reset" + confirmText={t('auth-config.provider-config-form.confirmText-reset', 'Reset')} onDismiss={() => setResetConfig(false)} onConfirm={async () => { await onResetConfig(); diff --git a/public/app/features/auth-config/ProviderConfigPage.tsx b/public/app/features/auth-config/ProviderConfigPage.tsx index 617626660fa..34614ddb746 100644 --- a/public/app/features/auth-config/ProviderConfigPage.tsx +++ b/public/app/features/auth-config/ProviderConfigPage.tsx @@ -6,7 +6,7 @@ import { t } from '@grafana/i18n'; import { Badge, Stack, Text } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { ProviderConfigForm } from './ProviderConfigForm'; import { UIMap } from './constants'; diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 6b0edf29549..ce38363d3d9 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -9,7 +9,7 @@ import { contextSrv } from 'app/core/core'; import { ServerDiscoveryField } from './components/ServerDiscoveryField'; import { FieldData, SSOProvider, SSOSettingsField } from './types'; import { isSelectableValue, isSelectableValueArray } from './utils/guards'; -import { isUrlValid } from './utils/url'; +import { isUrlValid, isValidDomain } from './utils/url'; type Section = Record< SSOProvider['provider'], @@ -59,6 +59,7 @@ export const getSectionFields = (): Section => { 'allowedDomains', 'allowedGroups', 'forceUseGraphApi', + 'domainHint', 'usePkce', 'useRefreshToken', 'tlsSkipVerifyInsecure', @@ -872,6 +873,23 @@ export function fieldMap(provider: string): Record<string, FieldData> { type: 'custom', content: (setValue) => <ServerDiscoveryField setValue={setValue} />, }, + domainHint: { + label: t('auth-config.fields.domain-hint-label', 'Domain hint'), + description: t( + 'auth-config.fields.domain-hint-description', + 'Parameter to indicate the realm of the user in the Azure AD/Entra ID tenant and streamline the login process.' + ), + type: 'text', + validation: { + validate: (value) => { + if (typeof value === 'string' && value.length) { + return isValidDomain(value); + } + return true; + }, + message: t('auth-config.fields.domain-hint-valid-domain', 'This field must be a valid domain.'), + }, + }, }; } diff --git a/public/app/features/auth-config/index.ts b/public/app/features/auth-config/index.ts index c927b22e701..69113401661 100644 --- a/public/app/features/auth-config/index.ts +++ b/public/app/features/auth-config/index.ts @@ -1,7 +1,8 @@ import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AccessControlAction, Settings, SettingsSection } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Settings, SettingsSection } from 'app/types/settings'; import { AuthProviderInfo, GetStatusHook, AuthProviderStatus } from './types'; diff --git a/public/app/features/auth-config/state/actions.ts b/public/app/features/auth-config/state/actions.ts index 074cb6b894f..cf1cdd8d7f7 100644 --- a/public/app/features/auth-config/state/actions.ts +++ b/public/app/features/auth-config/state/actions.ts @@ -2,7 +2,9 @@ import { lastValueFrom } from 'rxjs'; import { getBackendSrv, isFetchError } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Settings, ThunkResult, UpdateSettingsQuery } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Settings, UpdateSettingsQuery } from 'app/types/settings'; +import { ThunkResult } from 'app/types/store'; import { getAuthProviderStatus, getRegisteredAuthProviders, SSOProvider } from '..'; import { AuthProviderStatus, SettingsError } from '../types'; diff --git a/public/app/features/auth-config/state/reducers.ts b/public/app/features/auth-config/state/reducers.ts index 0194ed538d0..0dac91d70a6 100644 --- a/public/app/features/auth-config/state/reducers.ts +++ b/public/app/features/auth-config/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Settings } from 'app/types'; +import { Settings } from 'app/types/settings'; import { SettingsError, AuthProviderStatus, AuthConfigState, SSOProvider } from '../types'; diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts index b14336f06ab..933642c82d5 100644 --- a/public/app/features/auth-config/types.ts +++ b/public/app/features/auth-config/types.ts @@ -2,7 +2,7 @@ import { ReactElement } from 'react'; import { Validate, UseFormSetValue } from 'react-hook-form'; import { IconName, SelectableValue } from '@grafana/data'; -import { Settings } from 'app/types'; +import { Settings } from 'app/types/settings'; export interface AuthProviderInfo { id: string; type: string; @@ -60,6 +60,7 @@ export type SSOProviderSettingsBase = { tlsSkipVerifyInsecure?: boolean; // For Azure AD forceUseGraphApi?: boolean; + domainHint?: string; // For Google validateHd?: boolean; }; diff --git a/public/app/features/auth-config/utils/url.ts b/public/app/features/auth-config/utils/url.ts index cf98cf311c2..57371141ac4 100644 --- a/public/app/features/auth-config/utils/url.ts +++ b/public/app/features/auth-config/utils/url.ts @@ -16,3 +16,13 @@ export const isUrlValid = (url: unknown): boolean => { return false; } }; + +export const isValidDomain = (domain: string): boolean => { + if (typeof domain !== 'string' || !domain.length) { + return false; + } + + const domainRegex = + /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,6}$/; + return domainRegex.test(domain); +}; diff --git a/public/app/features/bookmarks/BookmarksPage.tsx b/public/app/features/bookmarks/BookmarksPage.tsx index 6a4c1b704df..6b7f9ab08ae 100644 --- a/public/app/features/bookmarks/BookmarksPage.tsx +++ b/public/app/features/bookmarks/BookmarksPage.tsx @@ -7,7 +7,7 @@ import { usePinnedItems } from 'app/core/components/AppChrome/MegaMenu/hooks'; import { findByUrl } from 'app/core/components/AppChrome/MegaMenu/utils'; import { NavLandingPageCard } from 'app/core/components/NavLandingPage/NavLandingPageCard'; import { Page } from 'app/core/components/Page/Page'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; export function BookmarksPage() { const styles = useStyles2(getStyles); diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 36182de2d44..a2bfe9ca651 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -163,17 +163,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => { describe('at the root level', () => { it('displays "Dashboards" as the page title', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); }); it('displays a search input', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('shows the "New" button', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); }); @@ -185,25 +185,25 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolders: false, }; }); - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); }); it('does not show "Folder actions"', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('does not show an "Edit title" button', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); }); it('does not show any tabs', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('tab', { name: 'Dashboards' })).not.toBeInTheDocument(); @@ -212,7 +212,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('displays the filters and hides the actions initially', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); await screen.findByPlaceholderText('Search for dashboards and folders'); expect(await screen.findByText('Sort')).toBeInTheDocument(); @@ -223,7 +223,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('selecting an item hides the filters and shows the actions instead', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid)); await userEvent.click(checkbox); @@ -238,7 +238,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('navigating into a child item resets the selected state', async () => { - const { rerender } = render(<BrowseDashboardsPage />); + const { rerender } = render(<BrowseDashboardsPage queryParams={{}} />); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid)); await userEvent.click(checkbox); @@ -248,7 +248,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); (useParams as jest.Mock).mockReturnValue({ uid: folderA.item.uid }); - rerender(<BrowseDashboardsPage />); + rerender(<BrowseDashboardsPage queryParams={{}} />); // Check the filters are now visible again expect(await screen.findByText('Filter by tag')).toBeInTheDocument(); @@ -266,17 +266,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('shows the folder name as the page title', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); }); it('displays a search input', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('shows the "New" button', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('button', { name: 'New' })).toBeInTheDocument(); }); @@ -288,13 +288,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canCreateFolders: false, }; }); - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); }); it('shows the "Folder actions" button', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('button', { name: 'Folder actions' })).toBeInTheDocument(); }); @@ -308,13 +308,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canViewPermissions: false, }; }); - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); }); it('shows an "Edit title" button', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('button', { name: 'Edit title' })).toBeInTheDocument(); }); @@ -325,13 +325,13 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canEditFolders: false, }; }); - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); }); it('displays all the folder tabs and shows the "Dashboards" tab as selected', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toBeInTheDocument(); expect(await screen.findByRole('tab', { name: 'Dashboards' })).toHaveAttribute('aria-selected', 'true'); @@ -343,7 +343,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('displays the filters and hides the actions initially', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); await screen.findByPlaceholderText('Search for dashboards and folders'); expect(await screen.findByText('Sort')).toBeInTheDocument(); @@ -354,7 +354,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('selecting an item hides the filters and shows the actions instead', async () => { - render(<BrowseDashboardsPage />); + render(<BrowseDashboardsPage queryParams={{}} />); const checkbox = await screen.findByTestId( selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 15fd546a36b..e392e6a3f53 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -10,7 +10,7 @@ import { config, reportInteraction } from '@grafana/runtime'; import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo'; import { contextSrv } from '../../core/services/context_srv'; @@ -25,12 +25,14 @@ import { BrowseFilters } from './components/BrowseFilters'; import { BrowseView } from './components/BrowseView'; import CreateNewButton from './components/CreateNewButton'; import { FolderActionsButton } from './components/FolderActionsButton'; +import { ProvisionedFolderPreviewBanner } from './components/ProvisionedFolderPreviewBanner'; import { SearchView } from './components/SearchView'; import { getFolderPermissions } from './permissions'; -import { setAllSelection, useHasSelection } from './state'; +import { useHasSelection } from './state/hooks'; +import { setAllSelection } from './state/slice'; // New Browse/Manage/Search Dashboards views for nested folders -const BrowseDashboardsPage = memo(() => { +const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string, string> }) => { const { uid: folderUID } = useParams(); const dispatch = useDispatch(); @@ -158,6 +160,7 @@ const BrowseDashboardsPage = memo(() => { } > <Page.Contents className={styles.pageContents}> + <ProvisionedFolderPreviewBanner queryParams={queryParams} /> <div> <FilterInput placeholder={getSearchPlaceholder(searchState.includePanels)} diff --git a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx index 3e6a25cfadf..3e18b71d399 100644 --- a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx @@ -8,15 +8,15 @@ import { FilterInput, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { ActionRow } from 'app/features/search/page/components/ActionRow'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; - -import { useDispatch } from '../../types'; +import { useDispatch } from 'app/types/store'; import { useRecentlyDeletedStateManager } from './api/useRecentlyDeletedStateManager'; import { RecentlyDeletedActions } from './components/RecentlyDeletedActions'; import { RecentlyDeletedEmptyState } from './components/RecentlyDeletedEmptyState'; import { SearchView } from './components/SearchView'; import { getFolderPermissions } from './permissions'; -import { setAllSelection, useHasSelection } from './state'; +import { useHasSelection } from './state/hooks'; +import { setAllSelection } from './state/slice'; const RecentlyDeletedPage = memo(() => { const dispatch = useDispatch(); diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 22ef7ed3ba2..d71d0c57be2 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -15,17 +15,12 @@ import { isDashboardV2Resource, isV1DashboardCommand, isV2DashboardCommand } fro import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dispatch } from 'app/store/store'; -import { - DescendantCount, - DescendantCountDTO, - FolderDTO, - FolderListItemDTO, - ImportDashboardResponseDTO, - PermissionLevelString, - SaveDashboardResponseDTO, -} from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; +import { SaveDashboardResponseDTO, ImportDashboardResponseDTO } from 'app/types/dashboard'; +import { FolderListItemDTO, FolderDTO, DescendantCount, DescendantCountDTO } from 'app/types/folders'; -import { refetchChildren, refreshParents } from '../state'; +import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager'; +import { refetchChildren, refreshParents } from '../state/actions'; import { DashboardTreeSelection } from '../types'; import { isProvisionedDashboard, isProvisionedFolder } from './isProvisioned'; @@ -293,6 +288,7 @@ export const browseDashboardsAPI = createApi({ queryFn: async ({ selectedItems }, _api, _extraOptions, baseQuery) => { const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]); const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); + const pageStateManager = getDashboardScenePageStateManager(); // Delete all the folders sequentially // TODO error handling here for (const folderUID of selectedFolders) { @@ -337,6 +333,9 @@ export const browseDashboardsAPI = createApi({ await getDashboardAPI().deleteDashboard(dashboardUID, true); + pageStateManager.clearDashboardCache(); + pageStateManager.removeSceneCache(dashboardUID); + // handling success alerts for these feature toggles // for legacy response, the success alert will be triggered by showSuccessAlert function in public/app/core/services/backend_srv.ts if (config.featureToggles.kubernetesDashboards) { diff --git a/public/app/features/browse-dashboards/api/isProvisioned.ts b/public/app/features/browse-dashboards/api/isProvisioned.ts index 7df88e77de3..f0b948d4d5e 100644 --- a/public/app/features/browse-dashboards/api/isProvisioned.ts +++ b/public/app/features/browse-dashboards/api/isProvisioned.ts @@ -1,6 +1,6 @@ import { Folder } from 'app/api/clients/folder/v1beta1'; import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from '../../dashboard/api/types'; diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts index c5eb2375b65..a6a511ba0f7 100644 --- a/public/app/features/browse-dashboards/api/services.ts +++ b/public/app/features/browse-dashboards/api/services.ts @@ -4,9 +4,9 @@ import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { NestedFolderDTO } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; import { DashboardViewItem } from 'app/features/search/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { contextSrv } from '../../../core/core'; -import { AccessControlAction } from '../../../types'; import { getFolderURL, isSharedWithMe } from '../components/utils'; export const PAGE_SIZE = 50; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index 87168ed0394..3dd60457c77 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -5,11 +5,12 @@ import { config, reportInteraction } from '@grafana/runtime'; import { Button, Stack, Tooltip } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; -import { useDispatch } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI'; -import { setAllSelection, useActionSelectionState } from '../../state'; +import { useActionSelectionState } from '../../state/hooks'; +import { setAllSelection } from '../../state/slice'; import { DashboardTreeSelection } from '../../types'; import { DeleteModal } from './DeleteModal'; diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index 2b9adfebd9d..b20fa522fc7 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -3,20 +3,18 @@ import { useCallback } from 'react'; import { Trans, t } from '@grafana/i18n'; import { CallToActionCard, EmptyState, LinkButton, TextLink } from '@grafana/ui'; import { DashboardViewItem } from 'app/features/search/types'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { PAGE_SIZE } from '../api/services'; +import { fetchNextChildrenPage } from '../state/actions'; import { useFlatTreeState, useCheckboxSelectionState, - setFolderOpenState, - setItemSelectionState, useChildrenByParentUIDState, - setAllSelection, useBrowseLoadingStatus, useLoadNextChildrenPage, - fetchNextChildrenPage, -} from '../state'; +} from '../state/hooks'; +import { setFolderOpenState, setItemSelectionState, setAllSelection } from '../state/slice'; import { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types'; import { DashboardsTree } from './DashboardsTree'; diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx index 902f3b0ef2a..e5635cdfd4d 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx @@ -2,7 +2,7 @@ import { render as rtlRender, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { mockFolderDTO } from '../fixtures/folder.fixture'; diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx index bb00a254d31..d1c487da4f8 100644 --- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx +++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx @@ -12,7 +12,7 @@ import { getNewFolderPhrase, getNewPhrase, } from 'app/features/search/tempI18nPhrases'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { ManagerKind } from '../../apiserver/types'; import { useNewFolderMutation } from '../api/browseDashboardsAPI'; diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx index f56e50bf023..01d9216ff5f 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; @@ -16,6 +16,11 @@ jest.mock('@grafana/runtime', () => ({ })), })); +const mockNavigate = jest.fn(); +jest.mock('react-router-dom-v5-compat', () => ({ + useNavigate: () => mockNavigate, +})); + jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ useDeleteRepositoryFilesWithPathMutation: jest.fn(), provisioningAPI: { @@ -140,6 +145,7 @@ function setup( ...renderResult, onDismiss, mockDeleteRepoFile, + mockNavigate, clickDeleteButton, }; } @@ -147,6 +153,7 @@ function setup( describe('DeleteProvisionedFolderForm', () => { beforeEach(() => { jest.clearAllMocks(); + mockNavigate.mockClear(); jest.spyOn(console, 'error').mockImplementation(() => {}); // Mock window.location.href Object.defineProperty(window, 'location', { @@ -272,13 +279,21 @@ describe('DeleteProvisionedFolderForm', () => { }); }); - it('should handle branch workflow success without navigation', async () => { + it('should handle branch workflow success with navigation', async () => { const branchFormData = { ...mockFormData, workflow: 'branch' } as unknown as typeof mockFormData; - const successState = { isLoading: false, isSuccess: true, isError: false, error: null }; - setup({}, { ...defaultHookData, initialValues: branchFormData }, successState); + const successState = { + isLoading: false, + isSuccess: true, + isError: false, + error: null, + data: { urls: { newPullRequestURL: 'https://github.com/test/repo/pull/new' } }, + }; + const { mockNavigate } = setup({}, { ...defaultHookData, initialValues: branchFormData }, successState); await waitFor(() => { - expect(window.location.href).toBe(''); + expect(mockNavigate).toHaveBeenCalledWith( + '/dashboards?new_pull_request_url=https://github.com/test/repo/pull/new' + ); }); }); }); diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx index 995f68bbd23..2491b7bc506 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -10,7 +11,7 @@ import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/ap import { AnnoKeySourcePath } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; @@ -42,6 +43,7 @@ function FormContent({ const resourceId = parentFolder?.uid || ''; const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation(); + const navigate = useNavigate(); const methods = useForm<BaseProvisionedFormData>({ defaultValues: initialValues }); const { handleSubmit, watch } = methods; @@ -66,9 +68,9 @@ function FormContent({ // TODO: move to a hook if this useEffect shared mostly the same logic as in NewProvisionedFolderForm useEffect(() => { if (request.isSuccess && repository) { - if (workflow === 'branch') { - // TODO: handle display banner https://github.com/grafana/git-ui-sync-project/issues/300 - // TODO: implement when BE is ready + const prUrl = request.data?.urls?.newPullRequestURL; + if (workflow === 'branch' && prUrl) { + navigate(`/dashboards?new_pull_request_url=${prUrl}`); return; } @@ -101,7 +103,7 @@ function FormContent({ }); return; } - }, [request, repository, workflow, parentFolder]); + }, [request, repository, workflow, parentFolder, navigate]); return ( <FormProvider {...methods}> diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx index d5cef0b3aa1..dcb798a9749 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx @@ -5,8 +5,8 @@ import { config, locationService, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { Permissions } from 'app/core/components/AccessControl'; import { appEvents } from 'app/core/core'; -import { FolderDTO } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { FolderDTO } from 'app/types/folders'; import { ManagerKind } from '../../apiserver/types'; import { useDeleteFolderMutation, useMoveFolderMutation } from '../api/browseDashboardsAPI'; @@ -103,8 +103,7 @@ export function FolderActionsButton({ folder }: Props) { <Menu> {canViewPermissions && <MenuItem onClick={() => setShowPermissionsDrawer(true)} label={managePermissionsLabel} />} {canMoveFolder && <MenuItem onClick={showMoveModal} label={moveLabel} />} - {/* TODO: remove isProvisionedFolder check once BE folder delete flow is complete */} - {canDeleteFolders && !isProvisionedFolder && ( + {canDeleteFolders && ( <MenuItem destructive onClick={isProvisionedFolder ? showDeleteProvisionedModal : showDeleteModal} diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index c85d98d5f03..3db558966a2 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -10,7 +10,7 @@ import { getIconForItem } from 'app/features/search/service/utils'; import { Indent } from '../../../core/components/Indent/Indent'; import { FolderRepo } from '../../../core/components/NestedFolderPicker/FolderRepo'; -import { useChildrenByParentUIDState } from '../state'; +import { useChildrenByParentUIDState } from '../state/hooks'; import { DashboardsTreeCellProps } from '../types'; import { makeRowID } from './utils'; diff --git a/public/app/features/browse-dashboards/components/NewFolderForm.tsx b/public/app/features/browse-dashboards/components/NewFolderForm.tsx index 25ee5b5282f..a5c8fdb1043 100644 --- a/public/app/features/browse-dashboards/components/NewFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewFolderForm.tsx @@ -28,18 +28,6 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { 'browse-dashboards.action.new-folder-name-required-phrase', 'Folder name is required.' ); - const validateFolderName = async (folderName: string) => { - try { - await validationSrv.validateNewFolderName(folderName); - return true; - } catch (e) { - if (e instanceof Error) { - return e.message; - } else { - throw e; - } - } - }; const fieldNameLabel = t('browse-dashboards.new-folder-form.name-label', 'Folder name'); @@ -75,3 +63,16 @@ export function NewFolderForm({ onCancel, onConfirm }: Props) { </form> ); } + +export async function validateFolderName(folderName: string) { + try { + await validationSrv.validateNewFolderName(folderName); + return true; + } catch (e) { + if (e instanceof Error) { + return e.message; + } else { + throw e; + } + } +} diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx index 9f44b30f384..a983a66ada0 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx @@ -6,8 +6,8 @@ import { getAppEvents } from '@grafana/runtime'; import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; +import { FolderDTO } from 'app/types/folders'; -import { FolderDTO } from '../../../types'; import { ProvisionedFolderFormDataResult, useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; import { NewProvisionedFolderForm } from './NewProvisionedFolderForm'; @@ -161,7 +161,7 @@ describe('NewProvisionedFolderForm', () => { (getAppEvents as jest.Mock).mockReturnValue(mockAppEvents); // Mock usePullRequestParam - (usePullRequestParam as jest.Mock).mockReturnValue(null); + (usePullRequestParam as jest.Mock).mockReturnValue({}); // Mock useCreateRepositoryFilesWithPathMutation const mockCreate = jest.fn(); @@ -409,7 +409,7 @@ describe('NewProvisionedFolderForm', () => { }); it('should show PR link when PR URL is available', () => { - (usePullRequestParam as jest.Mock).mockReturnValue('https://github.com/grafana/grafana/pull/1234'); + (usePullRequestParam as jest.Mock).mockReturnValue({ prURL: 'https://github.com/grafana/grafana/pull/1234' }); setup(); diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx index ed03a23cb7a..f7f141fac01 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx @@ -1,22 +1,26 @@ +import { css } from '@emotion/css'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { AppEvents } from '@grafana/data'; +import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { getAppEvents } from '@grafana/runtime'; -import { Alert, Button, Field, Input, Stack } from '@grafana/ui'; +import { Alert, Text, Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; import { Folder } from 'app/api/clients/folder/v1beta1'; import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; -import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; + +import { validateFolderName } from './NewFolderForm'; +import { formatFolderName, hasFolderNameCharactersToReplace } from './utils'; + interface FormProps extends Props { initialValues: BaseProvisionedFormData; repository?: RepositoryView; @@ -30,7 +34,7 @@ interface Props { } function FormContent({ initialValues, repository, workflowOptions, folder, isGitHub, onDismiss }: FormProps) { - const prURL = usePullRequestParam(); + const { prURL } = usePullRequestParam(); const navigate = useNavigate(); const [create, request] = useCreateRepositoryFilesWithPathMutation(); @@ -40,7 +44,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit }); const { handleSubmit, watch, register, formState } = methods; - const [workflow, ref] = watch(['workflow', 'ref']); + const [workflow, ref, title] = watch(['workflow', 'ref', 'title']); // TODO: replace with useProvisionedRequestHandler hook useEffect(() => { @@ -82,18 +86,6 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit } }, [request.isSuccess, request.isError, request.error, ref, request.data, workflow, navigate, repository, onDismiss]); - const validateFolderName = async (folderName: string) => { - try { - await validationSrv.validateNewFolderName(folderName); - return true; - } catch (e) { - if (e instanceof Error) { - return e.message; - } - return t('browse-dashboards.new-provisioned-folder-form.error-invalid-folder-name', 'Invalid folder name'); - } - }; - const doSave = async ({ ref, title, workflow, comment }: BaseProvisionedFormData) => { const repoName = repository?.name; if (!title || !repoName) { @@ -102,10 +94,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit const basePath = folder?.metadata?.annotations?.[AnnoKeySourcePath] ?? ''; // Convert folder title to filename format (lowercase, replace spaces with hyphens) - const titleInFilenameFormat = title - .toLowerCase() - .replace(/\s+/g, '-') - .replace(/[^a-z0-9-]/g, ''); + const titleInFilenameFormat = formatFolderName(title); // TODO: this is currently not working, issue created https://github.com/grafana/git-ui-sync-project/issues/314 const prefix = basePath ? `${basePath}/` : ''; const path = `${prefix}${titleInFilenameFormat}/`; @@ -163,6 +152,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit id="folder-name-input" /> </Field> + <FolderNamePreviewMessage folderName={title} /> <ResourceEditFormSharedFields resourceType="folder" @@ -210,7 +200,7 @@ export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) { const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({ folderUid: parentFolder?.uid, action: 'create', - title: parentFolder?.title, + title: '', // Empty title for new folders }); if (!initialValues) { @@ -229,3 +219,39 @@ export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) { /> ); } + +function FolderNamePreviewMessage({ folderName }: { folderName: string }) { + const styles = useStyles2(getStyles); + const isValidFolderName = + folderName.length && hasFolderNameCharactersToReplace(folderName) && validateFolderName(folderName); + + if (!isValidFolderName) { + return null; + } + + return ( + <div className={styles.folderNameMessage}> + <Icon name="check-circle" type="solid" /> + <Text color="success"> + {t( + 'browse-dashboards.new-provisioned-folder-form.text-your-folder-will-be-created-as', + 'Your folder will be created as {{folderName}}', + { + folderName: formatFolderName(folderName), + } + )} + </Text> + </div> + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + folderNameMessage: css({ + display: 'flex', + alignItems: 'center', + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.success.text, + }), + }; +}; diff --git a/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx b/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx new file mode 100644 index 00000000000..32068d49077 --- /dev/null +++ b/public/app/features/browse-dashboards/components/ProvisionedFolderPreviewBanner.tsx @@ -0,0 +1,23 @@ +import { config } from '@grafana/runtime'; +import { CommonBannerProps } from 'app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner'; +import { PreviewBannerViewPR } from 'app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR'; +import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; + +export function ProvisionedFolderPreviewBanner({ queryParams }: CommonBannerProps) { + const provisioningEnabled = config.featureToggles.provisioning; + const { prURL, newPrURL } = usePullRequestParam(); + + if (!provisioningEnabled || 'kiosk' in queryParams) { + return null; + } + + if (prURL) { + return <PreviewBannerViewPR prParam={prURL} />; + } + + if (newPrURL) { + return <PreviewBannerViewPR prParam={newPrURL} isNewPr />; + } + + return null; +} diff --git a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx index 6f553f478b7..18cc800213e 100644 --- a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx @@ -6,13 +6,14 @@ import { Button, Stack } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; -import { useDispatch } from 'app/types'; import { ShowModalReactEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { deletedDashboardsCache } from '../../search/service/deletedDashboardsCache'; import { useListDeletedDashboardsQuery, useRestoreDashboardMutation } from '../api/browseDashboardsAPI'; import { useRecentlyDeletedStateManager } from '../api/useRecentlyDeletedStateManager'; -import { clearFolders, setAllSelection, useActionSelectionState } from '../state'; +import { useActionSelectionState } from '../state/hooks'; +import { clearFolders, setAllSelection } from '../state/slice'; import { RestoreModal } from './RestoreModal'; diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx index 014a417c603..281455f140e 100644 --- a/public/app/features/browse-dashboards/components/SearchView.tsx +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -7,9 +7,10 @@ import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKey import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable'; import { SearchStateManager } from 'app/features/search/state/SearchStateManager'; import { DashboardViewItemKind, SearchState } from 'app/features/search/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; -import { setAllSelection, setItemSelectionState, useHasSelection } from '../state'; +import { useHasSelection } from '../state/hooks'; +import { setAllSelection, setItemSelectionState } from '../state/slice'; interface SearchViewProps { height: number; diff --git a/public/app/features/browse-dashboards/components/utils.test.ts b/public/app/features/browse-dashboards/components/utils.test.ts new file mode 100644 index 00000000000..2bf48e3d329 --- /dev/null +++ b/public/app/features/browse-dashboards/components/utils.test.ts @@ -0,0 +1,124 @@ +import { formatFolderName, hasFolderNameCharactersToReplace } from './utils'; + +describe('formatFolderName', () => { + it('should handle empty string', () => { + expect(formatFolderName('')).toBe(''); + }); + + it('should convert uppercase to lowercase', () => { + expect(formatFolderName('MyFolder')).toBe('myfolder'); + expect(formatFolderName('UPPERCASE')).toBe('uppercase'); + expect(formatFolderName('MiXeD cAsE')).toBe('mixed-case'); + }); + + it('should replace whitespace with hyphens', () => { + expect(formatFolderName('folder name')).toBe('folder-name'); + expect(formatFolderName('folder name')).toBe('folder-name'); // multiple spaces + expect(formatFolderName('folder\tname')).toBe('folder-name'); // tab + expect(formatFolderName('folder\nname')).toBe('folder-name'); // newline + expect(formatFolderName(' folder name ')).toBe('folder-name'); // leading/trailing spaces + }); + + it('should remove special characters', () => { + expect(formatFolderName('folder@name')).toBe('foldername'); + expect(formatFolderName('folder!@#$%^&*()name')).toBe('foldername'); + expect(formatFolderName('folder_name')).toBe('foldername'); + expect(formatFolderName('folder.name')).toBe('foldername'); + expect(formatFolderName('folder/name')).toBe('foldername'); + }); + + it('should preserve numbers and hyphens', () => { + expect(formatFolderName('folder-123')).toBe('folder-123'); + expect(formatFolderName('folder123')).toBe('folder123'); + expect(formatFolderName('123-folder')).toBe('123-folder'); + expect(formatFolderName('folder-name-123')).toBe('folder-name-123'); + }); + + it('should handle complex mixed cases', () => { + expect(formatFolderName('My Folder @2023!')).toBe('my-folder-2023'); + expect(formatFolderName(' FOLDER_NAME with-123 ')).toBe('foldername-with-123'); + expect(formatFolderName('Test@Folder#Name$123')).toBe('testfoldername123'); + expect(formatFolderName('Multiple Spaces Between')).toBe('multiple-spaces-between'); + }); + + it('should handle strings with only special characters', () => { + expect(formatFolderName('!@#$%^&*()')).toBe(''); + expect(formatFolderName('___')).toBe(''); + expect(formatFolderName('...')).toBe(''); + }); + + it('should handle strings with only whitespace', () => { + expect(formatFolderName(' ')).toBe(''); + expect(formatFolderName('\t\n\r')).toBe(''); + }); + + it('should handle already formatted names', () => { + expect(formatFolderName('already-formatted')).toBe('already-formatted'); + expect(formatFolderName('folder123')).toBe('folder123'); + expect(formatFolderName('test-folder-name-123')).toBe('test-folder-name-123'); + }); +}); + +describe('hasFolderNameCharactersToReplace', () => { + it('should return false for non-string inputs', () => { + // @ts-expect-error + expect(hasFolderNameCharactersToReplace(null)).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace(undefined)).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace(123)).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace({})).toBe(false); + // @ts-expect-error + expect(hasFolderNameCharactersToReplace([])).toBe(false); + }); + + it('should return false for empty string', () => { + expect(hasFolderNameCharactersToReplace('')).toBe(false); + }); + + it('should return false for valid folder names', () => { + expect(hasFolderNameCharactersToReplace('validname')).toBe(false); + expect(hasFolderNameCharactersToReplace('folder123')).toBe(false); + expect(hasFolderNameCharactersToReplace('test-folder-name')).toBe(false); + expect(hasFolderNameCharactersToReplace('folder-123')).toBe(false); + expect(hasFolderNameCharactersToReplace('123-folder')).toBe(false); + expect(hasFolderNameCharactersToReplace('a')).toBe(false); + expect(hasFolderNameCharactersToReplace('1')).toBe(false); + }); + + it('should return true for names with whitespace', () => { + expect(hasFolderNameCharactersToReplace('folder name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder\tname')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder\nname')).toBe(true); + expect(hasFolderNameCharactersToReplace(' folder')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder ')).toBe(true); + expect(hasFolderNameCharactersToReplace(' ')).toBe(true); + }); + + it('should return true for names with uppercase letters', () => { + expect(hasFolderNameCharactersToReplace('FolderName')).toBe(true); + expect(hasFolderNameCharactersToReplace('UPPERCASE')).toBe(true); + expect(hasFolderNameCharactersToReplace('MiXeD')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder-Name')).toBe(true); + }); + + it('should return true for names with special characters', () => { + expect(hasFolderNameCharactersToReplace('folder@name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder!name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder_name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder.name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder/name')).toBe(true); + expect(hasFolderNameCharactersToReplace('folder#name')).toBe(true); + }); + + it('should return true for mixed cases with multiple issues', () => { + expect(hasFolderNameCharactersToReplace('Test@Folder#Name$123')).toBe(true); + expect(hasFolderNameCharactersToReplace('Multiple Spaces Between')).toBe(true); + }); + + it('should return true for strings with only special characters', () => { + expect(hasFolderNameCharactersToReplace('!@#$%^&*()')).toBe(true); + }); +}); diff --git a/public/app/features/browse-dashboards/components/utils.ts b/public/app/features/browse-dashboards/components/utils.ts index af40b5e2f0f..8f29702bda7 100644 --- a/public/app/features/browse-dashboards/components/utils.ts +++ b/public/app/features/browse-dashboards/components/utils.ts @@ -22,3 +22,38 @@ export function getFolderURL(uid: string) { } return url; } + +export function hasFolderNameCharactersToReplace(folderName: string): boolean { + if (typeof folderName !== 'string') { + return false; + } + + // whitespace that needs to be replaced with hyphens + const hasWhitespace = /\s+/.test(folderName); + + // characters that are not lowercase letters, numbers, or hyphens + const hasInvalidCharacters = /[^a-z0-9-]/.test(folderName); + + return hasWhitespace || hasInvalidCharacters; +} + +export function formatFolderName(folderName?: string): string { + if (typeof folderName !== 'string') { + console.error('Invalid folder name type:', typeof folderName); + return ''; + } + + const result = folderName + .trim() // Remove leading/trailing whitespace first + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens + + // If the result is empty, return empty string + if (result === '') { + return ''; + } + + return result; +} diff --git a/public/app/features/browse-dashboards/fixtures/folder.fixture.ts b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts index 14cb3a83652..bab54a40975 100644 --- a/public/app/features/browse-dashboards/fixtures/folder.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts @@ -1,6 +1,6 @@ import { Chance } from 'chance'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; export function mockFolderDTO(seed = 1, partial?: Partial<FolderDTO>): FolderDTO { const random = Chance(seed); diff --git a/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts b/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts index 527ad6bf195..6fbc5f6c6eb 100644 --- a/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts +++ b/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts @@ -39,7 +39,7 @@ export function useProvisionedFolderFormData({ const initialValues = useMemo(() => { // Only create initial values when we have the data - if (!repository || !folder || isLoading) { + if (!repository || isLoading) { return undefined; } diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts index 49eff8d69ff..d589af9480a 100644 --- a/public/app/features/browse-dashboards/permissions.ts +++ b/public/app/features/browse-dashboards/permissions.ts @@ -1,6 +1,7 @@ import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, FolderDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { FolderDTO } from 'app/types/folders'; function checkFolderPermission(action: AccessControlAction, folderDTO?: FolderDTO) { return folderDTO ? contextSrv.hasPermissionInMetadata(action, folderDTO) : contextSrv.hasPermission(action); diff --git a/public/app/features/browse-dashboards/state/actions.ts b/public/app/features/browse-dashboards/state/actions.ts index 88073d0e518..f738493b342 100644 --- a/public/app/features/browse-dashboards/state/actions.ts +++ b/public/app/features/browse-dashboards/state/actions.ts @@ -1,6 +1,6 @@ import { GENERAL_FOLDER_UID } from 'app/features/search/constants'; import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types'; -import { createAsyncThunk } from 'app/types'; +import { createAsyncThunk } from 'app/types/store'; import { listDashboards, listFolders, PAGE_SIZE } from '../api/services'; import { DashboardViewItemWithUIItems, UIDashboardViewItem } from '../types'; diff --git a/public/app/features/browse-dashboards/state/hooks.test.ts b/public/app/features/browse-dashboards/state/hooks.test.ts index 373a6c0a0f7..d5fd74b4bb0 100644 --- a/public/app/features/browse-dashboards/state/hooks.test.ts +++ b/public/app/features/browse-dashboards/state/hooks.test.ts @@ -1,13 +1,13 @@ import { configureStore } from 'app/store/configureStore'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { fullyLoadedViewItemCollection } from '../fixtures/state.fixtures'; import { BrowseDashboardsState } from '../types'; import { useBrowseLoadingStatus } from './hooks'; -jest.mock('app/types', () => { - const original = jest.requireActual('app/types'); +jest.mock('app/types/store', () => { + const original = jest.requireActual('app/types/store'); return { ...original, useSelector: jest.fn(), diff --git a/public/app/features/browse-dashboards/state/hooks.ts b/public/app/features/browse-dashboards/state/hooks.ts index d46f0b44708..2c0bee62a98 100644 --- a/public/app/features/browse-dashboards/state/hooks.ts +++ b/public/app/features/browse-dashboards/state/hooks.ts @@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react'; import { createSelector } from 'reselect'; import { DashboardViewItem } from 'app/features/search/types'; -import { useSelector, StoreState, useDispatch } from 'app/types'; +import { useSelector, StoreState, useDispatch } from 'app/types/store'; import { PAGE_SIZE } from '../api/services'; import { isSharedWithMe } from '../components/utils'; diff --git a/public/app/features/browse-dashboards/state/index.ts b/public/app/features/browse-dashboards/state/index.ts deleted file mode 100644 index c5779d55154..00000000000 --- a/public/app/features/browse-dashboards/state/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './slice'; -export * from './actions'; -export * from './hooks'; diff --git a/public/app/features/canvas/element.ts b/public/app/features/canvas/element.ts index 7f987fed873..1821670a621 100644 --- a/public/app/features/canvas/element.ts +++ b/public/app/features/canvas/element.ts @@ -7,7 +7,7 @@ import { config } from 'app/core/config'; import { BackgroundConfig, Constraint, LineConfig, Placement } from 'app/plugins/panel/canvas/panelcfg.gen'; import { LineStyleConfig } from '../../plugins/panel/canvas/editor/LineStyleEditor'; -import { DimensionContext } from '../dimensions'; +import { DimensionContext } from '../dimensions/context'; import { StandardEditorConfig } from './types'; diff --git a/public/app/features/canvas/elements/button.tsx b/public/app/features/canvas/elements/button.tsx index d05b04531a7..749504e80ff 100644 --- a/public/app/features/canvas/elements/button.tsx +++ b/public/app/features/canvas/elements/button.tsx @@ -6,7 +6,7 @@ import { t } from '@grafana/i18n'; import { TextDimensionMode } from '@grafana/schema'; import { Button, Spinner, useStyles2 } from '@grafana/ui'; import { DimensionContext } from 'app/features/dimensions/context'; -import { ColorDimensionEditor } from 'app/features/dimensions/editors'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; import { APIEditor, APIEditorConfig } from 'app/plugins/panel/canvas/editor/element/APIEditor'; import { ButtonStyleConfig, ButtonStyleEditor } from 'app/plugins/panel/canvas/editor/element/ButtonStyleEditor'; diff --git a/public/app/features/canvas/elements/cloud.tsx b/public/app/features/canvas/elements/cloud.tsx index c4688bf7300..f215b4dfd9e 100644 --- a/public/app/features/canvas/elements/cloud.tsx +++ b/public/app/features/canvas/elements/cloud.tsx @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from 'app/core/config'; -import { DimensionContext } from 'app/features/dimensions'; +import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/elements/droneFront.tsx b/public/app/features/canvas/elements/droneFront.tsx index 628b70c3afb..f8c7a0ef1bb 100644 --- a/public/app/features/canvas/elements/droneFront.tsx +++ b/public/app/features/canvas/elements/droneFront.tsx @@ -4,8 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { ScalarDimensionConfig } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { DimensionContext } from 'app/features/dimensions'; -import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { DimensionContext } from 'app/features/dimensions/context'; +import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element'; diff --git a/public/app/features/canvas/elements/droneSide.tsx b/public/app/features/canvas/elements/droneSide.tsx index c4ec5fcc73f..9ff0a6deabb 100644 --- a/public/app/features/canvas/elements/droneSide.tsx +++ b/public/app/features/canvas/elements/droneSide.tsx @@ -4,8 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { ScalarDimensionConfig } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { DimensionContext } from 'app/features/dimensions'; -import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { DimensionContext } from 'app/features/dimensions/context'; +import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element'; diff --git a/public/app/features/canvas/elements/droneTop.tsx b/public/app/features/canvas/elements/droneTop.tsx index 864a32363e3..4994bfa0d91 100644 --- a/public/app/features/canvas/elements/droneTop.tsx +++ b/public/app/features/canvas/elements/droneTop.tsx @@ -4,8 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { ScalarDimensionConfig } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { DimensionContext } from 'app/features/dimensions'; -import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { DimensionContext } from 'app/features/dimensions/context'; +import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element'; diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index 48ea5beffd2..bf10461706c 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -6,9 +6,10 @@ import { LinkModel } from '@grafana/data'; import { t } from '@grafana/i18n'; import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema'; import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; -import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions/context'; -import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { ResourceDimensionEditor } from 'app/features/dimensions/editors/ResourceDimensionEditor'; +import { getPublicOrAbsoluteUrl } from 'app/features/dimensions/resource'; import { LineConfig } from 'app/plugins/panel/canvas/panelcfg.gen'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element'; diff --git a/public/app/features/canvas/elements/parallelogram.tsx b/public/app/features/canvas/elements/parallelogram.tsx index dc46cebf21f..2dc0df79799 100644 --- a/public/app/features/canvas/elements/parallelogram.tsx +++ b/public/app/features/canvas/elements/parallelogram.tsx @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from 'app/core/config'; -import { DimensionContext } from 'app/features/dimensions'; +import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/elements/server/server.tsx b/public/app/features/canvas/elements/server/server.tsx index 5ea7724b2f1..2ad0e4cd404 100644 --- a/public/app/features/canvas/elements/server/server.tsx +++ b/public/app/features/canvas/elements/server/server.tsx @@ -4,8 +4,9 @@ import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { t } from '@grafana/i18n'; import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema'; import config from 'app/core/config'; -import { DimensionContext } from 'app/features/dimensions'; -import { ColorDimensionEditor, ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { DimensionContext } from 'app/features/dimensions/context'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps } from '../../element'; diff --git a/public/app/features/canvas/elements/triangle.tsx b/public/app/features/canvas/elements/triangle.tsx index 13cba93f980..35b00d05761 100644 --- a/public/app/features/canvas/elements/triangle.tsx +++ b/public/app/features/canvas/elements/triangle.tsx @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from 'app/core/config'; -import { DimensionContext } from 'app/features/dimensions'; +import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; diff --git a/public/app/features/canvas/elements/windTurbine.tsx b/public/app/features/canvas/elements/windTurbine.tsx index 8070ab41973..0da6fea9455 100644 --- a/public/app/features/canvas/elements/windTurbine.tsx +++ b/public/app/features/canvas/elements/windTurbine.tsx @@ -4,8 +4,8 @@ import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { t } from '@grafana/i18n'; import { ScalarDimensionConfig } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; -import { DimensionContext } from 'app/features/dimensions'; -import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { DimensionContext } from 'app/features/dimensions/context'; +import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor'; import { CanvasElementItem, CanvasElementOptions, CanvasElementProps, defaultBgColor } from '../element'; diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index df072b83c58..047d42a5e45 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -15,7 +15,7 @@ import { t } from '@grafana/i18n'; import { ConfirmModal } from '@grafana/ui'; import { LayerElement } from 'app/core/components/Layers/types'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; -import { DimensionContext } from 'app/features/dimensions'; +import { DimensionContext } from 'app/features/dimensions/context'; import { BackgroundImageSize, Constraint, diff --git a/public/app/features/canvas/runtime/frame.tsx b/public/app/features/canvas/runtime/frame.tsx index d58accc0ade..7b9765ddecc 100644 --- a/public/app/features/canvas/runtime/frame.tsx +++ b/public/app/features/canvas/runtime/frame.tsx @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; -import { DimensionContext } from 'app/features/dimensions'; +import { DimensionContext } from 'app/features/dimensions/context'; import { HorizontalConstraint, Placement, VerticalConstraint } from 'app/plugins/panel/canvas/panelcfg.gen'; import { LayerActionID } from 'app/plugins/panel/canvas/types'; diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 5be6001272d..5e54afd9be8 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -16,7 +16,7 @@ import { } from '@grafana/schema'; import { Portal } from '@grafana/ui'; import { config } from 'app/core/config'; -import { DimensionContext } from 'app/features/dimensions'; +import { DimensionContext } from 'app/features/dimensions/context'; import { getColorDimensionFromData, getResourceDimensionFromData, diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index d3304f0db78..e7e8c8d6734 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -6,8 +6,8 @@ import { enrichHelpItem } from 'app/core/components/AppChrome/MegaMenu/utils'; import { performInviteUserClick, shouldRenderInviteUserButton } from 'app/core/components/InviteUserButton/utils'; import { changeTheme } from 'app/core/services/theme'; import { currentMockApiState, toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils'; +import { useSelector } from 'app/types/store'; -import { useSelector } from '../../../types'; import { CommandPaletteAction } from '../types'; import { ACTIONS_PRIORITY, DEFAULT_PRIORITY, PREFERENCES_PRIORITY } from '../values'; diff --git a/public/app/features/commandPalette/actions/useExtensionActions.ts b/public/app/features/commandPalette/actions/useExtensionActions.ts index 14b0c19b8cf..1050fb0deb9 100644 --- a/public/app/features/commandPalette/actions/useExtensionActions.ts +++ b/public/app/features/commandPalette/actions/useExtensionActions.ts @@ -13,7 +13,7 @@ export default function useExtensionActions(): CommandPaletteAction[] { const { links } = usePluginLinks({ extensionPointId: PluginExtensionPoints.CommandPalette, context, - limitPerPlugin: 3, + limitPerPlugin: 40, }); return useMemo(() => { diff --git a/public/app/features/connections/Connections.tsx b/public/app/features/connections/Connections.tsx index 05330a180b5..e4119a6bf3f 100644 --- a/public/app/features/connections/Connections.tsx +++ b/public/app/features/connections/Connections.tsx @@ -1,16 +1,14 @@ import { Navigate, Routes, Route, useLocation } from 'react-router-dom-v5-compat'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from './constants'; -import { - AddNewConnectionPage, - DataSourceDashboardsPage, - DataSourceDetailsPage, - DataSourcesListPage, - EditDataSourcePage, - NewDataSourcePage, -} from './pages'; +import { AddNewConnectionPage } from './pages/AddNewConnectionPage'; +import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage'; +import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage'; +import { DataSourcesListPage } from './pages/DataSourcesListPage'; +import { EditDataSourcePage } from './pages/EditDataSourcePage'; +import { NewDataSourcePage } from './pages/NewDataSourcePage'; function RedirectToAddNewConnection() { const { search } = useLocation(); diff --git a/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx b/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx index f9bb04c72de..438868f7751 100644 --- a/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx +++ b/public/app/features/connections/components/ConnectionsRedirectNotice/ConnectionsRedirectNotice.tsx @@ -4,9 +4,9 @@ import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; +import { AccessControlAction } from 'app/types/accessControl'; import { contextSrv } from '../../../../core/core'; -import { AccessControlAction } from '../../../../types'; import { ROUTES } from '../../constants'; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/connections/hooks/useDataSourceSettingsNav.ts b/public/app/features/connections/hooks/useDataSourceSettingsNav.ts index d5898d370d2..72c4a09059a 100644 --- a/public/app/features/connections/hooks/useDataSourceSettingsNav.ts +++ b/public/app/features/connections/hooks/useDataSourceSettingsNav.ts @@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks'; import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from 'app/features/datasources/state/navModel'; import { useGetSingle } from 'app/features/plugins/admin/state/hooks'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; export function useDataSourceSettingsNav(pageIdParam?: string) { const { uid = '' } = useParams<{ uid: string }>(); diff --git a/public/app/features/connections/pages/DataSourceDetailsPage.tsx b/public/app/features/connections/pages/DataSourceDetailsPage.tsx index be345ee77ed..f7223abc418 100644 --- a/public/app/features/connections/pages/DataSourceDetailsPage.tsx +++ b/public/app/features/connections/pages/DataSourceDetailsPage.tsx @@ -3,7 +3,8 @@ import { useParams } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; import { Alert, Badge, TextLink } from '@grafana/ui'; import { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage'; -import { StoreState, useSelector, AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from '../constants'; diff --git a/public/app/features/connections/pages/DataSourcesListPage.tsx b/public/app/features/connections/pages/DataSourcesListPage.tsx index 7a794ae3b52..7ec9062b1d7 100644 --- a/public/app/features/connections/pages/DataSourcesListPage.tsx +++ b/public/app/features/connections/pages/DataSourcesListPage.tsx @@ -2,8 +2,8 @@ import { Page } from 'app/core/components/Page/Page'; import { AdvisorRedirectNotice } from 'app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice'; import { DataSourceAddButton } from 'app/features/datasources/components/DataSourceAddButton'; import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList'; -import { getDataSourcesCount } from 'app/features/datasources/state'; -import { StoreState, useSelector } from 'app/types'; +import { getDataSourcesCount } from 'app/features/datasources/state/selectors'; +import { StoreState, useSelector } from 'app/types/store'; export function DataSourcesListPage() { const dataSourcesCount = useSelector(({ dataSources }: StoreState) => getDataSourcesCount(dataSources)); diff --git a/public/app/features/connections/pages/index.tsx b/public/app/features/connections/pages/index.tsx deleted file mode 100644 index 64d2ec896ec..00000000000 --- a/public/app/features/connections/pages/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export { AddNewConnectionPage } from './AddNewConnectionPage'; -export { DataSourceDetailsPage } from './DataSourceDetailsPage'; -export { DataSourcesListPage } from './DataSourcesListPage'; -export { DataSourceDashboardsPage } from './DataSourceDashboardsPage'; -export { EditDataSourcePage } from './EditDataSourcePage'; -export { NewDataSourcePage } from './NewDataSourcePage'; diff --git a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx index fe2006270ae..80b08bb663e 100644 --- a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx +++ b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx @@ -5,12 +5,10 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { featureEnabled } from '@grafana/runtime'; import { Card, Grid, useStyles2, Stack, Badge } from '@grafana/ui'; -import { - PluginDeprecatedBadge, - PluginDisabledBadge, - PluginInstalledBadge, - PluginUpdateAvailableBadge, -} from 'app/features/plugins/admin/components/Badges'; +import { PluginDeprecatedBadge } from 'app/features/plugins/admin/components/Badges/PluginDeprecatedBadge'; +import { PluginDisabledBadge } from 'app/features/plugins/admin/components/Badges/PluginDisabledBadge'; +import { PluginInstalledBadge } from 'app/features/plugins/admin/components/Badges/PluginInstallBadge'; +import { PluginUpdateAvailableBadge } from 'app/features/plugins/admin/components/Badges/PluginUpdateAvailableBadge'; import { getBadgeColor } from 'app/features/plugins/admin/components/Badges/sharedStyles'; import { isPluginUpdatable } from 'app/features/plugins/admin/helpers'; import { CatalogPlugin } from 'app/features/plugins/admin/types'; diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx index 0215b463ca9..a4dabd2bbf9 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.test.tsx @@ -7,7 +7,7 @@ import { locationService } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getCatalogPluginMock, getPluginsStateMock } from 'app/features/plugins/admin/mocks/mockHelpers'; import { CatalogPlugin } from 'app/features/plugins/admin/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AddNewConnection } from './ConnectData'; diff --git a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx index 30f0464b7ce..3668047af9f 100644 --- a/public/app/features/connections/tabs/ConnectData/ConnectData.tsx +++ b/public/app/features/connections/tabs/ConnectData/ConnectData.tsx @@ -14,7 +14,7 @@ import { SearchField } from 'app/features/plugins/admin/components/SearchField'; import { Sorters } from 'app/features/plugins/admin/helpers'; import { useHistory } from 'app/features/plugins/admin/hooks/useHistory'; import { useGetAll, useIsRemotePluginsAvailable } from 'app/features/plugins/admin/state/hooks'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ROUTES } from '../../constants'; diff --git a/public/app/features/correlations/CorrelationsPage.tsx b/public/app/features/correlations/CorrelationsPage.tsx index bab13964ae6..60ce5a5b166 100644 --- a/public/app/features/correlations/CorrelationsPage.tsx +++ b/public/app/features/correlations/CorrelationsPage.tsx @@ -22,7 +22,7 @@ import { import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AddCorrelationForm } from './Forms/AddCorrelationForm'; import { EditCorrelationForm } from './Forms/EditCorrelationForm'; diff --git a/public/app/features/correlations/Forms/AddCorrelationForm.tsx b/public/app/features/correlations/Forms/AddCorrelationForm.tsx index bda19c40de6..cba2a2bf76f 100644 --- a/public/app/features/correlations/Forms/AddCorrelationForm.tsx +++ b/public/app/features/correlations/Forms/AddCorrelationForm.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { PanelContainer, useStyles2 } from '@grafana/ui'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; -import { Wizard } from '../components/Wizard'; +import { Wizard } from '../components/Wizard/Wizard'; import { useCorrelations } from '../useCorrelations'; import { ConfigureCorrelationBasicInfoForm } from './ConfigureCorrelationBasicInfoForm'; diff --git a/public/app/features/correlations/Forms/EditCorrelationForm.tsx b/public/app/features/correlations/Forms/EditCorrelationForm.tsx index 55417f4fad0..11d4f6ad441 100644 --- a/public/app/features/correlations/Forms/EditCorrelationForm.tsx +++ b/public/app/features/correlations/Forms/EditCorrelationForm.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import { Wizard } from '../components/Wizard'; +import { Wizard } from '../components/Wizard/Wizard'; import { Correlation } from '../types'; import { useCorrelations } from '../useCorrelations'; diff --git a/public/app/features/correlations/components/Wizard/index.ts b/public/app/features/correlations/components/Wizard/index.ts deleted file mode 100644 index 7ccbf5e9774..00000000000 --- a/public/app/features/correlations/components/Wizard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Wizard'; -export * from './types'; diff --git a/public/app/features/correlations/utils.ts b/public/app/features/correlations/utils.ts index 1a6f97157da..37c81fc6728 100644 --- a/public/app/features/correlations/utils.ts +++ b/public/app/features/correlations/utils.ts @@ -9,7 +9,7 @@ import { getBackendSrv, getDataSourceSrv, } from '@grafana/runtime'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; import { formatValueName } from '../explore/PrometheusListView/ItemLabels'; import { parseLogsFrame } from '../logs/logsFrame'; diff --git a/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx b/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx index d437e1308ce..17ecf87eb8f 100644 --- a/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx +++ b/public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx @@ -9,7 +9,7 @@ import { Panel } from '@grafana/schema'; import { Alert, Button, Field, Modal, RadioButtonGroup } from '@grafana/ui'; import { DashboardPicker } from 'app/core/components/Select/DashboardPicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { addToDashboard, SubmissionError } from './addToDashboard'; diff --git a/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts b/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts index 7cad2fc7c3d..90afde16cd4 100644 --- a/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts +++ b/public/app/features/dashboard-scene/addToDashboard/addPanelsOnLoadBehavior.ts @@ -1,7 +1,7 @@ import { SceneTimeRange } from '@grafana/scenes'; import store from 'app/core/store'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts index e0f929c6f2e..e22feb9a502 100644 --- a/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts +++ b/public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts @@ -4,7 +4,7 @@ import { config, locationService } from '@grafana/runtime'; import { Panel } from '@grafana/schema'; import store from 'app/core/store'; import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; -import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardDTO } from 'app/types/dashboard'; export enum GenericError { UNKNOWN = 'unknown-error', diff --git a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx index 90b1524c1d2..ae55db117d0 100644 --- a/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx +++ b/public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx @@ -7,7 +7,7 @@ import { EmbeddedDashboardProps } from '@grafana/runtime'; import { SceneObjectStateChangedEvent, sceneUtils } from '@grafana/scenes'; import { Spinner, Alert, useStyles2 } from '@grafana/ui'; import { getMessageFromError } from 'app/core/utils/errors'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx index e6a1c1ffbf4..cab5eaf608c 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx @@ -24,7 +24,7 @@ import { TextLink, } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index b2776dc9414..016e72010bf 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -15,7 +15,7 @@ import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import store from 'app/core/store'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; -import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types/dashboard'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { setupLoadDashboardMockReject, setupLoadDashboardRuntimeErrorMock } from '../utils/test-utils'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index bcdcff100ec..ef1ec38b653 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -11,7 +11,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardPageError } from 'app/features/dashboard/containers/DashboardPageError'; import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { DashboardPrompt } from '../saving/DashboardPrompt'; import { DashboardPreviewBanner } from '../saving/provisioned/DashboardPreviewBanner'; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 5e8b9cd57ca..c6a484837c9 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -14,7 +14,7 @@ import { setDashboardLoaderSrv, } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSnapshotSrv } from 'app/features/dashboard/services/SnapshotSrv'; -import { DASHBOARD_FROM_LS_KEY, DashboardDataDTO, DashboardDTO, DashboardRoutes } from 'app/types'; +import { DASHBOARD_FROM_LS_KEY, DashboardDataDTO, DashboardDTO, DashboardRoutes } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; import { setupLoadDashboardMock, setupLoadDashboardMockReject } from '../utils/test-utils'; @@ -218,6 +218,39 @@ describe('DashboardScenePageStateManager v1', () => { }); describe('caching', () => { + it('should return cached scene if updated_at matches', async () => { + const loader = new DashboardScenePageStateManager({}); + + // set cache + loader.setSceneCache( + 'fake-dash', + new DashboardScene({ title: 'Dashboard 1', uid: 'fake-dash', meta: { created: '1' }, version: 0 }, 'v1') + ); + + // should return cached scene + expect( + loader.transformResponseToScene( + { + meta: { created: '1' }, + dashboard: { title: 'Dashboard 1', uid: 'fake-dash', schemaVersion: 1, version: 0 }, + }, + { uid: 'fake-dash', route: DashboardRoutes.Normal } + )?.state.title + ).toBe('Dashboard 1'); + + // try loading new scene + loader.transformResponseToScene( + { + meta: { created: '2' }, + dashboard: { title: 'Dashboard 2', uid: 'fake-dash', schemaVersion: 1, version: 0 }, + }, + { uid: 'fake-dash', route: DashboardRoutes.Normal } + ); + + // should update cache with new scene + expect(loader.getSceneFromCache('fake-dash').state.title).toBe('Dashboard 2'); + }); + it('should take scene from cache if it exists', async () => { setupLoadDashboardMock({ dashboard: { uid: 'fake-dash', version: 10 }, meta: {} }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index e143df2b776..1b2d7f80c2a 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -27,7 +27,7 @@ import { DashboardRoutes, HomeDashboardRedirectDTO, isRedirectResponse, -} from 'app/types'; +} from 'app/types/dashboard'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; @@ -362,6 +362,10 @@ abstract class DashboardScenePageStateManagerBase<T> this.cache[cacheKey] = scene; } + public removeSceneCache(cacheKey: string) { + delete this.cache[cacheKey]; + } + public clearSceneCache() { this.cache = {}; } @@ -371,7 +375,11 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag transformResponseToScene(rsp: DashboardDTO | null, options: LoadDashboardOptions): DashboardScene | null { const fromCache = this.getSceneFromCache(options.uid); - if (fromCache && fromCache.state.version === rsp?.dashboard.version) { + if ( + fromCache && + fromCache.state.version === rsp?.dashboard.version && + fromCache.state.meta.created === rsp?.meta.created + ) { return fromCache; } @@ -819,6 +827,11 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta this.activeManager.setSceneCache(cacheKey, scene); } + public removeSceneCache(cacheKey: string): void { + this.v1Manager.removeSceneCache(cacheKey); + this.v2Manager.removeSceneCache(cacheKey); + } + public getCache() { return this.activeManager.getCache(); } diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx index f7c88b2815a..07bcf478336 100644 --- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx @@ -16,7 +16,7 @@ import { PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams, } from 'app/features/dashboard/containers/types'; -import { AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; import { DashboardRoutes } from 'app/types/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 7f2b930b208..06573cc06ef 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -25,7 +25,8 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { configureStore } from 'app/store/configureStore'; -import { AccessControlAction, DashboardDataDTO } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto'; import { createDashboardSceneFromDashboardModel } from '../../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx index c76896c4afd..241b37165b5 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx @@ -22,7 +22,7 @@ import { config, locationService } from '@grafana/runtime'; import { PANEL_EDIT_LAST_USED_DATASOURCE } from 'app/features/dashboard/utils/dashboard'; import { InspectTab } from 'app/features/inspector/types'; import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { PanelTimeRange, PanelTimeRangeState } from '../../scene/PanelTimeRange'; import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index f1660b56772..ce2784758be 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -18,12 +18,15 @@ import { addQuery } from 'app/core/utils/query'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; +import { ExpressionTypeDropdown } from 'app/features/expressions/components/ExpressionTypeDropdown'; +import { ExpressionQueryType } from 'app/features/expressions/types'; +import { getDefaults } from 'app/features/expressions/utils/expressionTypes'; import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent'; import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows'; import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup'; import { updateQueries } from 'app/features/query/state/updateQueries'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions } from 'app/types/query'; import { MIXED_DATASOURCE_NAME } from '../../../../plugins/datasource/mixed/MixedDataSource'; import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext'; @@ -286,9 +289,15 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat return (dsSettings.meta.backend || dsSettings.meta.alerting || dsSettings.meta.mixed) === true; } - public onAddExpressionClick = () => { + public onAddExpressionOfType = (type: ExpressionQueryType) => { const queries = this.getQueries(); - this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery())); + // Create base expression query with the specified type + const baseQuery = expressionDatasource.newQuery(); + const queryWithType = { ...baseQuery, type }; + // Apply defaults specific to the expression type + const queryWithDefaults = getDefaults(queryWithType); + + this.onQueriesChange(addQuery(queries, queryWithDefaults)); }; public renderExtraActions() { @@ -316,6 +325,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel if (!datasource || !dsSettings || !data) { return null; } + const showAddButton = !isSharedDashboardQuery(dsSettings.name); const onSelectQueryFromLibrary = async (query: DataQuery) => { // ensure all queries explicitly define a datasource @@ -394,16 +404,11 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel </> )} {config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && ( - <Button - icon="plus" - onClick={model.onAddExpressionClick} - variant="secondary" - data-testid={selectors.components.QueryTab.addExpression} - > - <span> + <ExpressionTypeDropdown handleOnSelect={model.onAddExpressionOfType}> + <Button icon="plus" variant="secondary" data-testid={selectors.components.QueryTab.addExpression}> <Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.expression">Expression </Trans> - </span> - </Button> + </Button> + </ExpressionTypeDropdown> )} {model.renderExtraActions()} </Stack> diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx index 7ff6453297a..e06b9fc6da2 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx @@ -14,7 +14,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene'; import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index e186910de28..27f434da0a9 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -135,8 +135,11 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro 'dashboard-scene.panel-data-transformations-tab-rendered.title-delete-all-transformations', 'Delete all transformations?' )} - body="By deleting all transformations, you will go back to the main selection screen." - confirmText="Delete all" + body={t( + 'dashboard-scene.panel-data-transformations-tab-rendered.body-delete-all-transformations', + 'By deleting all transformations, you will go back to the main selection screen.' + )} + confirmText={t('dashboard-scene.panel-data-transformations-tab-rendered.confirmText-delete-all', 'Delete all')} onConfirm={() => { model.onChangeTransformations([]); setConfirmModalOpen(false); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx index fa88f3b5251..c00f4233fc1 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx @@ -52,10 +52,13 @@ export function TransformationsDrawer(props: TransformationsDrawerProps) { ) { return false; } - return ( - t.name.toLocaleLowerCase().includes(drawerState.search.toLocaleLowerCase()) || - t.description?.toLocaleLowerCase().includes(drawerState.search.toLocaleLowerCase()) - ); + const searchLower = drawerState.search.toLocaleLowerCase(); + const textMatch = + t.name.toLocaleLowerCase().includes(searchLower) || t.description?.toLocaleLowerCase().includes(searchLower); + const tagMatch = t.tags?.size + ? Array.from(t.tags).some((tag) => tag.toLocaleLowerCase().includes(searchLower)) + : false; + return textMatch || tagMatch; }); const searchBoxSuffix = ( diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 9c2f783c49e..00001fac1f8 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -118,7 +118,10 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { <form onSubmit={handleSubmit(() => onSave(false))}> <Field label={<TitleFieldLabel onChange={setValue} />} invalid={!!errors.title} error={errors.title?.message}> <Input - {...register('title', { required: 'Required', validate: validateDashboardName })} + {...register('title', { + required: t('dashboard-scene.save-dashboard-as-form.required', 'Required'), + validate: validateDashboardName, + })} aria-label={t( 'dashboard-scene.save-dashboard-as-form.aria-label-save-dashboard-title-field', 'Save dashboard title field' diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx index d78b4971bfe..dd4d6734919 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx @@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes'; import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { DashboardSceneState } from '../scene/DashboardScene'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts index b47c88473ef..17ef35baa60 100644 --- a/public/app/features/dashboard-scene/saving/getDashboardChanges.ts +++ b/public/app/features/dashboard-scene/saving/getDashboardChanges.ts @@ -10,7 +10,7 @@ import { } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { validateFiltersOrigin } from '../serialization/sceneVariablesSetToVariables'; import { jsonDiff } from '../settings/version-history/utils'; diff --git a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx index 1f7486a921f..f44552cbd69 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx @@ -1,13 +1,14 @@ -import { textUtil } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { Alert, Icon, Stack } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import { useGetRepositoryFilesWithPathQuery } from 'app/api/clients/provisioning/v0alpha1'; import { DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; -interface CommonBannerProps { +import { PreviewBannerViewPR } from './PreviewBannerViewPR'; + +export interface CommonBannerProps { queryParams: DashboardPageRouteSearchParams; path?: string; slug?: string; @@ -19,13 +20,13 @@ interface DashboardPreviewBannerProps extends CommonBannerProps { interface DashboardPreviewBannerContentProps extends Required<Omit<CommonBannerProps, 'route'>> {} -const commonAlertProps = { +export const commonAlertProps = { severity: 'info' as const, style: { flex: 0 } as const, }; function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPreviewBannerContentProps) { - const prParam = usePullRequestParam(); + const { prURL } = usePullRequestParam(); const file = useGetRepositoryFilesWithPathQuery({ name: slug, path, ref: queryParams.ref }); if (file.data?.errors) { @@ -43,56 +44,14 @@ function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPre } // This page was loaded with a `pull_request_url` in the URL - if (prParam?.length) { - return ( - <Alert - {...commonAlertProps} - title={t( - 'dashboard-scene.dashboard-preview-banner.title-dashboard-loaded-request-git-hub', - 'This dashboard is loaded from a pull request in GitHub.' - )} - buttonContent={ - <Stack alignItems="center"> - <Trans i18nKey="dashboard-scene.dashboard-preview-banner.view-pull-request-in-git-hub"> - View pull request in GitHub - </Trans> - <Icon name="external-link-alt" /> - </Stack> - } - onRemove={() => window.open(textUtil.sanitizeUrl(prParam), '_blank')} - > - <Trans i18nKey="dashboard-scene.dashboard-preview-banner.value-not-saved"> - The value is not yet saved in the Grafana database - </Trans> - </Alert> - ); + if (prURL?.length) { + return <PreviewBannerViewPR prParam={prURL} />; } // Check if this is a GitHub link const githubURL = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL; if (githubURL) { - return ( - <Alert - {...commonAlertProps} - title={t( - 'dashboard-scene.dashboard-preview-banner.title-dashboard-loaded-branch-git-hub', - 'This dashboard is loaded from a branch in GitHub.' - )} - buttonContent={ - <Stack alignItems="center"> - <Trans i18nKey="dashboard-scene.dashboard-preview-banner.open-pull-request-in-git-hub"> - Open pull request in GitHub - </Trans> - <Icon name="external-link-alt" /> - </Stack> - } - onRemove={() => window.open(textUtil.sanitizeUrl(githubURL), '_blank')} - > - <Trans i18nKey="dashboard-scene.dashboard-preview-banner.not-saved"> - The value is not yet saved in the Grafana database - </Trans> - </Alert> - ); + return <PreviewBannerViewPR prParam={githubURL} isNewPr />; } return ( diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx new file mode 100644 index 00000000000..94964f8004a --- /dev/null +++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { textUtil } from '@grafana/data'; + +import { PreviewBannerViewPR } from './PreviewBannerViewPR'; + +jest.mock('@grafana/data', () => ({ + ...jest.requireActual('@grafana/data'), + textUtil: { + sanitizeUrl: jest.fn(), + }, +})); + +jest.mock('@grafana/i18n', () => ({ + t: jest.fn((key: string, defaultValue: string) => defaultValue), + Trans: ({ children }: { children: React.ReactNode }) => children, +})); + +const mockTextUtil = jest.mocked(textUtil); + +function setup(props: { prParam: string; isFolder?: boolean; isNewPr?: boolean } = { prParam: 'test-url' }) { + const defaultProps = { + isFolder: false, + isNewPr: false, + ...props, + }; + + const renderResult = render(<PreviewBannerViewPR {...defaultProps} />); + + return { renderResult, props: defaultProps }; +} + +describe('PreviewBannerViewPR', () => { + let windowOpenSpy: jest.SpyInstance; + + beforeAll(() => { + Object.defineProperty(window, 'open', { + writable: true, + value: jest.fn(), + }); + windowOpenSpy = jest.spyOn(window, 'open'); + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockTextUtil.sanitizeUrl.mockImplementation((url) => url); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + afterAll(() => { + windowOpenSpy.mockRestore(); + }); + + describe('Dashboard scenarios', () => { + it('should render correct text for new PR dashboard', () => { + setup({ prParam: 'test-url', isFolder: false, isNewPr: true }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument(); + }); + + it('should render correct text for existing PR dashboard', () => { + setup({ prParam: 'test-url', isFolder: false, isNewPr: false }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument(); + }); + + it('should render correct button text for new PR dashboard', () => { + setup({ prParam: 'test-url', isFolder: false, isNewPr: true }); + + expect(screen.getByText('Open pull request in GitHub')).toBeInTheDocument(); + }); + + it('should render correct button text for existing PR dashboard', () => { + setup({ prParam: 'test-url', isFolder: false, isNewPr: false }); + + expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument(); + }); + }); + + describe('Folder scenarios', () => { + it('should render correct text for new PR folder', () => { + setup({ prParam: 'test-url', isFolder: true, isNewPr: true }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument(); + }); + + it('should render correct text for existing PR folder', () => { + setup({ prParam: 'test-url', isFolder: true, isNewPr: false }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument(); + }); + + it('should render correct button text for new PR folder', () => { + setup({ prParam: 'test-url', isFolder: true, isNewPr: true }); + + expect(screen.getByText('Open pull request in GitHub')).toBeInTheDocument(); + }); + + it('should render correct button text for existing PR folder', () => { + setup({ prParam: 'test-url', isFolder: true, isNewPr: false }); + + expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument(); + }); + }); + + describe('Button functionality', () => { + it('should open URL in new tab when button is clicked', async () => { + const testUrl = 'https://github.com/test/repo/pull/123'; + setup({ prParam: testUrl }); + + const button = screen.getByRole('button', { name: /close alert/i }); + await userEvent.click(button); + + expect(windowOpenSpy).toHaveBeenCalledWith(testUrl, '_blank'); + }); + }); +}); diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx new file mode 100644 index 00000000000..e1da054147f --- /dev/null +++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx @@ -0,0 +1,53 @@ +import { textUtil } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { Alert, Icon, Stack } from '@grafana/ui'; + +import { commonAlertProps } from './DashboardPreviewBanner'; + +// TODO: We have this https://github.com/grafana/git-ui-sync-project/issues/166 to add more details about the PR. + +interface Props { + prParam: string; + isNewPr?: boolean; +} + +/** + * @description This component is used to display a banner when a provisioned dashboard/folder is created or loaded from a new branch in Github. + */ +export function PreviewBannerViewPR({ prParam, isNewPr }: Props) { + const titleText = isNewPr + ? t( + 'provisioned-resource-preview-banner.title-created-branch-git-hub', + 'A new resource has been created in a branch in GitHub.' + ) + : t( + 'provisioned-resource-preview-banner.title-loaded-pull-request-git-hub', + 'This resource is loaded from a pull request in GitHub.' + ); + + return ( + <Alert + {...commonAlertProps} + title={titleText} + buttonContent={ + <Stack alignItems="center"> + {isNewPr + ? t( + 'provisioned-resource-preview-banner.preview-banner.open-pull-request-in-git-hub', + 'Open pull request in GitHub' + ) + : t( + 'provisioned-resource-preview-banner.preview-banner.view-pull-request-in-git-hub', + 'View pull request in GitHub' + )} + <Icon name="external-link-alt" /> + </Stack> + } + onRemove={() => window.open(textUtil.sanitizeUrl(prParam), '_blank')} + > + <Trans i18nKey="provisioned-resource-preview-banner.preview-banner.not-saved"> + The value is not yet saved in the Grafana database + </Trans> + </Alert> + ); +} diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx index fc27806d08f..7ffb27f7e9d 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx @@ -120,6 +120,8 @@ export function SaveProvisionedDashboardForm({ ref = loadedFromRef; } + const message = comment || `Save dashboard: ${dashboard.state.title}`; + const body = dashboard.getSaveResource({ isNew, title, @@ -130,7 +132,7 @@ export function SaveProvisionedDashboardForm({ ref, name: repo, path, - message: comment, + message, body, }); }; diff --git a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts index a274c9c7f88..e82ebcee548 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts +++ b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts @@ -4,7 +4,7 @@ import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; import { useUrlParams } from 'app/core/navigation/hooks'; import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath } from 'app/features/apiserver/types'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { DashboardScene } from '../../scene/DashboardScene'; import { ProvisionedDashboardFormData } from '../shared'; diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index 8977abc2365..8f75ecf0751 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -1,6 +1,7 @@ import { useAsyncFn } from 'react-use'; import { locationUtil } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; @@ -9,8 +10,8 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { SaveDashboardAsOptions, SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types'; -import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { updateDashboardUidLastUsedDatasource } from '../../dashboard/utils/dashboard'; import { DashboardScene } from '../scene/DashboardScene'; @@ -59,7 +60,7 @@ export function useSaveDashboard(isCopy = false) { // important that these happen before location redirect below appEvents.publish(new DashboardSavedEvent()); - notifyApp.success('Dashboard saved'); + notifyApp.success(t('dashboard-scene.use-save-dashboard.message-dashboard-saved', 'Dashboard saved')); //Update local storage dashboard to handle things like last used datasource updateDashboardUidLastUsedDatasource(resultData.uid); diff --git a/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts b/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts index 643ea0ac56f..f939f4a2a24 100644 --- a/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts +++ b/public/app/features/dashboard-scene/scene/AlertStatesDataLayer.ts @@ -19,7 +19,7 @@ import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { prometheusRuleType } from 'app/features/alerting/unified/utils/rules'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleNamespace } from 'app/types/unified-alerting'; import { PromAlertingRuleState, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index cd54c64a63f..46e67142648 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -31,7 +31,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DashboardJson } from 'app/features/manage-dashboards/types'; import { VariablesChanged } from 'app/features/variables/types'; -import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardMeta, KioskMode, SaveDashboardResponseDTO, DashboardDTO } from 'app/types/dashboard'; import { ShowConfirmModalEvent } from 'app/types/events'; import { @@ -59,7 +59,7 @@ import { buildGridItemForPanel, transformSaveModelToScene } from '../serializati import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; -import { historySrv } from '../settings/version-history'; +import { historySrv } from '../settings/version-history/HistorySrv'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { isInCloneChain } from '../utils/clone'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index bd300a5bbd3..19a6916552c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -6,7 +6,7 @@ import { ScopesContext } from '@grafana/runtime'; import { SceneComponentProps } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { DashboardEditPaneSplitter } from '../edit-pane/DashboardEditPaneSplitter'; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts index 4a7e9fca190..9dbd47cf919 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts @@ -1,7 +1,7 @@ import { AppEvents } from '@grafana/data'; import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; -import { KioskMode } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { getCloneKey } from '../utils/clone'; diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 1740fe87c8a..3d9acceed0b 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -5,7 +5,7 @@ import { config, locationService } from '@grafana/runtime'; import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; -import { KioskMode } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx index 104cccc9d86..846e6090fad 100644 --- a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.test.tsx @@ -11,10 +11,11 @@ import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; describe('GoToSnapshotOriginButton component', () => { beforeEach(async () => { locationService.push('/'); - const location = window.location; + const win: typeof globalThis = window; + const location = win.location; //@ts-ignore - delete window.location; - window.location = { + delete win.location; + win.location = { ...location, href: 'http://snapshots.grafana.com/snapshots/dashboard/abcdefghi/my-dash', }; diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx index 735c81c8401..b384ecacbbf 100644 --- a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx @@ -48,7 +48,7 @@ const onOpenSnapshotOriginalDashboard = (originalUrl: string) => { </> ), confirmVariant: 'primary', - confirmText: 'Proceed', + confirmText: t('dashboard-scene.on-open-snapshot-original-dashboard.confirmText.proceed', 'Proceed'), onConfirm: () => { window.location.href = sanitizedAppUrl.href; }, diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index 62a4cc2d77f..e8a3c3d4d8b 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -3,13 +3,21 @@ import { of } from 'rxjs'; import { FieldType, LoadingState, PanelData, getDefaultTimeRange, toDataFrame } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test'; import { config, setPluginImportUtils, setRunRequest } from '@grafana/runtime'; -import { SceneCanvasText, sceneGraph, SceneGridLayout, VizPanel } from '@grafana/scenes'; +import { + SceneCanvasText, + SceneDataTransformer, + sceneGraph, + SceneGridLayout, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; import { LibraryPanel } from '@grafana/schema'; import * as libpanels from 'app/features/library-panels/state/api'; import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; import { NEW_LINK } from '../settings/links/utils'; import { activateFullSceneTree } from '../utils/test-utils'; +import { getPanelIdForVizPanel } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { LibraryPanelBehavior } from './LibraryPanelBehavior'; @@ -179,6 +187,24 @@ describe('LibraryPanelBehavior', () => { expect(behaviorClone.state._loadedPanel?.name).toBe('LibraryPanel A'); expect(behaviorClone.state._loadedPanel?.uid).toBe('111'); }); + + it('should use dashboard panel ID for data provider filtering', async () => { + const { gridItem } = await buildTestSceneWithLibraryPanel(); + + const vizPanel = gridItem.state.body; + + // Get the dashboard panel ID from the VizPanel key + const dashboardPanelId = getPanelIdForVizPanel(vizPanel); + expect(dashboardPanelId).toBe(1); // Based on key 'panel-1' + + // Verify the data provider uses the dashboard panel ID for filtering + const dataProvider = vizPanel.state.$data as SceneDataTransformer; + expect(dataProvider).toBeDefined(); + + // Access the SceneQueryRunner through the SceneDataTransformer + const queryRunner = dataProvider.state?.$data as SceneQueryRunner; + expect(queryRunner?.state?.dataLayerFilter?.panelId).toBe(dashboardPanelId); + }); }); async function buildTestSceneWithLibraryPanel() { diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index c42d1f1b423..ecccc4b21d1 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -8,6 +8,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { createPanelDataProvider } from '../utils/createPanelDataProvider'; +import { getPanelIdForVizPanel } from '../utils/utils'; import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks'; import { panelLinksBehavior } from './PanelMenuBehavior'; @@ -50,6 +51,10 @@ export class LibraryPanelBehavior extends SceneObjectBase<LibraryPanelBehaviorSt const libPanelModel = new PanelModel(libPanel.model); + // Use dashboard panel ID for data layer filtering + const dashboardPanelId = getPanelIdForVizPanel(vizPanel); + libPanelModel.id = dashboardPanelId; + const titleItems: SceneObject[] = []; titleItems.push( diff --git a/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx index 3e6761dfed0..fe025098988 100644 --- a/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx +++ b/public/app/features/dashboard-scene/scene/ManagedDashboardNavBarBadge.tsx @@ -1,6 +1,6 @@ import { Badge } from '@grafana/ui'; import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; export default function ManagedDashboardNavBarBadge({ meta }: { meta: DashboardMeta }) { const obj = meta.k8s; diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index abca5175425..384a2e842e6 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { LocationServiceProvider, config, locationService } from '@grafana/runtime'; import { SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -151,7 +151,7 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Share')).toBeInTheDocument(); const newShareButton = screen.queryByTestId(selectors.pages.Dashboard.DashNav.newShareButton.container); expect(newShareButton).not.toBeInTheDocument(); - const newExportButton = screen.queryByTestId(selectors.pages.Dashboard.DashNav.NewExportButton.container); + const newExportButton = screen.queryByRole('button', { name: /export dashboard/i }); expect(newExportButton).not.toBeInTheDocument(); }); it('Should show new share button when newDashboardSharingComponent FF is enabled', async () => { @@ -165,7 +165,7 @@ describe('NavToolbarActions', () => { it('Should show new export button when newDashboardSharingComponent FF is enabled', async () => { config.featureToggles.newDashboardSharingComponent = true; setup(); - const newExportButton = screen.getByTestId(selectors.pages.Dashboard.DashNav.NewExportButton.container); + const newExportButton = screen.getByRole('button', { name: /export dashboard/i }); expect(newExportButton).toBeInTheDocument(); }); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 77113ff1b2b..49b4a3c9f53 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -5,17 +5,7 @@ import { GrafanaTheme2, store } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { config, locationService } from '@grafana/runtime'; -import { - Badge, - Button, - ButtonGroup, - Dropdown, - Icon, - Menu, - ToolbarButton, - ToolbarButtonRow, - useStyles2, -} from '@grafana/ui'; +import { Button, ButtonGroup, Dropdown, Icon, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import grafanaConfig from 'app/core/config'; @@ -23,7 +13,7 @@ import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { contextSrv } from 'app/core/core'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { shareDashboardType } from '../../dashboard/components/ShareModal/utils'; import { selectFolderRepository } from '../../provisioning/utils/selectors'; @@ -39,6 +29,7 @@ import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton'; import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge'; import { LeftActions } from './new-toolbar/LeftActions'; import { RightActions } from './new-toolbar/RightActions'; +import { PublicDashboardBadge } from './new-toolbar/actions/PublicDashboardBadge'; interface Props { dashboard: DashboardScene; @@ -119,23 +110,13 @@ export function ToolbarActions({ dashboard }: Props) { }, }); - if (meta.publicDashboardEnabled) { - toolbarActions.push({ - group: 'icon-actions', - condition: uid && Boolean(meta.canStar) && isShowingDashboard && !isEditing, - render: () => { - return ( - <Badge - color="blue" - text={t('dashboard.toolbar.public-dashboard', 'Public')} - key="public-dashboard-button-badge" - className={styles.publicBadge} - data-testid={selectors.pages.Dashboard.DashNav.publicDashboardTag} - /> - ); - }, - }); - } + toolbarActions.push({ + group: 'icon-actions', + condition: uid && Boolean(meta.canStar) && isShowingDashboard && !isEditing, + render: () => { + return <PublicDashboardBadge key="public-dashboard-badge" dashboard={dashboard} />; + }, + }); if (dashboard.isManaged() && meta.canEdit) { toolbarActions.push({ diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 998541edf80..10191b32a58 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -23,7 +23,7 @@ import { GetExploreUrlArguments } from 'app/core/utils/explore'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form'; import * as storeModule from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 8b07ede1cb8..d10b2ab5b65 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -29,7 +29,7 @@ import { pluginExtensionRegistries } from 'app/features/plugins/extensions/regis import { GetPluginExtensions } from 'app/features/plugins/extensions/types'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowConfirmModalEvent } from 'app/types/events'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; diff --git a/public/app/features/dashboard-scene/scene/UnlinkModal.tsx b/public/app/features/dashboard-scene/scene/UnlinkModal.tsx index d092436c6d5..51dbd4b045d 100644 --- a/public/app/features/dashboard-scene/scene/UnlinkModal.tsx +++ b/public/app/features/dashboard-scene/scene/UnlinkModal.tsx @@ -12,9 +12,11 @@ export const UnlinkModal = ({ isOpen, onConfirm, onDismiss }: Props) => { <ConfirmModal title={t('dashboard-scene.unlink-modal.title-really-unlink-panel', 'Do you really want to unlink this panel?')} icon="question-circle" - body="If you unlink this panel, you will be able to edit it without affecting any other dashboards. - However, once you make a change you will not be able to revert to its original reusable panel." - confirmText="Yes, unlink" + body={t( + 'dashboard-scene.unlink-modal.body-unlink-panel', + 'If you unlink this panel, you will be able to edit it without affecting any other dashboards. However, once you make a change you will not be able to revert to its original reusable panel.' + )} + confirmText={t('dashboard-scene.unlink-modal.confirmText-yes-unlink', 'Yes, unlink')} onConfirm={() => { onConfirm(); onDismiss(); diff --git a/public/app/features/dashboard-scene/scene/VariableControls.tsx b/public/app/features/dashboard-scene/scene/VariableControls.tsx index 8ebde16f6f1..0c99500c2cd 100644 --- a/public/app/features/dashboard-scene/scene/VariableControls.tsx +++ b/public/app/features/dashboard-scene/scene/VariableControls.tsx @@ -99,8 +99,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', // No border for second element (inputs) as label and input border is shared '> :nth-child(2)': css({ - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, + borderTopLeftRadius: 'unset', + borderBottomLeftRadius: 'unset', }), }), labelWrapper: css({ diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index cde032a413e..0d7ad8f85a8 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -4,7 +4,7 @@ import { sceneGraph, VizPanel } from '@grafana/scenes'; import appEvents from 'app/core/app_events'; import { KeybindingSet } from 'app/core/services/KeybindingSet'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { shareDashboardType } from '../../dashboard/components/ShareModal/utils'; import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer'; diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index 05bd804b77c..e19803ed5f9 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -57,17 +57,38 @@ export class DashboardGridItem public constructor(state: DashboardGridItemState) { super(state); - this.addActivationHandler(() => this.handleVariableName()); + this.addActivationHandler(() => this._activationHandler()); + } + + private _activationHandler() { + this.handleVariableName(); + + return () => { + this._handleGridSizeUnsubscribe(); + }; + } + + private _handleGridSizeSubscribe() { + if (!this._gridSizeSub) { + this._gridSizeSub = this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)); + } + } + + private _handleGridSizeUnsubscribe() { + if (this._gridSizeSub) { + this._gridSizeSub.unsubscribe(); + this._gridSizeSub = undefined; + } } private _handleGridResize(newState: DashboardGridItemState, prevState: DashboardGridItemState) { - const itemCount = this.state.repeatedPanels?.length ?? 1; - const stateChange: Partial<DashboardGridItemState> = {}; - if (newState.height === prevState.height) { return; } + const itemCount = this.state.repeatedPanels?.length ?? 1; + const stateChange: Partial<DashboardGridItemState> = {}; + if (this.getRepeatDirection() === 'v') { stateChange.itemHeight = Math.ceil(newState.height! / itemCount); } else { @@ -203,16 +224,9 @@ export class DashboardGridItem public handleVariableName() { if (this.state.variableName) { - if (!this._gridSizeSub) { - this._gridSizeSub = this.subscribeToState((newState, prevState) => this._handleGridResize(newState, prevState)); - this._subs.add(this._gridSizeSub); - } + this._handleGridSizeSubscribe(); } else { - if (this._gridSizeSub) { - this._gridSizeSub.unsubscribe(); - this._subs.remove(this._gridSizeSub); - this._gridSizeSub = undefined; - } + this._handleGridSizeUnsubscribe(); } this.performRepeat(); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx index ec37d446145..6f7f361caf6 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx @@ -16,7 +16,6 @@ export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => { const isViewingPanel = Boolean(viewPanelScene); const isEditingDashboard = Boolean(isEditing); const isEditingPanel = Boolean(editPanel); - const isPublicDashboard = Boolean(meta.publicDashboardEnabled); const hasUid = Boolean(uid); const canEdit = Boolean(meta.canEdit); const canStar = Boolean(meta.canStar); @@ -37,7 +36,7 @@ export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => { key: 'public-dashboard-badge', component: PublicDashboardBadge, group: 'actions', - condition: isPublicDashboard && hasUid && canStar && isShowingDashboard && !isEditingDashboard, + condition: hasUid && canStar && isShowingDashboard && !isEditingDashboard, }, { key: 'managed-dashboard-badge', diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx index 46cbebda4fd..36ee44d14de 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge.tsx @@ -3,12 +3,29 @@ import { css } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { Badge, useStyles2 } from '@grafana/ui'; +import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi'; import { ToolbarActionProps } from '../types'; -export const PublicDashboardBadge = ({}: ToolbarActionProps) => { +export const PublicDashboardBadge = ({ dashboard }: ToolbarActionProps) => { + if (!dashboard.state.uid) { + return null; + } + + return <PublicDashboardBadgeInternal uid={dashboard.state.uid} />; +}; + +// Used in old architecture +export const PublicDashboardBadgeLegacy = PublicDashboardBadgeInternal; + +function PublicDashboardBadgeInternal({ uid }: { uid: string }) { + const { data: publicDashboard } = useGetPublicDashboardQuery(uid); const styles = useStyles2(getStyles); + if (!publicDashboard) { + return null; + } + return ( <Badge color="blue" @@ -17,7 +34,7 @@ export const PublicDashboardBadge = ({}: ToolbarActionProps) => { data-testid={selectors.pages.Dashboard.DashNav.publicDashboardTag} /> ); -}; +} const getStyles = () => ({ badge: css({ diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx index 00c09366d37..66eec343ea3 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ShareDashboardButton.tsx @@ -15,7 +15,7 @@ const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButt export const ShareDashboardButton = ({ dashboard }: ToolbarActionProps) => { const [_, buildUrl] = useAsyncFn(async () => { DashboardInteractions.toolbarShareClick(); - return await buildShareUrl(dashboard); + await buildShareUrl(dashboard); }, [dashboard]); return ( diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index 121a3a4cca2..66d73dd2ff9 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -12,7 +12,7 @@ import { getV2SchemaVariables, } from 'app/features/dashboard/utils/tracking'; import { DashboardJson } from 'app/features/manage-dashboards/types'; -import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { getRawDashboardChanges, getRawDashboardV2Changes } from '../saving/getDashboardChanges'; import { DashboardChangeInfo } from '../saving/shared'; diff --git a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts index 705ab15ddee..436563c8379 100644 --- a/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts @@ -13,7 +13,7 @@ import { import { AnnoKeyFolder } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; export async function buildNewDashboardSaveModel(urlFolderUid?: string): Promise<DashboardDTO> { let variablesList = defaultDashboard.templating?.list; diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index 72cc20f2943..e0a3cfe305d 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -101,6 +101,8 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, allValue: 'test-all', isMulti: true, + staticOptions: [{ label: 'test', value: 'test' }], + staticOptionsOrder: 'after', }); const set = new SceneVariableSet({ @@ -136,6 +138,13 @@ describe('sceneVariablesSetToVariables', () => { "query": "query", "refresh": 1, "regex": "", + "staticOptions": [ + { + "text": "test", + "value": "test", + }, + ], + "staticOptionsOrder": "after", "type": "query", } `); @@ -155,6 +164,8 @@ describe('sceneVariablesSetToVariables', () => { allValue: 'test-all', allowCustomValue: false, isMulti: true, + staticOptions: [{ label: 'test', value: 'test' }], + staticOptionsOrder: 'after', }); const set = new SceneVariableSet({ variables: [variable], @@ -189,6 +200,13 @@ describe('sceneVariablesSetToVariables', () => { "query": "query", "refresh": 1, "regex": "", + "staticOptions": [ + { + "text": "test", + "value": "test", + }, + ], + "staticOptionsOrder": "after", "type": "query", } `); @@ -221,7 +239,7 @@ describe('sceneVariablesSetToVariables', () => { expect(result[0].options).toEqual([]); }); - it('should handle Query variable when sceneVariablesSetToVariables shoudl keep options', () => { + it('should handle Query variable when sceneVariablesSetToVariables should keep options', () => { const variable = new QueryVariable({ name: 'test', label: 'test-label', @@ -846,6 +864,8 @@ describe('sceneVariablesSetToVariables', () => { includeAll: true, allValue: 'test-all', isMulti: true, + staticOptions: [{ label: 'test', value: 'test' }], + staticOptionsOrder: 'after', }); const set = new SceneVariableSet({ @@ -891,6 +911,13 @@ describe('sceneVariablesSetToVariables', () => { "regex": "", "skipUrlSync": false, "sort": "disabled", + "staticOptions": [ + { + "text": "test", + "value": "test", + }, + ], + "staticOptionsOrder": "after", }, } `); diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 74304a2a9a6..d90254a5de7 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -86,6 +86,11 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio multi: variable.state.isMulti, allowCustomValue: variable.state.allowCustomValue, skipUrlSync: variable.state.skipUrlSync, + staticOptions: variable.state.staticOptions?.map((option) => ({ + text: option.label, + value: String(option.value), + })), + staticOptionsOrder: variable.state.staticOptionsOrder, }); } else if (sceneUtils.isCustomVariable(variable)) { variables.push({ @@ -324,6 +329,11 @@ export function sceneVariablesSetToSchemaV2Variables( multi: variable.state.isMulti || false, skipUrlSync: variable.state.skipUrlSync || false, allowCustomValue: variable.state.allowCustomValue ?? true, + staticOptions: variable.state.staticOptions?.map((option) => ({ + text: option.label, + value: String(option.value), + })), + staticOptionsOrder: variable.state.staticOptionsOrder, }, }; variables.push(queryVariable); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 10dbc1d11e9..ae132430e0c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -50,7 +50,7 @@ import { DeprecatedInternalId, } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index cc1e2ee2ec1..ef03bdd17ee 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -25,7 +25,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { createPanelSaveModel } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/constants'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 2c51bee4427..2936ccfcf49 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -25,7 +25,7 @@ import { contextSrv } from 'app/core/core'; import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { DashboardDTO, DashboardDataDTO } from 'app/types'; +import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 9b6ec578234..b543bdeb10b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -19,9 +19,9 @@ import { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; -import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; +import { getReduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; @@ -46,7 +46,7 @@ import { trimDashboardForSnapshot, } from './transformSceneToSaveModel'; -standardTransformersRegistry.setInit(() => [reduceTransformRegistryItem]); +standardTransformersRegistry.setInit(() => [getReduceTransformRegistryItem()]); setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), getPanelPluginFromCache: (id: string) => undefined, diff --git a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx index 72510de5130..bbc49a8fc30 100644 --- a/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx @@ -11,7 +11,8 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; -import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from './annotations'; +import { AnnotationSettingsEdit, newAnnotationName } from './annotations/AnnotationSettingsEdit'; +import { AnnotationSettingsList } from './annotations/AnnotationSettingsList'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; export enum MoveDirection { diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index ed7c74ee3e0..ad47d202db9 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -31,20 +31,16 @@ import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; import { DeleteDashboardButton } from './DeleteDashboardButton'; +import { MoveProvisionedDashboardDrawer } from './MoveProvisionedDashboardDrawer'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; -export interface GeneralSettingsEditViewState extends DashboardEditViewState {} - -const EDITABLE_OPTIONS = [ - { label: 'Editable', value: true }, - { label: 'Read-only', value: false }, -]; - -const GRAPH_TOOLTIP_OPTIONS = [ - { value: 0, label: 'Default' }, - { value: 1, label: 'Shared crosshair' }, - { value: 2, label: 'Shared Tooltip' }, -]; +export interface GeneralSettingsEditViewState extends DashboardEditViewState { + showMoveModal?: boolean; + moveModalProps?: { + targetFolderUID?: string; + targetFolderTitle?: string; + }; +} export class GeneralSettingsEditView extends SceneObjectBase<GeneralSettingsEditViewState> @@ -167,22 +163,84 @@ export class GeneralSettingsEditView public onDeleteDashboard = () => {}; + public onProvisionedFolderChange = async (newUID?: string, newTitle?: string) => { + if (newUID !== this._dashboard.state.meta.folderUid) { + this.setState({ + showMoveModal: true, + moveModalProps: { + targetFolderUID: newUID, + targetFolderTitle: newTitle, + }, + }); + } + }; + + public onMoveModalDismiss = () => { + this.setState({ + showMoveModal: false, + moveModalProps: undefined, + }); + }; + + private onMoveSuccess = (folderUID: string, folderTitle: string) => { + const newMeta = { + ...this._dashboard.state.meta, + folderUid: folderUID, + folderTitle: folderTitle, + }; + this._dashboard.setState({ meta: newMeta }); + this.onMoveModalDismiss(); + }; + static Component = ({ model }: SceneComponentProps<GeneralSettingsEditView>) => { const dashboard = model.getDashboard(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { title, description, tags, meta, editable } = dashboard.useState(); + const { showMoveModal, moveModalProps } = model.useState(); const { sync: graphTooltip } = model.getCursorSync()?.useState() || {}; const { timeZone, weekStart, UNSAFE_nowDelay: nowDelay } = model.getTimeRange().useState(); const { intervals } = model.getRefreshPicker().useState(); const { hideTimeControls } = model.getDashboardControls().useState(); const { enabled: liveNow } = model.getLiveNowTimer().useState(); + const EDITABLE_OPTIONS = [ + { + label: t('dashboard-scene.general-settings-edit-view.editable_options.label.editable', 'Editable'), + value: true, + }, + { + label: t('dashboard-scene.general-settings-edit-view.editable_options.label.readonly', 'Read-only'), + value: false, + }, + ]; + + const GRAPH_TOOLTIP_OPTIONS = [ + { + value: 0, + label: t('dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.default', 'Default'), + }, + { + value: 1, + label: t( + 'dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.shared-crosshair', + 'Shared crosshair' + ), + }, + { + value: 2, + label: t( + 'dashboard-scene.general-settings-edit-view.graph_tooltip_options.label.shared-tooltip', + 'Shared tooltip' + ), + }, + ]; return ( <Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <NavToolbarActions dashboard={dashboard} /> <div style={{ maxWidth: '600px' }}> - <Box marginBottom={5}> + <Box display="flex" direction="column" gap={2} marginBottom={5}> <Field + noMargin label={ <Stack justifyContent="space-between"> <Label htmlFor="title-input"> @@ -202,6 +260,7 @@ export class GeneralSettingsEditView /> </Field> <Field + noMargin label={ <Stack justifyContent="space-between"> <Label htmlFor="description-input"> @@ -220,18 +279,18 @@ export class GeneralSettingsEditView onChange={(e: ChangeEvent<HTMLTextAreaElement>) => model.onDescriptionChange(e.target.value)} /> </Field> - <Field label={t('dashboard-settings.general.tags-label', 'Tags')}> + <Field noMargin label={t('dashboard-settings.general.tags-label', 'Tags')}> <TagsInput id="tags-input" tags={tags} onChange={model.onTagsChange} width={40} /> </Field> - <Field label={t('dashboard-settings.general.folder-label', 'Folder')}> - {dashboard.isManagedRepository() ? ( - <Input readOnly value={meta.folderTitle} /> - ) : ( - <FolderPicker value={meta.folderUid} onChange={model.onFolderChange} /> - )} + <Field noMargin label={t('dashboard-settings.general.folder-label', 'Folder')}> + <FolderPicker + value={meta.folderUid} + onChange={dashboard.isManagedRepository() ? model.onProvisionedFolderChange : model.onFolderChange} + /> </Field> <Field + noMargin label={t('dashboard-settings.general.editable-label', 'Editable')} description={t( 'dashboard-settings.general.editable-description', @@ -262,33 +321,51 @@ export class GeneralSettingsEditView label={t('dashboard-settings.general.panel-options-label', 'Panel options')} isOpen={true} > - <Field - label={t('dashboard-settings.general.panel-options-graph-tooltip-label', 'Graph tooltip')} - description={t( - 'dashboard-settings.general.panel-options-graph-tooltip-description', - 'Controls tooltip and hover highlight behavior across different panels. Reload the dashboard for changes to take effect' - )} - > - <RadioButtonGroup onChange={model.onTooltipChange} options={GRAPH_TOOLTIP_OPTIONS} value={graphTooltip} /> - </Field> + <Stack direction="column" gap={2}> + <Field + noMargin + label={t('dashboard-settings.general.panel-options-graph-tooltip-label', 'Graph tooltip')} + description={t( + 'dashboard-settings.general.panel-options-graph-tooltip-description', + 'Controls tooltip and hover highlight behavior across different panels. Reload the dashboard for changes to take effect' + )} + > + <RadioButtonGroup + onChange={model.onTooltipChange} + options={GRAPH_TOOLTIP_OPTIONS} + value={graphTooltip} + /> + </Field> - <Field - label={t('dashboard-settings.general.panels-preload-label', 'Preload panels')} - description={t( - 'dashboard-settings.general.panels-preload-description', - 'When enabled all panels will start loading as soon as the dashboard has been loaded.' - )} - > - <Switch - id="preload-panels-dashboards-toggle" - value={dashboard.state.preload} - onChange={(e) => model.onPreloadChange(e.currentTarget.checked)} - /> - </Field> + <Field + noMargin + label={t('dashboard-settings.general.panels-preload-label', 'Preload panels')} + description={t( + 'dashboard-settings.general.panels-preload-description', + 'When enabled all panels will start loading as soon as the dashboard has been loaded.' + )} + > + <Switch + id="preload-panels-dashboards-toggle" + value={dashboard.state.preload} + onChange={(e) => model.onPreloadChange(e.currentTarget.checked)} + /> + </Field> + </Stack> </CollapsableSection> <Box marginTop={3}>{meta.canDelete && <DeleteDashboardButton dashboard={dashboard} />}</Box> </div> + + {showMoveModal && moveModalProps && ( + <MoveProvisionedDashboardDrawer + dashboard={dashboard} + targetFolderUID={moveModalProps.targetFolderUID} + targetFolderTitle={moveModalProps.targetFolderTitle} + onDismiss={model.onMoveModalDismiss} + onSuccess={model.onMoveSuccess} + /> + )} </Page> ); }; diff --git a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx index 803068b979d..c6b40b81ff0 100644 --- a/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx +++ b/public/app/features/dashboard-scene/settings/JsonModelEditView.tsx @@ -11,7 +11,7 @@ import { Page } from 'app/core/components/Page/Page'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; -import { DashboardDataDTO, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDataDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { NameAlreadyExistsError, diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx new file mode 100644 index 00000000000..b1caef27bf5 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx @@ -0,0 +1,43 @@ +import { useProvisionedDashboardData } from '../saving/provisioned/hooks'; +import { DashboardScene } from '../scene/DashboardScene'; + +import { MoveProvisionedDashboardForm } from './MoveProvisionedDashboardForm'; + +export interface Props { + dashboard: DashboardScene; + targetFolderUID?: string; + targetFolderTitle?: string; + onDismiss: () => void; + onSuccess: (folderUID: string, folderTitle: string) => void; +} + +export function MoveProvisionedDashboardDrawer({ + dashboard, + targetFolderUID, + targetFolderTitle, + onDismiss, + onSuccess, +}: Props) { + const { defaultValues, loadedFromRef, readOnly, isGitHub, workflowOptions, isNew } = + useProvisionedDashboardData(dashboard); + + if (!defaultValues) { + return null; + } + + return ( + <MoveProvisionedDashboardForm + dashboard={dashboard} + defaultValues={defaultValues} + loadedFromRef={loadedFromRef} + readOnly={readOnly} + isGitHub={isGitHub} + isNew={isNew} + workflowOptions={workflowOptions} + targetFolderUID={targetFolderUID} + targetFolderTitle={targetFolderTitle} + onDismiss={onDismiss} + onSuccess={onSuccess} + /> + ); +} diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx new file mode 100644 index 00000000000..1e6496d4229 --- /dev/null +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx @@ -0,0 +1,241 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { getAppEvents } from '@grafana/runtime'; +import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; +import { + useCreateRepositoryFilesWithPathMutation, + useDeleteRepositoryFilesWithPathMutation, + useGetRepositoryFilesWithPathQuery, +} from 'app/api/clients/provisioning/v0alpha1'; +import { AnnoKeySourcePath } from 'app/features/apiserver/types'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; + +import { MoveProvisionedDashboardForm, Props } from './MoveProvisionedDashboardForm'; + +jest.mock('@grafana/runtime', () => { + const actual = jest.requireActual('@grafana/runtime'); + return { + ...actual, + getAppEvents: jest.fn(), + }; +}); + +jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ + useGetRepositoryFilesWithPathQuery: jest.fn(), + useCreateRepositoryFilesWithPathMutation: jest.fn(), + useDeleteRepositoryFilesWithPathMutation: jest.fn(), +})); + +jest.mock('app/api/clients/folder/v1beta1', () => ({ + useGetFolderQuery: jest.fn(), +})); + +jest.mock('../utils/useProvisionedRequestHandler', () => ({ + useProvisionedRequestHandler: jest.fn(), +})); + +jest.mock('react-router-dom-v5-compat', () => ({ + useNavigate: () => jest.fn(), +})); + +jest.mock('../components/Provisioned/ResourceEditFormSharedFields', () => ({ + ResourceEditFormSharedFields: () => <div data-testid="resource-edit-form" />, +})); + +function setup(props: Partial<Props> = {}) { + const user = userEvent.setup(); + + const mockDashboard = { + useState: jest.fn().mockReturnValue({ + editPanel: null, + }), + state: { + title: 'Test Dashboard', + }, + } as unknown as DashboardScene; + + const defaultProps: Props = { + dashboard: mockDashboard, + defaultValues: { + repo: 'test-repo', + path: 'folder1/dashboard.json', + ref: 'main', + workflow: 'write', + comment: '', + title: 'Test Dashboard', + description: '', + folder: { uid: '', title: '' }, + }, + readOnly: false, + isGitHub: true, + workflowOptions: [ + { label: 'Write', value: 'write' }, + { label: 'Branch', value: 'branch' }, + ], + targetFolderUID: 'target-folder-uid', + targetFolderTitle: 'Target Folder', + onDismiss: jest.fn(), + onSuccess: jest.fn(), + ...props, + }; + + return { + user, + ...render(<MoveProvisionedDashboardForm {...defaultProps} />), + props: defaultProps, + }; +} + +const mockCreateRequest = { + isSuccess: false, + isError: false, + isLoading: false, + error: null, +}; + +const mockDeleteRequest = { + isSuccess: false, + isError: false, + isLoading: false, + error: null, +}; + +describe('MoveProvisionedDashboardForm', () => { + beforeEach(() => { + jest.clearAllMocks(); + + // Setup default mocks + const mockAppEvents = { + publish: jest.fn(), + }; + (getAppEvents as jest.Mock).mockReturnValue(mockAppEvents); + + // Mock hooks + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: { + resource: { + file: { spec: { title: 'Test Dashboard' } }, + dryRun: { + metadata: { + annotations: { + [AnnoKeySourcePath]: 'folder1/dashboard.json', + }, + }, + }, + }, + }, + isLoading: false, + }); + + (useGetFolderQuery as jest.Mock).mockReturnValue({ + data: { + metadata: { + annotations: { + [AnnoKeySourcePath]: 'target-folder', + }, + }, + }, + isLoading: false, + }); + + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([jest.fn(), mockCreateRequest]); + + (useDeleteRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([jest.fn(), mockDeleteRequest]); + + (useProvisionedRequestHandler as jest.Mock).mockReturnValue(undefined); + }); + + it('should render the form with correct title and subtitle', () => { + setup(); + + expect(screen.getByText('Move Provisioned Dashboard')).toBeInTheDocument(); + expect(screen.getByText('Test Dashboard')).toBeInTheDocument(); + }); + + it('should render form even when currentFileData is not available', () => { + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: null, + isLoading: false, + }); + + setup(); + + // Form should still render, but move button should be disabled + expect(screen.getByText('Move Provisioned Dashboard')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /move dashboard/i })).toBeDisabled(); + }); + + it('should show loading spinner when file data is loading', () => { + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + }); + + setup(); + + expect(screen.getByText('Loading dashboard data')).toBeInTheDocument(); + }); + + it('should show read-only alert when repository is read-only', () => { + setup({ readOnly: true }); + + expect(screen.getByText('This repository is read only')).toBeInTheDocument(); + expect(screen.getByText(/This dashboard cannot be moved directly from Grafana/)).toBeInTheDocument(); + }); + + it('should show target path input with calculated path', () => { + setup(); + + expect(screen.getByDisplayValue('target-folder/dashboard.json')).toBeInTheDocument(); + }); + + it('should show error alert when file data has errors', () => { + (useGetRepositoryFilesWithPathQuery as jest.Mock).mockReturnValue({ + data: { + errors: ['File not found', 'Permission denied'], + resource: null, + }, + isLoading: false, + }); + + setup(); + + expect(screen.getByText('Error loading dashboard')).toBeInTheDocument(); + expect(screen.getByText('File not found')).toBeInTheDocument(); + expect(screen.getByText('Permission denied')).toBeInTheDocument(); + }); + + it('should disable move button when form is submitting', () => { + (useCreateRepositoryFilesWithPathMutation as jest.Mock).mockReturnValue([ + jest.fn(), + { + ...mockCreateRequest, + isLoading: true, + }, + ]); + + setup(); + + const moveButton = screen.getByRole('button', { name: /moving/i }); + expect(moveButton).toBeDisabled(); + expect(moveButton).toHaveTextContent('Moving...'); + }); + + it('should show move dashboard button when not loading', () => { + setup(); + + expect(screen.getByRole('button', { name: /move dashboard/i })).toBeInTheDocument(); + }); + + it('should call onDismiss when cancel button is clicked', async () => { + const { user, props } = setup(); + + const cancelButton = screen.getByRole('button', { name: /cancel/i }); + await user.click(cancelButton); + + expect(props.onDismiss).toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx new file mode 100644 index 00000000000..6a17c8df03b --- /dev/null +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx @@ -0,0 +1,238 @@ +import { useEffect, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { AppEvents } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { getAppEvents } from '@grafana/runtime'; +import { Alert, Button, Drawer, Field, Input, Spinner, Stack } from '@grafana/ui'; +import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; +import { + useCreateRepositoryFilesWithPathMutation, + useDeleteRepositoryFilesWithPathMutation, + useGetRepositoryFilesWithPathQuery, +} from 'app/api/clients/provisioning/v0alpha1'; +import { AnnoKeySourcePath } from 'app/features/apiserver/types'; + +import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields'; +import { ProvisionedDashboardFormData } from '../saving/shared'; +import { DashboardScene } from '../scene/DashboardScene'; +import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; + +export interface Props { + dashboard: DashboardScene; + defaultValues: ProvisionedDashboardFormData; + readOnly: boolean; + isGitHub: boolean; + isNew?: boolean; + workflowOptions: Array<{ label: string; value: string }>; + loadedFromRef?: string; + targetFolderUID?: string; + targetFolderTitle?: string; + onDismiss: () => void; + onSuccess: (folderUID: string, folderTitle: string) => void; +} + +export function MoveProvisionedDashboardForm({ + dashboard, + defaultValues, + loadedFromRef, + readOnly, + isGitHub, + isNew, + workflowOptions, + targetFolderUID, + targetFolderTitle, + onDismiss, + onSuccess, +}: Props) { + const methods = useForm<ProvisionedDashboardFormData>({ defaultValues }); + const { editPanel: panelEditor } = dashboard.useState(); + const { handleSubmit, watch } = methods; + const appEvents = getAppEvents(); + + const [ref, workflow] = watch(['ref', 'workflow']); + + const { data: currentFileData, isLoading: isLoadingFileData } = useGetRepositoryFilesWithPathQuery({ + name: defaultValues.repo, + path: defaultValues.path, + }); + + const { data: targetFolder } = useGetFolderQuery({ name: targetFolderUID! }, { skip: !targetFolderUID }); + + const [createFile, createRequest] = useCreateRepositoryFilesWithPathMutation(); + const [deleteFile, deleteRequest] = useDeleteRepositoryFilesWithPathMutation(); + const [targetPath, setTargetPath] = useState<string>(''); + + const navigate = useNavigate(); + + useEffect(() => { + const currentSourcePath = currentFileData?.resource?.dryRun?.metadata?.annotations?.[AnnoKeySourcePath]; + if (!targetFolderUID || !targetFolder || !currentSourcePath) { + return; + } + + const folderAnnotations = targetFolder.metadata.annotations || {}; + const targetFolderPath = folderAnnotations[AnnoKeySourcePath] || targetFolderTitle; + + const filename = currentSourcePath.split('/').pop(); + const newPath = `${targetFolderPath}/${filename}`; + + setTargetPath(newPath); + }, [currentFileData, targetFolder, targetFolderUID, targetFolderTitle]); + + const handleSubmitForm = async ({ repo, path, comment }: ProvisionedDashboardFormData) => { + if (!currentFileData?.resource?.file) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [ + t( + 'dashboard-scene.move-provisioned-dashboard-form.current-file-not-found', + 'Current dashboard file could not be found' + ), + ], + }); + return; + } + + const branchRef = workflow === 'write' ? loadedFromRef : ref; + const commitMessage = comment || `Move dashboard: ${dashboard.state.title}`; + + try { + await createFile({ + name: repo, + path: targetPath, + ref: branchRef, + message: commitMessage, + body: currentFileData.resource.file, + }).unwrap(); + + await deleteFile({ + name: repo, + path: path, + ref: branchRef, + message: commitMessage, + }).unwrap(); + } catch (error) { + if (createRequest.isSuccess && !deleteRequest.isSuccess) { + appEvents.publish({ + type: AppEvents.alertWarning.name, + payload: [ + t( + 'dashboard-scene.move-provisioned-dashboard-form.partial-failure-warning', + 'Dashboard was created at new location but could not be deleted from original location. Please manually remove the old file.' + ), + ], + }); + } + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('dashboard-scene.move-provisioned-dashboard-form.api-error', 'Failed to move dashboard'), error], + }); + } + }; + + const onWriteSuccess = () => { + panelEditor?.onDiscard(); + if (targetFolderUID && targetFolderTitle) { + onSuccess(targetFolderUID, targetFolderTitle); + } + navigate('/dashboards'); + }; + + const onBranchSuccess = () => { + panelEditor?.onDiscard(); + navigate(`/dashboards?new_pull_request_url=${createRequest.data?.urls?.newPullRequestURL}`); + }; + + useProvisionedRequestHandler({ + dashboard, + request: createRequest, + workflow, + handlers: { + onBranchSuccess, + onWriteSuccess, + }, + }); + + const isLoading = createRequest.isLoading || deleteRequest.isLoading; + + return ( + <Drawer + title={t('dashboard-scene.move-provisioned-dashboard-form.drawer-title', 'Move Provisioned Dashboard')} + subtitle={dashboard.state.title} + onClose={onDismiss} + > + <FormProvider {...methods}> + <form onSubmit={handleSubmit(handleSubmitForm)}> + <Stack direction="column" gap={2}> + {readOnly && ( + <Alert + title={t( + 'dashboard-scene.move-provisioned-dashboard-form.title-this-repository-is-read-only', + 'This repository is read only' + )} + > + <Trans i18nKey="dashboard-scene.move-provisioned-dashboard-form.move-read-only-message"> + This dashboard cannot be moved directly from Grafana because the repository is read-only. To move this + dashboard, please move the file in your Git repository. + </Trans> + </Alert> + )} + + {isLoadingFileData && ( + <Stack alignItems="center" gap={2}> + <Spinner /> + <div> + {t('dashboard-scene.move-provisioned-dashboard-form.loading-file-data', 'Loading dashboard data')} + </div> + </Stack> + )} + + {currentFileData?.errors?.length && currentFileData.errors.length > 0 && ( + <Alert + title={t('dashboard-scene.move-provisioned-dashboard-form.file-load-error', 'Error loading dashboard')} + severity="error" + > + {currentFileData.errors.map((error, index) => ( + <div key={index}>{error}</div> + ))} + </Alert> + )} + + <Field + noMargin + label={t('dashboard-scene.move-provisioned-dashboard-form.target-path-label', 'Target path')} + > + <Input readOnly value={targetPath} /> + </Field> + + <ResourceEditFormSharedFields + resourceType="dashboard" + isNew={isNew} + readOnly={readOnly} + workflow={workflow} + workflowOptions={workflowOptions} + isGitHub={isGitHub} + /> + + <Stack gap={2}> + <Button + variant="primary" + type="submit" + disabled={isLoading || readOnly || !currentFileData || isLoadingFileData} + > + {isLoading + ? t('dashboard-scene.move-provisioned-dashboard-form.moving', 'Moving...') + : t('dashboard-scene.move-provisioned-dashboard-form.move-action', 'Move dashboard')} + </Button> + <Button variant="secondary" onClick={onDismiss} fill="outline"> + <Trans i18nKey="dashboard-scene.move-provisioned-dashboard-form.cancel-action">Cancel</Trans> + </Button> + </Stack> + </Stack> + </form> + </FormProvider> + </Drawer> + ); +} diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx index 5dfa1a49659..d438ff9e694 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx @@ -3,7 +3,7 @@ import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { Permissions } from 'app/core/components/AccessControl'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx index c2d238354c0..501b6d7dfae 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx @@ -5,7 +5,7 @@ import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; import { VERSIONS_FETCH_LIMIT, VersionsEditView } from './VersionsEditView'; -import { historySrv } from './version-history'; +import { historySrv } from './version-history/HistorySrv'; jest.mock('./version-history/HistorySrv'); diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx index 6eb427966cf..1620111aecd 100644 --- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx @@ -11,14 +11,11 @@ import { NavToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; -import { - RevisionsModel, - VersionHistoryComparison, - VersionHistoryHeader, - VersionHistoryTable, - VersionsHistoryButtons, - historySrv, -} from './version-history'; +import { RevisionsModel, historySrv } from './version-history/HistorySrv'; +import { VersionsHistoryButtons } from './version-history/VersionHistoryButtons'; +import { VersionHistoryComparison } from './version-history/VersionHistoryComparison'; +import { VersionHistoryHeader } from './version-history/VersionHistoryHeader'; +import { VersionHistoryTable } from './version-history/VersionHistoryTable'; export const VERSIONS_FETCH_LIMIT = 10; diff --git a/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx index 739e19fcace..269d7bc4056 100644 --- a/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx +++ b/public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx @@ -245,7 +245,7 @@ export const AnnotationSettingsEdit = ({ annotation, editIndex, panels, onUpdate > <> <Select - options={panelFilters} + options={getPanelFilters()} value={panelFilter} onChange={onFilterTypeChange} data-testid={selectors.components.Annotations.annotationsTypeInput} @@ -320,20 +320,29 @@ enum PanelFilterType { ExcludePanels, } -const panelFilters = [ +const getPanelFilters = () => [ { - label: 'All panels', + label: t('dashboard-scene.get-panel-filters.label.all-panels', 'All panels'), value: PanelFilterType.AllPanels, - description: 'Send the annotation data to all panels that support annotations', + description: t( + 'dashboard-scene.get-panel-filters.description.annotation-panels-support-annotations', + 'Send the annotation data to all panels that support annotations' + ), }, { - label: 'Selected panels', + label: t('dashboard-scene.get-panel-filters.label.selected-panels', 'Selected panels'), value: PanelFilterType.IncludePanels, - description: 'Send the annotations to the explicitly listed panels', + description: t( + 'dashboard-scene.get-panel-filters.description.annotations-explicitly-listed-panels', + 'Send the annotations to the explicitly listed panels' + ), }, { - label: 'All panels except', + label: t('dashboard-scene.get-panel-filters.label.all-panels-except', 'All panels except'), value: PanelFilterType.ExcludePanels, - description: 'Do not send annotation data to the following panels', + description: t( + 'dashboard-scene.get-panel-filters.description.annotation-following-panels', + 'Do not send annotation data to the following panels' + ), }, ]; diff --git a/public/app/features/dashboard-scene/settings/annotations/index.tsx b/public/app/features/dashboard-scene/settings/annotations/index.tsx deleted file mode 100644 index 5c5ebb6cb18..00000000000 --- a/public/app/features/dashboard-scene/settings/annotations/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { AnnotationSettingsEdit, newAnnotationName } from './AnnotationSettingsEdit'; -export { AnnotationSettingsList } from './AnnotationSettingsList'; diff --git a/public/app/features/dashboard-scene/settings/links/DashboardLinkForm.tsx b/public/app/features/dashboard-scene/settings/links/DashboardLinkForm.tsx index fd784a09c8f..f82a9e78e61 100644 --- a/public/app/features/dashboard-scene/settings/links/DashboardLinkForm.tsx +++ b/public/app/features/dashboard-scene/settings/links/DashboardLinkForm.tsx @@ -7,11 +7,6 @@ import { CollapsableSection, TagsInput, Select, Field, Input, Checkbox, Button } import { LINK_ICON_MAP, NEW_LINK } from './utils'; -const linkTypeOptions = [ - { value: 'dashboards', label: 'Dashboards' }, - { value: 'link', label: 'Link' }, -]; - const linkIconOptions = Object.keys(LINK_ICON_MAP).map((key) => ({ label: key, value: key })); interface DashboardLinkFormProps { @@ -21,6 +16,13 @@ interface DashboardLinkFormProps { } export function DashboardLinkForm({ link, onUpdate, onGoBack }: DashboardLinkFormProps) { + const linkTypeOptions = [ + { + value: 'dashboards', + label: t('dashboard-scene.dashboard-link-form.link-type-options.label.dashboards', 'Dashboards'), + }, + { value: 'link', label: t('dashboard-scene.dashboard-link-form.link-type-options.label.link', 'Link') }, + ]; const onTagsChange = (tags: string[]) => { onUpdate({ ...link, tags: tags }); }; diff --git a/public/app/features/dashboard-scene/settings/utils.ts b/public/app/features/dashboard-scene/settings/utils.ts index 907612875c8..88011f08b00 100644 --- a/public/app/features/dashboard-scene/settings/utils.ts +++ b/public/app/features/dashboard-scene/settings/utils.ts @@ -5,7 +5,8 @@ import { t } from '@grafana/i18n'; import { SceneObject, SceneObjectState } from '@grafana/scenes'; import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; -import { AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx index a783a201324..a261cd00852 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx @@ -85,7 +85,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete <Trans i18nKey="dashboard-scene.variable-editor-form.general">General</Trans> </VariableLegend> <VariableTextField - name="Name" + name={t('dashboard-scene.variable-editor-form.name-name', 'Name')} description={t( 'dashboard-scene.variable-editor-form.description-template-variable-characters', 'The name of the template variable. (Max. 50 characters)' @@ -101,7 +101,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete error={nameError} /> <VariableTextField - name="Label" + name={t('dashboard-scene.variable-editor-form.name-label', 'Label')} description={t( 'dashboard-scene.variable-editor-form.description-optional-display-name', 'Optional display name' @@ -112,7 +112,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2} /> <VariableTextAreaField - name="Description" + name={t('dashboard-scene.variable-editor-form.name-description', 'Description')} defaultValue={description ?? ''} placeholder={t('dashboard-scene.variable-editor-form.placeholder-descriptive-text', 'Descriptive text')} onBlur={onDescriptionBlur} @@ -136,7 +136,10 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete showModal(ConfirmModal, { title: t('dashboard-scene.variable-editor-form.title.delete-variable', 'Delete variable'), body: `Are you sure you want to delete: ${name}?`, - confirmText: 'Delete variable', + confirmText: t( + 'dashboard-scene.variable-editor-form.confirmText.delete-variable', + 'Delete variable' + ), onConfirm: onDeleteVariable(hideModal), onDismiss: hideModal, isOpen: true, @@ -168,7 +171,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete text={t('dashboard-scene.variable-editor-form.text-running-query', 'Running query...')} /> ) : ( - `Run query` + t('dashbaord-scene.variable-editor-form.run-query', 'Run query') )} </Button> )} diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorListRow.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorListRow.tsx index 56a1ef550ea..1340fe923c0 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorListRow.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorListRow.tsx @@ -122,8 +122,15 @@ export function VariableEditorListRow({ <ConfirmModal isOpen={showDeleteModal} title={t('dashboard-scene.variable-editor-list-row.title-delete-variable', 'Delete variable')} - body={`Are you sure you want to delete: ${variableState.name}?`} - confirmText="Delete variable" + body={t( + 'dashboard-scene.variable-editor-list-row.body-delete-variable', + 'Are you sure you want to delete: {{variable}}?', + { variable: variableState.name } + )} + confirmText={t( + 'dashboard-scene.variable-editor-list-row.confirmText-delete-variable', + 'Delete variable' + )} onConfirm={onDeleteVariable} onDismiss={handleDeleteVariableModal(false)} /> diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx index 2d2beb67ff4..fcdf7881fce 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -119,7 +119,7 @@ export function AdHocVariableForm({ {datasourceSupported && onAllowCustomValueChange && ( <VariableCheckboxField value={allowCustomValue ?? true} - name="Allow custom values" + name={t('dashboard-scene.ad-hoc-variable-form.name-allow-custom-values', 'Allow custom values')} description={t( 'dashboard-scene.ad-hoc-variable-form.description-enables-users-custom-values', 'Enables users to add custom values to the list' diff --git a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx index 8ef97b0bd47..bdc98b74a94 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/CustomVariableForm.tsx @@ -46,7 +46,7 @@ export function CustomVariableForm({ </VariableLegend> <VariableTextAreaField - name="Values separated by comma" + name={t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma')} defaultValue={query} // eslint-disable-next-line @grafana/i18n/no-untranslated-strings placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value" diff --git a/public/app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm.tsx index a817f952cb7..6b763272d4c 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm.tsx @@ -2,7 +2,7 @@ import { FormEvent } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { SelectionOptionsForm } from './SelectionOptionsForm'; import { VariableLegend } from './VariableLegend'; @@ -50,7 +50,7 @@ export function DataSourceVariableForm({ <Trans i18nKey="dashboard-scene.data-source-variable-form.data-source-options">Data source options</Trans> </VariableLegend> <VariableSelectField - name="Type" + name={t('dashboard-scene.data-source-variable-form.name-type', 'Type')} value={typeValue} options={optionTypes} onChange={onChange} @@ -59,7 +59,7 @@ export function DataSourceVariableForm({ <VariableTextField defaultValue={regex} - name="Instance name filter" + name={t('dashboard-scene.data-source-variable-form.name-instance-name-filter', 'Instance name filter')} // eslint-disable-next-line @grafana/i18n/no-untranslated-strings placeholder="/.*-(.*)-.*/" onBlur={onRegExBlur} diff --git a/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx index 12653d4edfd..a8ed015b76a 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.tsx @@ -119,7 +119,7 @@ export function GroupByVariableForm({ {datasourceSupported && !inline && onAllowCustomValueChange && ( <VariableCheckboxField value={allowCustomValue} - name="Allow custom values" + name={t('dashboard-scene.group-by-variable-form.name-allow-custom-values', 'Allow custom values')} description={t( 'dashboard-scene.group-by-variable-form.description-enables-users-custom-values', 'Enables users to add custom values to the list' diff --git a/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx index e485f7dd40f..25b6686aa66 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/IntervalVariableForm.tsx @@ -51,7 +51,7 @@ export function IntervalVariableForm({ )} <VariableTextField defaultValue={intervals} - name="Values" + name={t('dashboard-scene.interval-variable-form.name-values', 'Values')} // eslint-disable-next-line @grafana/i18n/no-untranslated-strings placeholder="1m,10m,1h,6h,1d,7d" onBlur={onIntervalsChange} @@ -62,9 +62,9 @@ export function IntervalVariableForm({ <VariableCheckboxField value={autoEnabled} - name="Auto option" + name={t('dashboard-scene.interval-variable-form.name-auto-option', 'Auto option')} description={t( - 'dashbaord-scene.interval-variable-form.description-auto-option', + 'dashboard-scene.interval-variable-form.description-auto-option', 'Dynamically calculates interval by dividing time range by the count specified' )} onChange={onAutoEnabledChange} @@ -73,7 +73,7 @@ export function IntervalVariableForm({ {autoEnabled && ( <div className={styles.autoFields}> <VariableSelectField - name="Step count" + name={t('dashboard-scene.interval-variable-form.name-step-count', 'Step count')} description={t( 'dashboard-scene.interval-variable-form.description-step-count', 'How many times the current time range should be divided to calculate the value' @@ -86,7 +86,7 @@ export function IntervalVariableForm({ /> <VariableTextField value={autoMinInterval} - name="Min interval" + name={t('dashboard-scene.interval-variable-form.name-min-interval', 'Min interval')} description={t( 'dashboard-scene.interval-variable-form.description-calculated-value-below-threshold', 'The calculated value will not go below this threshold' diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index 3b5fc70d2e9..c643a98b212 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -77,6 +77,8 @@ describe('QueryVariableEditorForm', () => { const mockOnIncludeAllChange = jest.fn(); const mockOnAllValueChange = jest.fn(); const mockOnAllowCustomValueChange = jest.fn(); + const mockOnStaticOptionsChange = jest.fn(); + const mockOnStaticOptionsOrderChange = jest.fn(); const defaultProps: React.ComponentProps<typeof QueryVariableEditorForm> = { datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type }, @@ -99,6 +101,8 @@ describe('QueryVariableEditorForm', () => { allValue: 'custom all value', onAllValueChange: mockOnAllValueChange, onAllowCustomValueChange: mockOnAllowCustomValueChange, + onStaticOptionsChange: mockOnStaticOptionsChange, + onStaticOptionsOrderChange: mockOnStaticOptionsOrderChange, }; async function setup(props?: React.ComponentProps<typeof QueryVariableEditorForm>) { @@ -142,6 +146,10 @@ describe('QueryVariableEditorForm', () => { selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch ); + const staticOptionsToggle = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + ); + expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(regexInput).toBeInTheDocument(); @@ -158,6 +166,7 @@ describe('QueryVariableEditorForm', () => { expect(includeAllSwitch).toBeChecked(); expect(allValueInput).toBeInTheDocument(); expect(allValueInput).toHaveValue('custom all value'); + expect(staticOptionsToggle).toBeInTheDocument(); }); it('should call onDataSourceChange when changing the datasource', async () => { @@ -293,4 +302,169 @@ describe('QueryVariableEditorForm', () => { ((mockOnAllValueChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement).value ).toBe('custom all value and another value'); }); + + it('should call onStaticOptionsOrderChange when changing the static options order', async () => { + const { + renderer: { getByTestId }, + } = await setup(); + + // First enable static options + const staticOptionsToggle = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + ); + await userEvent.click(staticOptionsToggle); + + // Then access the dropdown + const staticOptionsOrderDropdown = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown + ); + await userEvent.click(staticOptionsOrderDropdown); // open the select + const anotherOption = await screen.getByText('After query values'); + await userEvent.click(anotherOption); + + expect(mockOnStaticOptionsOrderChange).toHaveBeenCalledTimes(1); + expect(mockOnStaticOptionsOrderChange.mock.calls[0][0]).toBe('after'); + }); + + it('should call onStaticOptionsChange when adding a static option', async () => { + const { + renderer: { getByTestId, getAllByTestId }, + } = await setup(); + + // First enable static options + const staticOptionsToggle = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + ); + await userEvent.click(staticOptionsToggle); + + const addButton = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton + ); + await userEvent.click(addButton); + + // Now enter label and value for the new option + const labelInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput + ); + const valueInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput + ); + + // Enter label for the new option (second input) + await userEvent.type(labelInputs[1], 'New Option Label'); + await userEvent.type(valueInputs[1], 'new-option-value'); + + expect(mockOnStaticOptionsChange).toHaveBeenCalled(); + expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([ + { value: 'new-option-value', label: 'New Option Label' }, + ]); + }); + + it('should call onStaticOptionsChange when removing a static option', async () => { + const { + renderer: { getAllByTestId }, + } = await setup({ + ...defaultProps, + staticOptions: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }); + + const deleteButtons = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton + ); + + // Remove the first option + await userEvent.click(deleteButtons[0]); + + expect(mockOnStaticOptionsChange).toHaveBeenCalledTimes(1); + // Should call with only the second option remaining + expect(mockOnStaticOptionsChange.mock.calls[0][0]).toEqual([{ value: 'option2', label: 'Option 2' }]); + }); + + it('should call onStaticOptionsChange when editing a static option label', async () => { + const { + renderer: { getAllByTestId }, + } = await setup({ + ...defaultProps, + staticOptions: [{ value: 'test', label: 'Test Label' }], + }); + + const labelInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput + ); + + await userEvent.clear(labelInputs[0]); + await userEvent.type(labelInputs[0], 'Updated Label'); + + expect(mockOnStaticOptionsChange).toHaveBeenCalled(); + expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([{ value: 'test', label: 'Updated Label' }]); + }); + + it('should call onStaticOptionsChange when editing a static option value', async () => { + const { + renderer: { getAllByTestId }, + } = await setup({ + ...defaultProps, + staticOptions: [{ value: 'old-value', label: 'Test Label' }], + }); + + const valueInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput + ); + + await userEvent.clear(valueInputs[0]); + await userEvent.type(valueInputs[0], 'new-value'); + + expect(mockOnStaticOptionsChange).toHaveBeenCalled(); + expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([{ value: 'new-value', label: 'Test Label' }]); + }); + + it('should remove static options and hide UI elements when static options switch is unchecked', async () => { + const { + renderer: { getByTestId, queryByTestId, getAllByTestId }, + } = await setup({ + ...defaultProps, + staticOptions: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ], + }); + + // Static options should be visible initially + expect( + getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle) + ).toBeChecked(); + expect( + getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown + ) + ).toBeInTheDocument(); + + // Option rows should be visible + expect( + getAllByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow) + ).toHaveLength(2); + + // Uncheck the static options switch + const staticOptionsToggle = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + ); + await userEvent.click(staticOptionsToggle); + + // Should call onStaticOptionsChange to remove static options + expect(mockOnStaticOptionsChange).toHaveBeenCalledTimes(1); + expect(mockOnStaticOptionsChange).toHaveBeenCalledWith(undefined); + + // Static options UI elements should be hidden + expect( + queryByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown + ) + ).not.toBeInTheDocument(); + expect( + queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow) + ).not.toBeInTheDocument(); + }); }); diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 941f7d97978..d6d4d467c9a 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -14,6 +14,11 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor'; import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect'; import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect'; +import { + StaticOptionsOrderType, + StaticOptionsType, + QueryVariableStaticOptions, +} from 'app/features/variables/query/QueryVariableStaticOptions'; import { VariableLegend } from './VariableLegend'; import { VariableTextAreaField } from './VariableTextAreaField'; @@ -41,6 +46,10 @@ interface QueryVariableEditorFormProps { onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void; allValue: string; onAllValueChange: (event: FormEvent<HTMLInputElement>) => void; + staticOptions?: StaticOptionsType; + staticOptionsOrder?: StaticOptionsOrderType; + onStaticOptionsChange?: (staticOptions: StaticOptionsType) => void; + onStaticOptionsOrderChange?: (staticOptionsOrder: StaticOptionsOrderType) => void; } export function QueryVariableEditorForm({ @@ -64,6 +73,10 @@ export function QueryVariableEditorForm({ onIncludeAllChange, allValue, onAllValueChange, + staticOptions, + staticOptionsOrder, + onStaticOptionsChange, + onStaticOptionsOrderChange, }: QueryVariableEditorFormProps) { const { value: dsConfig } = useAsync(async () => { const datasource = await getDataSourceSrv().get(datasourceRef ?? ''); @@ -106,7 +119,7 @@ export function QueryVariableEditorForm({ <VariableTextAreaField defaultValue={regex ?? ''} - name="Regex" + name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')} description={ <div> <Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional"> @@ -144,6 +157,15 @@ export function QueryVariableEditorForm({ refresh={refresh} /> + {onStaticOptionsChange && onStaticOptionsOrderChange && ( + <QueryVariableStaticOptions + staticOptions={staticOptions} + staticOptionsOrder={staticOptionsOrder} + onStaticOptionsChange={onStaticOptionsChange} + onStaticOptionsOrderChange={onStaticOptionsOrderChange} + /> + )} + <VariableLegend> <Trans i18nKey="dashboard-scene.query-variable-editor-form.selection-options">Selection options</Trans> </VariableLegend> diff --git a/public/app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm.tsx index 56858da935e..7043e029d1b 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm.tsx @@ -31,7 +31,7 @@ export function SelectionOptionsForm({ <Stack direction="column" gap={2} height="inherit" alignItems="start"> <VariableCheckboxField value={multi} - name="Multi-value" + name={t('dashboard-scene.selection-options-form.name-multi-value', 'Multi-value')} description={t( 'dashboard-scene.selection-options-form.description-enables-multiple-values-selected', 'Enables multiple values to be selected at the same time' @@ -42,7 +42,7 @@ export function SelectionOptionsForm({ {onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup <VariableCheckboxField value={allowCustomValue ?? true} - name="Allow custom values" + name={t('dashboard-scene.selection-options-form.name-allow-custom-values', 'Allow custom values')} description={t( 'dashboard-scene.selection-options-form.description-enables-users-custom-values', 'Enables users to add custom values to the list' @@ -53,7 +53,7 @@ export function SelectionOptionsForm({ )} <VariableCheckboxField value={includeAll} - name="Include All option" + name={t('dashboard-scene.selection-options-form.name-include-all-option', 'Include All option')} description={t( 'dashboard-scene.selection-options-form.description-enables-option-include-variables', 'Enables an option to include all values' @@ -65,7 +65,7 @@ export function SelectionOptionsForm({ <VariableTextField defaultValue={allValue ?? ''} onBlur={onAllValueChange} - name="Custom all value" + name={t('dashboard-scene.selection-options-form.name-custom-all-value', 'Custom all value')} testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput} /> )} diff --git a/public/app/features/dashboard-scene/settings/variables/components/TextBoxVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/TextBoxVariableForm.tsx index 57158f8820a..d5dd3579124 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/TextBoxVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/TextBoxVariableForm.tsx @@ -25,7 +25,7 @@ export function TextBoxVariableForm({ defaultValue, value, onChange, onBlur, inl <VariableTextField value={value} defaultValue={defaultValue} - name={inline ? undefined : 'Default value'} + name={inline ? undefined : t('dashboard-scene.text-box-variable.name-default-value', 'Default value')} placeholder={t('dashboard-scene.text-box-variable-form.placeholder-default-value-if-any', '(optional)')} onChange={onChange} onBlur={onBlur} diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableHideSelect.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableHideSelect.tsx index 92af84ba463..3ca3dfda718 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/VariableHideSelect.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableHideSelect.tsx @@ -10,14 +10,28 @@ interface Props { type: VariableType; } -const HIDE_OPTIONS = [ - { label: 'Nothing', value: VariableHide.dontHide }, - { label: 'Variable', value: VariableHide.hideVariable }, - { label: 'Label', value: VariableHide.hideLabel }, -]; - export function VariableHideSelect({ onChange, hide, type }: PropsWithChildren<Props>) { - const value = useMemo(() => HIDE_OPTIONS.find((o) => o.value === hide)?.value ?? HIDE_OPTIONS[0].value, [hide]); + const HIDE_OPTIONS = useMemo( + () => [ + { + label: t('dashboard-scene.variable-hide-select.hide_options.label.nothing', 'Nothing'), + value: VariableHide.dontHide, + }, + { + label: t('dashboard-scene.variable-hide-select.hide_options.label.variable', 'Variable'), + value: VariableHide.hideVariable, + }, + { + label: t('dashboard-scene.variable-hide-select.hide_options.label.label', 'Label'), + value: VariableHide.hideLabel, + }, + ], + [] + ); + const value = useMemo( + () => HIDE_OPTIONS.find((o) => o.value === hide)?.value ?? HIDE_OPTIONS[0].value, + [hide, HIDE_OPTIONS] + ); if (type === 'constant') { return null; diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableOptionsInput.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableOptionsInput.tsx new file mode 100644 index 00000000000..103aa10c54f --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableOptionsInput.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { t, Trans } from '@grafana/i18n'; +import { VariableValueOption } from '@grafana/scenes'; +import { Button, Input, Stack } from '@grafana/ui'; + +interface VariableOptionsFieldProps { + options: VariableValueOption[]; + onChange: (options: VariableValueOption[]) => void; + width?: number; +} + +export function VariableOptionsInput({ options, onChange, width }: VariableOptionsFieldProps) { + const [optionsLocal, setOptionsLocal] = useState(options.length ? options : [{ value: '', label: '' }]); + + const updateOptions = (newOptions: VariableValueOption[]) => { + setOptionsLocal(newOptions); + onChange( + newOptions + .map((option) => ({ + label: option.label.trim(), + value: String(option.value).trim(), + })) + .filter((option) => !!option.label) + ); + }; + + const handleValueChange = (index: number, value: string) => { + if (optionsLocal[index].value !== value) { + const newOptions = [...optionsLocal]; + newOptions[index] = { ...newOptions[index], value }; + updateOptions(newOptions); + } + }; + + const handleLabelChange = (index: number, label: string) => { + if (optionsLocal[index].label !== label) { + const newOptions = [...optionsLocal]; + newOptions[index] = { ...newOptions[index], label }; + updateOptions(newOptions); + } + }; + + const addOption = () => { + const newOption: VariableValueOption = { value: '', label: '' }; + const newOptions = [...optionsLocal, newOption]; + updateOptions(newOptions); + }; + + const removeOption = (index: number) => { + const newOptions = optionsLocal.filter((_, i) => i !== index); + updateOptions(newOptions); + }; + + return ( + <Stack direction="column" gap={2} width={width}> + {optionsLocal.map((option, index) => ( + <Stack + direction="row" + key={index} + data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow} + > + <Input + value={option.label} + placeholder={t('variables.query-variable-static-options.label-placeholder', 'display label')} + onChange={(e) => handleLabelChange(index, e.currentTarget.value)} + data-testid={ + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput + } + /> + <Input + value={String(option.value)} + placeholder={t('variables.query-variable-static-options.value-placeholder', 'value, default empty string')} + onChange={(e) => handleValueChange(index, e.currentTarget.value)} + data-testid={ + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput + } + /> + <Button + icon="times" + variant="secondary" + aria-label={t('variables.query-variable-static-options.remove-option-button-label', 'Remove option')} + onClick={() => removeOption(index)} + data-testid={ + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton + } + /> + </Stack> + ))} + <div> + <Button + icon="plus" + variant="secondary" + onClick={addOption} + data-testid={ + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton + } + aria-label={t('variables.query-variable-static-options.add-option-button-label', 'Add option')} + > + <Trans i18nKey="variables.query-variable-static-options.add-option-button-label">Add option</Trans> + </Button> + </div> + </Stack> + ); +} diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableTypeSelect.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableTypeSelect.tsx index 69a111f1cf2..3408469a6e2 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/VariableTypeSelect.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableTypeSelect.tsx @@ -2,6 +2,7 @@ import { PropsWithChildren, useMemo } from 'react'; import { SelectableValue, VariableType } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { t } from '@grafana/i18n'; import { VariableSelectField } from 'app/features/dashboard-scene/settings/variables/components/VariableSelectField'; import { EditableVariableType, getVariableTypeSelectOptions } from '../utils'; @@ -20,7 +21,7 @@ export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>) return ( <VariableSelectField - name="Variable type" + name={t('dashboard-scene.variable-type-select.name-variable-type', 'Variable type')} value={value} options={options} onChange={onChange} diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index 9dd39275c62..64c14e4cfc1 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -123,6 +123,10 @@ describe('QueryVariableEditor', () => { selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch ); + const staticOptionsToggle = renderer.getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + ); + expect(dataSourcePicker).toBeInTheDocument(); expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(queryEditor).toBeInTheDocument(); @@ -141,6 +145,7 @@ describe('QueryVariableEditor', () => { expect(includeAllSwitch).toBeChecked(); expect(allValueInput).toBeInTheDocument(); expect(allValueInput).toHaveValue('custom all value'); + expect(staticOptionsToggle).toBeInTheDocument(); }); it('should update the variable with default query for the selected DS', async () => { @@ -362,6 +367,70 @@ describe('QueryVariableEditor', () => { expect(variable.state.allValue).toBe('custom all value and another value'); }); + it('should update the variable state when adding two static options', async () => { + const { + variable, + renderer: { getByTestId, getAllByTestId }, + user, + } = await setup(); + + // Initially no static options + expect(variable.state.staticOptions).toBeUndefined(); + + // First enable static options + const staticOptionsToggle = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + ); + await userEvent.click(staticOptionsToggle); + + // Add first static option + const addButton = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton + ); + await user.click(addButton); + + // Enter label and value for first option + const labelInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput + ); + const valueInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput + ); + + await user.type(labelInputs[0], 'First Option'); + await user.type(valueInputs[0], 'first-value'); + + await waitFor(async () => { + await lastValueFrom(variable.validateAndUpdate()); + }); + + expect(variable.state.staticOptions).toEqual([{ label: 'First Option', value: 'first-value' }]); + + // Add second static option + await user.click(addButton); + + // Get updated inputs (now there should be 2 sets) + const updatedLabelInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput + ); + const updatedValueInputs = getAllByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput + ); + + // Enter label and value for second option + await user.type(updatedLabelInputs[1], 'Second Option'); + await user.type(updatedValueInputs[1], 'second-value'); + + await waitFor(async () => { + await lastValueFrom(variable.validateAndUpdate()); + }); + + expect(variable.state.staticOptions).toEqual([ + { label: 'First Option', value: 'first-value' }, + { label: 'Second Option', value: 'second-value' }, + ]); + }); + it('should return an empty array if variable is not a QueryVariable', () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const variable = new TextBoxVariable({ name: 'test', value: 'test value' }); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index 30d20b2cdd6..48d95fc3be5 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -14,6 +14,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor'; import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect'; import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect'; +import { StaticOptionsOrderType, StaticOptionsType } from 'app/features/variables/query/QueryVariableStaticOptions'; import { QueryVariableEditorForm } from '../components/QueryVariableForm'; import { VariableTextAreaField } from '../components/VariableTextAreaField'; @@ -27,8 +28,19 @@ interface QueryVariableEditorProps { type VariableQueryType = QueryVariable['state']['query']; export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) { - const { datasource, regex, sort, refresh, isMulti, includeAll, allValue, query, allowCustomValue } = - variable.useState(); + const { + datasource, + regex, + sort, + refresh, + isMulti, + includeAll, + allValue, + query, + allowCustomValue, + staticOptions, + staticOptionsOrder, + } = variable.useState(); const { value: timeRange } = sceneGraph.getTimeRange(variable).useState(); const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => { @@ -67,6 +79,16 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito onRunQuery(); }; + const onStaticOptionsChange = (staticOptions: StaticOptionsType) => { + onRunQuery(); + variable.setState({ staticOptions }); + }; + + const onStaticOptionsOrderChange = (staticOptionsOrder: StaticOptionsOrderType) => { + onRunQuery(); + variable.setState({ staticOptionsOrder }); + }; + return ( <QueryVariableEditorForm datasource={datasource ?? undefined} @@ -89,6 +111,10 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito onAllValueChange={onAllValueChange} allowCustomValue={allowCustomValue} onAllowCustomValueChange={onAllowCustomValueChange} + staticOptions={staticOptions} + staticOptionsOrder={staticOptionsOrder} + onStaticOptionsChange={onStaticOptionsChange} + onStaticOptionsOrderChange={onStaticOptionsOrderChange} /> ); } @@ -227,7 +253,7 @@ export function Editor({ variable }: { variable: QueryVariable }) { <VariableTextAreaField defaultValue={regex ?? ''} - name="Regex" + name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')} description={ <div> <Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional"> diff --git a/public/app/features/dashboard-scene/settings/variables/utils.test.ts b/public/app/features/dashboard-scene/settings/variables/utils.test.ts index b5a54c242c8..642b6df7d92 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.test.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.test.ts @@ -25,8 +25,8 @@ import { QueryVariableEditor } from './editors/QueryVariableEditor'; import { TextBoxVariableEditor } from './editors/TextBoxVariableEditor'; import { isEditableVariableType, - EDITABLE_VARIABLES, EDITABLE_VARIABLES_SELECT_ORDER, + getEditableVariables, getVariableTypeSelectOptions, getVariableEditor, getVariableScene, @@ -136,20 +136,22 @@ describe('getVariableTypeSelectOptions', () => { it('should contain all editable variable types', () => { const options = getVariableTypeSelectOptions(); - expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length); + const editableVariables = getEditableVariables(); + expect(options).toHaveLength(Object.keys(editableVariables).length); EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => { - expect(EDITABLE_VARIABLES).toHaveProperty(type); + expect(editableVariables).toHaveProperty(type); }); }); it('should return an array of selectable values for editable variable types', () => { + const editableVariables = getEditableVariables(); const options = getVariableTypeSelectOptions(); expect(options).toHaveLength(8); options.forEach((option, index) => { const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index]; - const variableTypeConfig = EDITABLE_VARIABLES[editableType]; + const variableTypeConfig = editableVariables[editableType]; expect(option.value).toBe(editableType); expect(option.label).toBe(variableTypeConfig.name); @@ -160,21 +162,23 @@ describe('getVariableTypeSelectOptions', () => { describe('when groupByVariable is disabled', () => { it('should contain all editable variable types except groupby', () => { + const editableVariables = getEditableVariables(); const options = getVariableTypeSelectOptions(); - expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length - 1); + expect(options).toHaveLength(Object.keys(editableVariables).length - 1); EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => { - expect(EDITABLE_VARIABLES).toHaveProperty(type); + expect(editableVariables).toHaveProperty(type); }); }); it('should return an array of selectable values for editable variable types', () => { + const editableVariables = getEditableVariables(); const options = getVariableTypeSelectOptions(); expect(options).toHaveLength(7); options.forEach((option, index) => { const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index]; - const variableTypeConfig = EDITABLE_VARIABLES[editableType]; + const variableTypeConfig = editableVariables[editableType]; expect(option.value).toBe(editableType); expect(option.label).toBe(variableTypeConfig.name); @@ -185,11 +189,12 @@ describe('getVariableTypeSelectOptions', () => { }); describe('getVariableEditor', () => { + const editableVariables = getEditableVariables(); beforeEach(() => { jest.clearAllMocks(); }); - it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])( + it.each(Object.keys(editableVariables) as EditableVariableType[])( 'should define an editor for variable type "%s"', (type) => { const editor = getVariableEditor(type); @@ -212,11 +217,12 @@ describe('getVariableEditor', () => { }); describe('getVariableScene', () => { + const editableVariables = getEditableVariables(); beforeAll(() => { setTemplateSrv(templateSrv); }); - it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])( + it.each(Object.keys(editableVariables) as EditableVariableType[])( 'should define a scene object for every variable type', (type) => { const variable = getVariableScene(type, { name: 'foo' }); diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index a43c6e9d8f3..684ba7b1ef0 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -1,6 +1,7 @@ import { chain } from 'lodash'; import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { ConstantVariable, @@ -46,59 +47,81 @@ export function isEditableVariableType(type: VariableType): type is EditableVari return type !== 'system'; } -export const EDITABLE_VARIABLES: Record<EditableVariableType, EditableVariableConfig> = { +export const getEditableVariables: () => Record<EditableVariableType, EditableVariableConfig> = () => ({ custom: { - name: 'Custom', - description: 'Values are static and defined manually', + name: t('dashboard-scene.get-editable-variables.name.custom', 'Custom'), + description: t( + 'dashboard-scene.get-editable-variables.description.values-are-static-and-defined-manually', + 'Values are static and defined manually' + ), editor: CustomVariableEditor, getOptions: getCustomVariableOptions, }, query: { - name: 'Query', - description: 'Values are fetched from a data source query', + name: t('dashboard-scene.get-editable-variables.name.query', 'Query'), + description: t( + 'dashboard-scene.get-editable-variables.description.values-fetched-source-query', + 'Values are fetched from a data source query' + ), editor: QueryVariableEditor, getOptions: getQueryVariableOptions, }, constant: { - name: 'Constant', - description: 'A hidden constant variable, useful for metric prefixes in dashboards you want to share', + name: t('dashboard-scene.get-editable-variables.name.constant', 'Constant'), + description: t( + 'dashboard-scene.get-editable-variables.description.hidden-constant-variable', + 'A hidden constant variable, useful for metric prefixes in dashboards you want to share' + ), editor: ConstantVariableEditor, getOptions: getConstantVariableOptions, }, interval: { - name: 'Interval', - description: 'Values are timespans, ex 1m, 1h, 1d', + name: t('dashboard-scene.get-editable-variables.name.interval', 'Interval'), + description: t( + 'dashboard-scene.get-editable-variables.description.values-timespans', + 'Values are timespans, ex 1m, 1h, 1d' + ), editor: IntervalVariableEditor, getOptions: getIntervalVariableOptions, }, datasource: { - name: 'Data source', - description: 'Dynamically switch the data source for multiple panels', + name: t('dashboard-scene.get-editable-variables.name.data-source', 'Data source'), + description: t( + 'dashboard-scene.get-editable-variables.description.dynamically-switch-source-multiple-panels', + 'Dynamically switch the data source for multiple panels' + ), editor: DataSourceVariableEditor, getOptions: getDataSourceVariableOptions, }, adhoc: { - name: 'Ad hoc filters', - description: 'Add key/value filters on the fly', + name: t('dashboard-scene.get-editable-variables.name.ad-hoc-filters', 'Ad hoc filters'), + description: t( + 'dashboard-scene.get-editable-variables.description.add-keyvalue-filters-on-the-fly', + 'Add key/value filters on the fly' + ), editor: AdHocFiltersVariableEditor, getOptions: getAdHocFilterOptions, }, groupby: { - name: 'Group by', - description: 'Add keys to group by on the fly', + name: t('dashboard-scene.get-editable-variables.name.group-by', 'Group by'), + description: t('dashboard-scene.get-editable-variables.description.group', 'Add keys to group by on the fly'), editor: GroupByVariableEditor, getOptions: getGroupByVariableOptions, }, textbox: { - name: 'Textbox', - description: 'Users can enter any arbitrary strings in a textbox', + name: t('dashboard-scene.get-editable-variables.name.textbox', 'Textbox'), + description: t( + 'dashboard-scene.get-editable-variables.description.users-enter-arbitrary-strings-textbox', + 'Users can enter any arbitrary strings in a textbox' + ), editor: TextBoxVariableEditor, getOptions: getTextBoxVariableOptions, }, -}; +}); export function getEditableVariableDefinition(type: string): EditableVariableConfig { - const editableVariable = EDITABLE_VARIABLES[type as EditableVariableType]; + const editableVariables = getEditableVariables(); + const editableVariable = editableVariables[type as EditableVariableType]; if (!editableVariable) { throw new Error(`Variable type ${type} not found`); } @@ -118,10 +141,11 @@ export const EDITABLE_VARIABLES_SELECT_ORDER: EditableVariableType[] = [ ]; export function getVariableTypeSelectOptions(): Array<SelectableValue<EditableVariableType>> { + const editableVariables = getEditableVariables(); const results = EDITABLE_VARIABLES_SELECT_ORDER.map((variableType) => ({ - label: EDITABLE_VARIABLES[variableType].name, + label: editableVariables[variableType].name, value: variableType, - description: EDITABLE_VARIABLES[variableType].description, + description: editableVariables[variableType].description, })); if (!config.featureToggles.groupByVariable) { @@ -133,7 +157,8 @@ export function getVariableTypeSelectOptions(): Array<SelectableValue<EditableVa } export function getVariableEditor(type: EditableVariableType) { - return EDITABLE_VARIABLES[type].editor; + const editableVariables = getEditableVariables(); + return editableVariables[type].editor; } interface CommonVariableProperties { diff --git a/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx b/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx index ac45683edbc..535f191c393 100644 --- a/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx @@ -44,7 +44,11 @@ export const RevertDashboardModal = ({ hideModal, onRestore, version }: RevertDa </Trans> </p> } - confirmText={`Yes, restore to version ${version.version}`} + confirmText={t( + 'dashboard-scene.revert-dashboard-modal.confirmText-restore-version', + 'Yes, restore to version {{version}}', + { version: version.version } + )} /> ); }; diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx index ba750d48f7d..f95912f333d 100644 --- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx +++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx @@ -65,7 +65,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck, onRestore } <td>{version.message}</td> <td className="text-right"> {idx === 0 ? ( - <Tag name="Latest" colorIndex={17} /> + <Tag name={t('dashboard-scene.version-history-table.name-latest', 'Latest')} colorIndex={17} /> ) : ( <ModalsController> {({ showModal, hideModal }) => ( diff --git a/public/app/features/dashboard-scene/settings/version-history/index.ts b/public/app/features/dashboard-scene/settings/version-history/index.ts deleted file mode 100644 index 880541e40d2..00000000000 --- a/public/app/features/dashboard-scene/settings/version-history/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { HistorySrv, historySrv, type RevisionsModel } from './HistorySrv'; -export { VersionHistoryTable } from './VersionHistoryTable'; -export { VersionHistoryHeader } from './VersionHistoryHeader'; -export { VersionsHistoryButtons } from './VersionHistoryButtons'; -export { VersionHistoryComparison } from './VersionHistoryComparison'; diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx index e6db41d98a3..aabd9707377 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsCode.tsx @@ -13,7 +13,6 @@ import { notifyApp } from 'app/core/actions'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { dispatch } from 'app/store/store'; -import { DashboardInteractions } from '../../utils/interactions'; import { ShareExportTab } from '../ShareExportTab'; import { ExportMode, ResourceExport } from './ResourceExport'; @@ -120,9 +119,7 @@ function ExportAsCodeRenderer({ model }: SceneComponentProps<ExportAsCode>) { icon="copy" disabled={dashboardJson.loading} getText={() => stringifiedDashboard ?? ''} - onClipboardCopy={() => { - DashboardInteractions.exportCopyJsonClicked(); - }} + onClipboardCopy={model.onClipboardCopy} > <Trans i18nKey="export.json.copy-button">Copy to clipboard</Trans> </ClipboardButton> diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsImage.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsImage.tsx new file mode 100644 index 00000000000..54202039923 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsImage.tsx @@ -0,0 +1,179 @@ +import { css } from '@emotion/css'; +import { saveAs } from 'file-saver'; +import { useEffect } from 'react'; +import { useAsyncFn } from 'react-use'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; +import { Alert, Button, useStyles2 } from '@grafana/ui'; +import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; +import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils'; + +import { ImagePreview } from '../components/ImagePreview'; +import { SceneShareTabState, ShareView } from '../types'; + +import { generateDashboardImage } from './utils'; + +export class ExportAsImage extends SceneObjectBase<SceneShareTabState> implements ShareView { + static Component = ExportAsImageRenderer; + + public getTabLabel() { + return t('share-modal.image.title', 'Export as image'); + } +} + +function ExportAsImageRenderer({ model }: SceneComponentProps<ExportAsImage>) { + const { onDismiss } = model.useState(); + const dashboard = getDashboardSceneFor(model); + const styles = useStyles2(getStyles); + + const [{ loading: isLoading, value: imageBlob, error }, onExport] = useAsyncFn(async () => { + try { + const result = await generateDashboardImage({ + dashboard, + scale: config.rendererDefaultImageScale || 1, + }); + + if (result.error) { + throw new Error(result.error); + } + + DashboardInteractions.generateDashboardImageClicked({ + scale: config.rendererDefaultImageScale || 1, + shareResource: 'dashboard', + success: true, + }); + + return result.blob; + } catch (error) { + console.error('Error exporting image:', error); + DashboardInteractions.generateDashboardImageClicked({ + scale: config.rendererDefaultImageScale || 1, + shareResource: 'dashboard', + success: false, + error: error instanceof Error ? error.message : 'Failed to generate image', + }); + throw error; // Re-throw to let useAsyncFn handle the error state + } + }, [dashboard]); + + // Clean up object URLs when component unmounts + useEffect(() => { + return () => { + if (imageBlob) { + URL.revokeObjectURL(URL.createObjectURL(imageBlob)); + } + }; + }, [imageBlob]); + + const onDownload = () => { + if (!imageBlob) { + return; + } + + const time = new Date().getTime(); + const name = dashboard.state.title; + saveAs(imageBlob, `${name}-${time}.png`); + + DashboardInteractions.downloadDashboardImageClicked({ + fileName: `${name}-${time}.png`, + shareResource: 'dashboard', + }); + }; + + if (!config.rendererAvailable) { + return <RendererAlert />; + } + + return ( + <main> + <p className={styles.info}> + <Trans i18nKey="share-modal.image.info-text">Save this dashboard as an image</Trans> + </p> + + <div + className={styles.buttonRow} + role="group" + aria-label={t('share-modal.image.actions', 'Image export actions')} + > + {!imageBlob ? ( + <Button + variant="primary" + onClick={onExport} + disabled={isLoading} + icon="gf-layout-simple" + aria-describedby={isLoading ? 'generate-status' : undefined} + > + <Trans i18nKey="share-modal.image.generate-button">Generate image</Trans> + </Button> + ) : ( + <Button variant="primary" onClick={onDownload} icon="download-alt"> + <Trans i18nKey="share-modal.image.download-button">Download image</Trans> + </Button> + )} + <Button variant="secondary" onClick={onDismiss} fill="outline"> + <Trans i18nKey="share-modal.image.cancel-button">Cancel</Trans> + </Button> + </div> + + {isLoading && ( + <div id="generate-status" aria-live="polite" className="sr-only"> + <Trans i18nKey="share-modal.image.generating">Generating image...</Trans> + </div> + )} + + <ImagePreview + imageBlob={imageBlob || null} + isLoading={isLoading} + error={ + error + ? { + title: t('share-modal.image.error-title', 'Failed to generate image'), + message: error instanceof Error ? error.message : 'Failed to generate image', + } + : null + } + title={dashboard.state.title} + /> + </main> + ); +} + +function RendererAlert() { + if (config.rendererAvailable) { + return null; + } + + return ( + <Alert severity="info" title={t('share-modal.link.render-alert', 'Image renderer plugin not installed')}> + <div>{t('share-modal.link.render-alert', 'Image renderer plugin not installed')}</div> + <div> + <Trans i18nKey="share-modal.link.render-instructions"> + To render an image, you must install the{' '} + <a + href="https://grafana.com/grafana/plugins/grafana-image-renderer" + target="_blank" + rel="noopener noreferrer" + className="external-link" + > + Grafana image renderer plugin + </a> + . Please contact your Grafana administrator to install the plugin. + </Trans> + </div> + </Alert> + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + info: css({ + marginBottom: theme.spacing(2), + }), + buttonRow: css({ + display: 'flex', + gap: theme.spacing(2), + marginBottom: theme.spacing(2), + }), +}); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx index 22c5d54e16f..5fa8dc77b48 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.test.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../../scene/DashboardScene'; @@ -9,21 +8,32 @@ import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGrid import ExportButton from './ExportButton'; -const selector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton; - describe('ExportButton', () => { - it('should render Export menu', async () => { + it('should render Export button', async () => { setup(); - expect(await screen.findByTestId(selector.arrowMenu)).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /export dashboard/i })).toBeInTheDocument(); }); - it('should render menu when arrow button clicked', async () => { + it('should show menu when export button is clicked', async () => { setup(); - const arrowMenu = await screen.findByTestId(selector.arrowMenu); - await userEvent.click(arrowMenu); + const exportButton = await screen.findByRole('button', { name: /export dashboard/i }); + expect(exportButton).toHaveAttribute('aria-expanded', 'false'); - expect(await screen.findByTestId(selector.Menu.container)).toBeInTheDocument(); + await userEvent.click(exportButton); + + expect(exportButton).toHaveAttribute('aria-expanded', 'true'); + expect(await screen.findByRole('menu')).toBeInTheDocument(); + }); + + it('should show export options in the menu', async () => { + setup(); + + const exportButton = await screen.findByRole('button', { name: /export dashboard/i }); + await userEvent.click(exportButton); + + // Should show JSON export option + expect(await screen.findByRole('menuitem', { name: /export as json/i })).toBeInTheDocument(); }); }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx index 5870eb7a5ec..7486301500b 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportButton.tsx @@ -8,8 +8,6 @@ import { DashboardScene } from '../../scene/DashboardScene'; import ExportMenu from './ExportMenu'; -const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton; - interface Props { dashboard: DashboardScene; } @@ -24,17 +22,20 @@ export default function ExportButton({ dashboard }: Props) { const MenuActions = () => <ExportMenu dashboard={dashboard} />; return ( - <ButtonGroup data-testid={newExportButtonSelector.container}> + <ButtonGroup> <Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}> <Button - data-testid={newExportButtonSelector.arrowMenu} size="sm" variant="secondary" fill="solid" tooltip={t('export.menu.export-as-json-tooltip', 'Export')} + aria-label={t('dashboard.export.button.label', 'Export dashboard')} + aria-haspopup="menu" + aria-expanded={isOpen} + data-testid={e2eSelectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu} > <Trans i18nKey="export.menu.export-as-json-label">Export</Trans>  - <Icon name={isOpen ? 'angle-up' : 'angle-down'} size="sm" /> + <Icon name={isOpen ? 'angle-up' : 'angle-down'} size="sm" aria-hidden="true" /> </Button> </Dropdown> </ButtonGroup> diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx index 530dd8ae765..82af5dd953e 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../../scene/DashboardScene'; @@ -8,12 +8,24 @@ import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGrid import ExportMenu from './ExportMenu'; -const selector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton.Menu; - describe('ExportMenu', () => { it('should render menu items', async () => { setup(); - expect(await screen.findByTestId(selector.exportAsJson)).toBeInTheDocument(); + expect(await screen.findByRole('menuitem', { name: /export as json/i })).toBeInTheDocument(); + }); + + describe('sharingDashboardImage feature toggle', () => { + it('should render image export option when enabled', async () => { + config.featureToggles.sharingDashboardImage = true; + setup(); + expect(await screen.findByRole('menuitem', { name: /export as image/i })).toBeInTheDocument(); + }); + + it('should not render image export option when disabled', async () => { + config.featureToggles.sharingDashboardImage = false; + setup(); + expect(screen.queryByRole('menuitem', { name: /export as image/i })).not.toBeInTheDocument(); + }); }); }); @@ -23,11 +35,13 @@ function setup() { pluginId: 'table', key: 'panel-12', }); + const dashboard = new DashboardScene({ title: 'hello', uid: 'dash-1', $timeRange: new SceneTimeRange({}), body: DefaultGridLayoutManager.fromVizPanels([panel]), }); + render(<ExportMenu dashboard={dashboard} />); } diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx index a38a8fa1964..d5524108b07 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportMenu.tsx @@ -13,7 +13,7 @@ const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportBu export interface ExportDrawerMenuItem { shareId: string; - testId: string; + testId?: string; label: string; description?: string; icon: IconName; @@ -50,6 +50,14 @@ export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) onClick: () => onMenuItemClick(shareDashboardType.export), }); + menuItems.push({ + shareId: shareDashboardType.image, + icon: 'camera', + label: t('share-dashboard.menu.export-image-title', 'Export as image'), + renderCondition: Boolean(config.featureToggles.sharingDashboardImage), + onClick: () => onMenuItemClick(shareDashboardType.image), + }); + return menuItems.filter((item) => item.renderCondition); }, []); @@ -63,15 +71,18 @@ export default function ExportMenu({ dashboard }: { dashboard: DashboardScene }) }; return ( - <Menu data-testid={newExportButtonSelector.container}> + <Menu + ariaLabel={t('dashboard.export.menu.label', 'Export dashboard menu')} + data-testid={newExportButtonSelector.container} + > {buildMenuItems().map((item) => ( <Menu.Item key={item.label} - testId={item.testId} label={item.label} icon={item.icon} description={item.description} onClick={() => onClick(item)} + testId={item.testId} /> ))} </Menu> diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx index 0fe12b9e977..edc4aaf3384 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx @@ -1,5 +1,6 @@ import { AsyncState } from 'react-use/lib/useAsync'; +import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; @@ -28,6 +29,8 @@ interface Props { onViewYAML: () => void; } +const selector = e2eSelectors.pages.ExportDashboardDrawer.ExportAsJson; + export function ResourceExport({ dashboardJson, isSharingExternally, @@ -110,7 +113,12 @@ export function ResourceExport({ (initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && ( <Stack gap={1} alignItems="start"> <Label>{switchExportLabel}</Label> - <Switch label={switchExportLabel} value={isSharingExternally} onChange={onShareExternallyChange} /> + <Switch + label={switchExportLabel} + value={isSharingExternally} + onChange={onShareExternallyChange} + data-testid={selector.exportExternallyToggle} + /> </Stack> )} </Stack> diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts b/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts new file mode 100644 index 00000000000..1c4ad371d0a --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ExportButton/utils.test.ts @@ -0,0 +1,132 @@ +import { of } from 'rxjs'; + +import { config, getBackendSrv } from '@grafana/runtime'; +import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; + +import { DashboardScene } from '../../scene/DashboardScene'; + +import { generateDashboardImage } from './utils'; + +// Mock the dependencies +jest.mock('@grafana/runtime', () => ({ + config: { + rendererAvailable: true, + bootData: { + user: { + orgId: 1, + }, + }, + }, + getBackendSrv: jest.fn(), +})); + +jest.mock('app/features/dashboard-scene/utils/getDashboardUrl', () => ({ + getDashboardUrl: jest + .fn() + .mockImplementation((params: { updateQuery?: Record<string, string | number | boolean> }) => { + const url = new URL('http://test-url'); + if (params.updateQuery) { + Object.entries(params.updateQuery).forEach(([key, value]) => { + url.searchParams.append(key, String(value)); + }); + } + return url.toString(); + }), +})); + +describe('Dashboard Export Image Utils', () => { + describe('generateDashboardImage', () => { + it('should handle various error scenarios', async () => { + const testCases = [ + { + setup: () => { + config.rendererAvailable = false; + // Reset the mock for this test case + (getBackendSrv as jest.Mock).mockReset(); + }, + expectedError: 'Image renderer plugin not installed', + }, + { + setup: () => { + config.rendererAvailable = true; + (getBackendSrv as jest.Mock).mockReturnValue({ + fetch: jest.fn().mockReturnValue(of({ ok: false, status: 500, statusText: 'Server Error' })), + }); + }, + expectedError: 'Failed to generate image: 500 Server Error', + }, + { + setup: () => { + config.rendererAvailable = true; + (getBackendSrv as jest.Mock).mockReturnValue({ + fetch: jest.fn().mockReturnValue(of({ ok: true, data: 'invalid-data' })), + }); + }, + expectedError: 'Invalid response data format', + }, + { + setup: () => { + config.rendererAvailable = true; + (getBackendSrv as jest.Mock).mockReturnValue({ + fetch: jest.fn().mockReturnValue(of(Promise.reject(new Error('Network error')))), + }); + }, + expectedError: 'Network error', + }, + ]; + + const dashboard = { + state: { + uid: 'test-uid', + }, + } as DashboardScene; + + for (const testCase of testCases) { + testCase.setup(); + const result = await generateDashboardImage({ dashboard }); + expect(result.error).toBe(testCase.expectedError); + expect(result.blob).toBeInstanceOf(Blob); + expect(result.blob.size).toBe(0); + } + }); + + it('should generate image successfully with custom scale', async () => { + config.rendererAvailable = true; + const mockBlob = new Blob(['test'], { type: 'image/png' }); + const fetchMock = jest.fn().mockReturnValue(of({ ok: true, data: mockBlob })); + (getBackendSrv as jest.Mock).mockReturnValue({ fetch: fetchMock }); + + const dashboard = { + state: { + uid: 'test-uid', + }, + } as DashboardScene; + + const result = await generateDashboardImage({ dashboard, scale: 2 }); + + expect(result.error).toBeUndefined(); + expect(result.blob).toBe(mockBlob); + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringMatching(/height=-1.*scale=2.*kiosk=true.*hideNav=true.*fullPageImage=true/), + responseType: 'blob', + }) + ); + expect(getDashboardUrl).toHaveBeenCalledWith({ + uid: 'test-uid', + currentQueryParams: '', + render: true, + absolute: true, + updateQuery: { + height: -1, + width: 1000, + scale: 2, + kiosk: true, + hideNav: true, + orgId: '1', + fullPageImage: true, + }, + }); + }); + }); +}); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts b/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts new file mode 100644 index 00000000000..64d84893b45 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/ExportButton/utils.ts @@ -0,0 +1,89 @@ +import { lastValueFrom } from 'rxjs'; + +import { config, getBackendSrv } from '@grafana/runtime'; +import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; + +import { DashboardScene } from '../../scene/DashboardScene'; + +/** + * Options for generating a dashboard image + */ +export interface ImageGenerationOptions { + dashboard: DashboardScene; + scale?: number; +} + +/** + * Result of image generation attempt + */ +export interface ImageGenerationResult { + blob: Blob; + error?: string; +} + +/** + * Generates a dashboard image using the renderer service + * @param options The options for image generation + * @returns A promise that resolves to the image generation result + */ +export async function generateDashboardImage({ + dashboard, + scale = config.rendererDefaultImageScale || 1, +}: ImageGenerationOptions): Promise<ImageGenerationResult> { + try { + // Check if renderer plugin is available + if (!config.rendererAvailable) { + return { + blob: new Blob(), + error: 'Image renderer plugin not installed', + }; + } + + const imageUrl = getDashboardUrl({ + uid: dashboard.state.uid, + currentQueryParams: window.location.search, + render: true, + absolute: true, + updateQuery: { + height: -1, // image renderer will scroll through the dashboard and set the appropriate height + width: config.rendererDefaultImageWidth || 1000, + scale, + kiosk: true, + hideNav: true, + orgId: String(config.bootData.user.orgId), + fullPageImage: true, + }, + }); + + const response = await lastValueFrom( + getBackendSrv().fetch<Blob>({ + url: imageUrl, + responseType: 'blob', + }) + ); + + if (!response.ok) { + return { + blob: new Blob(), + error: `Failed to generate image: ${response.status} ${response.statusText}`, + }; + } + + // Validate response data format + if (!(response.data instanceof Blob)) { + return { + blob: new Blob(), + error: 'Invalid response data format', + }; + } + + return { + blob: response.data, + }; + } catch (error) { + return { + blob: new Blob(), + error: error instanceof Error && error.message ? error.message : 'Failed to generate image', + }; + } +} diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx index 0dad98fb05e..00af6541807 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareButton.tsx @@ -22,8 +22,8 @@ export default function ShareButton({ dashboard, panel }: { dashboard: Dashboard const [_, buildUrl] = useAsyncFn(async () => { DashboardInteractions.toolbarShareClick(); - return await buildShareUrl(dashboard, panel); - }, [dashboard]); + await buildShareUrl(dashboard, panel); + }, [dashboard, panel]); const onMenuClick = useCallback((isOpen: boolean) => { if (isOpen) { diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index 91439fe662a..c049579fb38 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -3,9 +3,9 @@ import { render, screen } from '@testing-library/react'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { SceneTimeRange, VizPanel } from '@grafana/scenes'; import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types/accessControl'; import { config } from '../../../../core/config'; -import { AccessControlAction } from '../../../../types'; import { grantUserPermissions } from '../../../alerting/unified/mocks'; import { DashboardScene, DashboardSceneState } from '../../scene/DashboardScene'; import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx index bd8c983e1c8..0a50fb9530e 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx @@ -7,7 +7,7 @@ import { config, locationService } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; import { IconName, Menu } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { isPublicDashboardsEnabled } from '../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { getTrackingSource, shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx index d2affefd482..4d6ca049b89 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx @@ -7,7 +7,7 @@ import { contextSrv } from 'app/core/core'; import { publicDashboardApi, useAddRecipientMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { validEmailRegex } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { useShareDrawerContext } from '../../../../ShareDrawer/ShareDrawerContext'; import ShareConfiguration from '../../ShareConfiguration'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx index 3f1c51db7b8..4c482660047 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/CreateEmailSharing.tsx @@ -9,7 +9,7 @@ import { contextSrv } from 'app/core/core'; import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { PublicDashboardShareType } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { EmailSharingPricingAlert } from '../../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/EmailSharingPricingAlert'; import { useShareDrawerContext } from '../../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx index 8352e8a9495..c871537e4c4 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/PublicShare/CreatePublicSharing.tsx @@ -9,7 +9,7 @@ import { contextSrv } from 'app/core/core'; import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { PublicDashboardShareType } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PublicDashboardAlert } from '../../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/PublicDashboardAlert'; import { useShareDrawerContext } from '../../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx index 9e74065d62e..6c7b0d55ec7 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareAlerts.tsx @@ -7,7 +7,7 @@ import { PublicDashboard, PublicDashboardShareType, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { NoUpsertPermissionsAlert } from '../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert'; import { PublicDashboardAlert } from '../../../../dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/PublicDashboardAlert'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx index 57d2423e167..62d22223663 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareConfiguration.tsx @@ -10,7 +10,7 @@ import { contextSrv } from 'app/core/core'; import { publicDashboardApi, useUpdatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { ConfigPublicDashboardForm } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx index ca95eee9110..41637090ad0 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareExternally.tsx @@ -21,7 +21,7 @@ import { } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { getDashboardSceneFor } from 'app/features/dashboard-scene/utils/utils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction'; import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx index f58c16298ca..89e14df3c5f 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-externally/ShareTypeSelect.tsx @@ -14,7 +14,7 @@ import { PublicDashboardShareType, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { useShareDrawerContext } from '../../ShareDrawer/ShareDrawerContext'; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx index 89285aa03a6..75861e9ee92 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { SceneComponentProps } from '@grafana/scenes'; import { Alert, Button, ClipboardButton, Spinner, Stack, TextLink } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { SnapshotSharingOptions } from '../../../../dashboard/services/SnapshotSrv'; import { ShareDrawerConfirmAction } from '../../ShareDrawer/ShareDrawerConfirmAction'; diff --git a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx index 7b5b87a11cf..80a880a04c9 100644 --- a/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareDrawer/ShareDrawer.tsx @@ -6,6 +6,7 @@ import { shareDashboardType } from '../../../dashboard/components/ShareModal/uti import { DashboardScene } from '../../scene/DashboardScene'; import { getDashboardSceneFor } from '../../utils/utils'; import { ExportAsCode } from '../ExportButton/ExportAsCode'; +import { ExportAsImage } from '../ExportButton/ExportAsImage'; import { ShareExternally } from '../ShareButton/share-externally/ShareExternally'; import { ShareInternally } from '../ShareButton/share-internally/ShareInternally'; import { ShareSnapshot } from '../ShareButton/share-snapshot/ShareSnapshot'; @@ -93,6 +94,8 @@ function getShareView( return new ShareSnapshot({ dashboardRef, panelRef, onDismiss }); case shareDashboardType.export: return new ExportAsCode({ onDismiss }); + case shareDashboardType.image: + return new ExportAsImage({ onDismiss }); default: return new ShareInternally({ onDismiss }); } diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 88f4df0beb6..1c19882b877 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -13,7 +13,7 @@ import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from import { ObjectMeta } from 'app/features/apiserver/types'; import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; -import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; +import { isDashboardV2Spec, isV1ClassicDashboard } from 'app/features/dashboard/api/utils'; import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1'; import { K8S_V2_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v2'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; @@ -27,7 +27,7 @@ import { transformSceneToSaveModel } from '../serialization/transformSceneToSave import { transformSceneToSaveModelSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2'; import { getVariablesCompatibility } from '../utils/getVariablesCompatibility'; import { DashboardInteractions } from '../utils/interactions'; -import { getDashboardSceneFor } from '../utils/utils'; +import { getDashboardSceneFor, hasLibraryPanelsInV1Dashboard } from '../utils/utils'; import { ExportMode, ResourceExport } from './ExportButton/ResourceExport'; import { SceneShareTabState, ShareView } from './types'; @@ -171,7 +171,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme status: {}, }, initialSaveModelVersion, - hasLibraryPanels: undefined, + hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec1), }; } catch (err) { return { @@ -195,7 +195,7 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme status: {}, }, initialSaveModelVersion, - hasLibraryPanels: undefined, + hasLibraryPanels: hasLibraryPanelsInV1Dashboard(spec), }; } } @@ -216,6 +216,10 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme status: {}, }, initialSaveModelVersion, + hasLibraryPanels: + initialSaveModelVersion === 'v1' && !isDashboardV2Spec(origDashboard) + ? hasLibraryPanelsInV1Dashboard(origDashboard) + : false, }; } @@ -238,15 +242,19 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme const exportableV1 = isSharingExternally ? await makeExportableV1(oldModel) : initialSaveModel; return { json: exportableV1, - hasLibraryPanels: undefined, + hasLibraryPanels: hasLibraryPanelsInV1Dashboard(initialSaveModel), initialSaveModelVersion, }; } // legacy mode or classic mode when dashboardNewLayouts is disabled + // At this point we know that dashboard should be V1 or could have produced an error return { json: exportable, - hasLibraryPanels: undefined, + hasLibraryPanels: + 'error' in exportable || !isV1ClassicDashboard(origDashboard) + ? false + : hasLibraryPanelsInV1Dashboard(origDashboard), initialSaveModelVersion, }; }; @@ -267,8 +275,27 @@ export class ShareExportTab extends SceneObjectBase<ShareExportTabState> impleme } const extension = isViewingYAML ? 'yaml' : 'json'; saveAs(blob, `${title}-${time}.${extension}`); + DashboardInteractions.exportDownloadJsonClicked({ externally: isSharingExternally, + dashboard_schema_version: dashboard.initialSaveModelVersion, + has_library_panels: Boolean(dashboard.hasLibraryPanels), + format: isViewingYAML ? 'yaml' : 'json', + action: 'download', + }); + }; + + public onClipboardCopy = async () => { + const dashboard = await this.getExportableDashboardJson(); + const { isSharingExternally, isViewingYAML, exportMode } = this.state; + + DashboardInteractions.exportCopyJsonClicked({ + externally: isSharingExternally, + dashboard_schema_version: dashboard.initialSaveModelVersion, + has_library_panels: Boolean(dashboard.hasLibraryPanels), + export_mode: exportMode || 'classic', + format: isViewingYAML ? 'yaml' : 'json', + action: 'copy', }); }; } @@ -428,6 +455,7 @@ function ShareExportTabRenderer({ model }: SceneComponentProps<ShareExportTab>) icon="copy" disabled={dashboardJson.loading} getText={() => stringifiedDashboard ?? ''} + onClipboardCopy={model.onClipboardCopy} > <Trans i18nKey="share-modal.view-json.copy-button">Copy to Clipboard</Trans> </ClipboardButton> diff --git a/public/app/features/dashboard-scene/sharing/ShareInternallyConfiguration.tsx b/public/app/features/dashboard-scene/sharing/ShareInternallyConfiguration.tsx index 0c32b943cda..3fcf705d9a9 100644 --- a/public/app/features/dashboard-scene/sharing/ShareInternallyConfiguration.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareInternallyConfiguration.tsx @@ -42,6 +42,7 @@ export default function ShareInternallyConfiguration({ 'link.share.time-range-description', 'Change the current relative time range to an absolute time range' )} + id="time-range-description" > <Trans i18nKey="link.share.time-range-label">Lock time range</Trans> </Label> diff --git a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx index 6b6166b3eb9..91c4a9baa03 100644 --- a/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareLinkTab.tsx @@ -214,7 +214,7 @@ function ShareLinkTabRenderer({ model }: SceneComponentProps<ShareLinkTab>) { bottomSpacing={0} > <Trans i18nKey="share-modal.link.render-instructions"> - To render a panel image, you must install the{' '} + To render an image, you must install the{' '} <a href="https://grafana.com/grafana/plugins/grafana-image-renderer" target="_blank" diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index db5f89a32fb..f389382cbaf 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -6,7 +6,7 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState, import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { isPublicDashboardsEnabled } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { getTrackingSource } from '../../dashboard/components/ShareModal/utils'; import { DashboardInteractions } from '../utils/interactions'; diff --git a/public/app/features/dashboard-scene/sharing/components/ImagePreview.test.tsx b/public/app/features/dashboard-scene/sharing/components/ImagePreview.test.tsx new file mode 100644 index 00000000000..8ec9414626b --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/components/ImagePreview.test.tsx @@ -0,0 +1,107 @@ +import { render, screen, cleanup } from '@testing-library/react'; + +import { ImagePreview } from './ImagePreview'; + +// Mock URL.createObjectURL and URL.revokeObjectURL +const mockCreateObjectURL = jest.fn(); +const mockRevokeObjectURL = jest.fn(); + +beforeAll(() => { + global.URL.createObjectURL = mockCreateObjectURL; + global.URL.revokeObjectURL = mockRevokeObjectURL; +}); + +afterEach(() => { + mockCreateObjectURL.mockClear(); + mockRevokeObjectURL.mockClear(); + cleanup(); +}); + +describe('ImagePreview', () => { + const defaultProps = { + imageBlob: null, + isLoading: false, + error: null, + }; + + it('should render empty container when no image, loading, or error', () => { + render(<ImagePreview {...defaultProps} />); + // Container should exist with proper role and label + expect(screen.getByRole('region', { name: 'Image preview' })).toBeInTheDocument(); + // Loading bar should not be visible + expect(screen.queryByLabelText('Loading bar')).not.toBeInTheDocument(); + // Image should not be visible + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + // Error alert should not be visible + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); + + it('should show loading state with title when loading', () => { + render(<ImagePreview {...defaultProps} isLoading={true} title="Test Title" />); + // Loading state should be announced properly + expect(screen.getByRole('status', { name: 'Generating image...' })).toBeInTheDocument(); + expect(screen.getByLabelText('Loading bar')).toBeInTheDocument(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + }); + + it('should show error state when error is present', () => { + const error = { + title: 'Error Title', + message: 'Error Message', + }; + render(<ImagePreview {...defaultProps} error={error} />); + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText('Error Title')).toBeInTheDocument(); + expect(screen.getByText('Error Message')).toBeInTheDocument(); + }); + + it('should show image when imageBlob is present', () => { + const imageBlob = new Blob(['test'], { type: 'image/png' }); + mockCreateObjectURL.mockReturnValue('mock-url'); + render(<ImagePreview {...defaultProps} imageBlob={imageBlob} />); + const image = screen.getByRole('img', { name: 'Generated image preview' }); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('alt', 'Preview'); + expect(image).toHaveAttribute('src', 'mock-url'); + expect(mockCreateObjectURL).toHaveBeenCalledWith(imageBlob); + }); + + it('should revoke object URL on unmount', () => { + const imageBlob = new Blob(['test'], { type: 'image/png' }); + mockCreateObjectURL.mockReturnValue('mock-url'); + const { unmount } = render(<ImagePreview {...defaultProps} imageBlob={imageBlob} />); + unmount(); + expect(mockRevokeObjectURL).toHaveBeenCalledWith('mock-url'); + }); + + it('should not show image when loading', () => { + const imageBlob = new Blob(['test'], { type: 'image/png' }); + render(<ImagePreview {...defaultProps} imageBlob={imageBlob} isLoading={true} />); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + // Should show loading state instead + expect(screen.getByRole('status', { name: 'Generating image...' })).toBeInTheDocument(); + }); + + it('should not show error when loading', () => { + const error = { + title: 'Error Title', + message: 'Error Message', + }; + render(<ImagePreview {...defaultProps} error={error} isLoading={true} />); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + // Should show loading state instead + expect(screen.getByRole('status', { name: 'Generating image...' })).toBeInTheDocument(); + }); + + it('should not show duplicate message when error title and message are the same', () => { + const error = { + title: 'Failed to generate image', + message: 'Failed to generate image', + }; + render(<ImagePreview {...defaultProps} error={error} />); + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText('Failed to generate image')).toBeInTheDocument(); + // Check that the message doesn't appear twice by counting occurrences + expect(screen.getAllByText('Failed to generate image')).toHaveLength(1); + }); +}); diff --git a/public/app/features/dashboard-scene/sharing/components/ImagePreview.tsx b/public/app/features/dashboard-scene/sharing/components/ImagePreview.tsx new file mode 100644 index 00000000000..0d54c68dff6 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/components/ImagePreview.tsx @@ -0,0 +1,115 @@ +import { css } from '@emotion/css'; +import { useMemo, useEffect } from 'react'; +import { useMeasure } from 'react-use'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { Alert, LoadingBar, Text, useStyles2 } from '@grafana/ui'; + +type ErrorState = { + message: string; + title: string; +} | null; + +interface ImagePreviewProps { + imageBlob: Blob | null; + isLoading: boolean; + error: ErrorState; + title?: string; +} + +export function ImagePreview({ imageBlob, isLoading, error, title }: ImagePreviewProps) { + const styles = useStyles2(getStyles); + const [ref, { width: measuredWidth }] = useMeasure<HTMLDivElement>(); + + // Memoize and clean up the object URL for the image + const imageUrl = useMemo(() => { + if (!imageBlob) { + return undefined; + } + const url = URL.createObjectURL(imageBlob); + return url; + }, [imageBlob]); + + useEffect(() => { + return () => { + if (imageUrl) { + URL.revokeObjectURL(imageUrl); + } + }; + }, [imageUrl]); + + return ( + <div + className={styles.previewContainer} + ref={ref} + aria-label={t('share-modal.image.preview-region', 'Image preview')} + role="region" + > + {isLoading && ( + <div + className={styles.loadingBarContainer} + role="status" + aria-label={t('share-modal.image.generating', 'Generating image...')} + > + <LoadingBar width={measuredWidth} /> + {title && ( + <div className={styles.titleContainer}> + <Text variant="body">{title}</Text> + </div> + )} + </div> + )} + + {error && !isLoading && <ErrorAlert error={error} />} + {!isLoading && imageUrl && ( + <img + src={imageUrl} + alt={t('share-modal.image.preview', 'Preview')} + className={styles.image} + aria-label={t('share-modal.image.preview-aria', 'Generated image preview')} + /> + )} + </div> + ); +} + +function ErrorAlert({ error }: { error: ErrorState }) { + if (!error) { + return null; + } + + // Only show message if it's different from the title to avoid repetition + const showMessage = error.message && error.message !== error.title; + + return ( + <Alert severity="error" title={error.title}> + {showMessage && <div>{error.message}</div>} + </Alert> + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + previewContainer: css({ + position: 'relative', + width: '100%', + minHeight: '200px', + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + overflow: 'hidden', + }), + loadingBarContainer: css({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + }), + titleContainer: css({ + padding: theme.spacing(1), + }), + image: css({ + maxWidth: '100%', + maxHeight: '100%', + objectFit: 'contain', + }), +}); diff --git a/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.test.tsx b/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.test.tsx index d2cbfaa14f7..a02ed3e5a1d 100644 --- a/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.test.tsx +++ b/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.test.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import { getPanelPlugin } from '@grafana/data/test'; -import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, setPluginImportUtils } from '@grafana/runtime'; import { SceneTimeRange, VizPanel } from '@grafana/scenes'; @@ -17,38 +16,45 @@ setPluginImportUtils({ getPanelPluginFromCache: (id: string) => undefined, }); -const selector = e2eSelectors.pages.ShareDashboardDrawer.ShareInternally.SharePanel; - describe('SharePanelInternally', () => { it('should disable all image generation inputs when renderer is not available', async () => { config.rendererAvailable = false; buildAndRenderScenario(); - expect(await screen.findByTestId(selector.preview)).toBeInTheDocument(); - [ - selector.widthInput, - selector.heightInput, - selector.scaleFactorInput, - selector.generateImageButton, - selector.downloadImageButton, - ].forEach((selector) => { - expect(screen.getByTestId(selector)).toBeDisabled(); - }); + // Check that the panel preview is rendered + expect(await screen.findByText('Panel preview')).toBeInTheDocument(); + + // All inputs should be disabled - use placeholder text to find inputs + expect(screen.getByPlaceholderText('1000')).toBeDisabled(); // Width input + expect(screen.getByPlaceholderText('500')).toBeDisabled(); // Height input + expect(screen.getByPlaceholderText('1')).toBeDisabled(); // Scale factor input + expect(screen.getByRole('button', { name: /generate image/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /download image/i })).toBeDisabled(); }); it('should enable all image generation inputs when renderer is available', async () => { config.rendererAvailable = true; buildAndRenderScenario(); - expect(await screen.findByTestId(selector.preview)).toBeInTheDocument(); - [selector.widthInput, selector.heightInput, selector.scaleFactorInput].forEach((selector) => { - expect(screen.getByTestId(selector)).toBeEnabled(); - }); + // Check that the panel preview is rendered + expect(await screen.findByText('Panel preview')).toBeInTheDocument(); - await userEvent.type(screen.getByTestId(selector.widthInput), '1000'); - await userEvent.type(screen.getByTestId(selector.widthInput), '2000'); - expect(screen.getByTestId(selector.generateImageButton)).toBeEnabled(); - expect(screen.getByTestId(selector.downloadImageButton)).toBeDisabled(); + // Form inputs should be enabled + expect(screen.getByPlaceholderText('1000')).toBeEnabled(); // Width input + expect(screen.getByPlaceholderText('500')).toBeEnabled(); // Height input + expect(screen.getByPlaceholderText('1')).toBeEnabled(); // Scale factor input + + // Test form interaction + const widthInput = screen.getByPlaceholderText('1000'); + const heightInput = screen.getByPlaceholderText('500'); + + await userEvent.clear(widthInput); + await userEvent.type(widthInput, '1000'); + await userEvent.clear(heightInput); + await userEvent.type(heightInput, '2000'); + + expect(screen.getByRole('button', { name: /generate image/i })).toBeEnabled(); + expect(screen.getByRole('button', { name: /download image/i })).toBeDisabled(); }); }); diff --git a/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.tsx b/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.tsx index ebea6880e9a..668a04a9b32 100644 --- a/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.tsx +++ b/public/app/features/dashboard-scene/sharing/panel-share/SharePanelInternally.tsx @@ -78,7 +78,7 @@ function SharePanelInternallyRenderer({ model }: SceneComponentProps<SharePanelI bottomSpacing={0} > <Trans i18nKey="share-modal.link.render-instructions"> - To render a panel image, you must install the{' '} + To render an image, you must install the{' '} <a href="https://grafana.com/grafana/plugins/grafana-image-renderer" target="_blank" diff --git a/public/app/features/dashboard-scene/sharing/panel-share/SharePanelPreview.tsx b/public/app/features/dashboard-scene/sharing/panel-share/SharePanelPreview.tsx index 25797e29b29..57ea1bde7af 100644 --- a/public/app/features/dashboard-scene/sharing/panel-share/SharePanelPreview.tsx +++ b/public/app/features/dashboard-scene/sharing/panel-share/SharePanelPreview.tsx @@ -6,12 +6,12 @@ import { useAsyncFn } from 'react-use'; import { lastValueFrom } from 'rxjs'; import { GrafanaTheme2, UrlQueryMap } from '@grafana/data'; -import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { config, getBackendSrv, isFetchError } from '@grafana/runtime'; -import { Alert, Button, Field, FieldSet, Icon, Input, LoadingBar, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; +import { config, getBackendSrv } from '@grafana/runtime'; +import { Button, Field, FieldSet, Icon, Input, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; import { DashboardInteractions } from '../../utils/interactions'; +import { ImagePreview } from '../components/ImagePreview'; type ImageSettingsForm = { width: number; @@ -27,8 +27,6 @@ type Props = { theme: string; }; -const selector = e2eSelectors.pages.ShareDashboardDrawer.ShareInternally.SharePanel; - export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme }: Props) { const styles = useStyles2(getStyles); @@ -78,12 +76,15 @@ export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme } }; return ( - <div data-testid={selector.preview}> + <section aria-labelledby="panel-preview-heading"> <Stack gap={2} direction="column"> - <Text element="h4"> + <Text element="h4" id="panel-preview-heading"> <Trans i18nKey="share-panel-image.preview.title">Panel preview</Trans> </Text> - <form onSubmit={handleSubmit(onRenderImageClick)}> + <form + onSubmit={handleSubmit(onRenderImageClick)} + aria-label={t('share-panel-image.form.label', 'Panel image settings')} + > <FieldSet disabled={!config.rendererAvailable} label={ @@ -123,7 +124,7 @@ export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme } placeholder={t('share-panel-image.settings.width-placeholder', '1000')} type="number" suffix="px" - data-testid={selector.widthInput} + aria-label={t('share-panel-image.settings.width-label', 'Width')} /> </Field> <Field @@ -146,7 +147,7 @@ export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme } placeholder={t('share-panel-image.settings.height-placeholder', '500')} type="number" suffix="px" - data-testid={selector.heightInput} + aria-label={t('share-panel-image.settings.height-label', 'Height')} /> </Field> <Field @@ -171,7 +172,7 @@ export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme } })} placeholder={t('share-panel-image.settings.scale-factor-placeholder', '1')} type="number" - data-testid={selector.scaleFactorInput} + aria-label={t('share-panel-image.settings.scale-factor-label', 'Scale factor')} /> </Field> </Stack> @@ -182,7 +183,7 @@ export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme } fill="solid" type="submit" disabled={disabled || loading || !isValid} - data-testid={selector.generateImageButton} + aria-describedby={disabled ? 'generate-button-disabled-help' : undefined} > <Trans i18nKey="link.share-panel.render-image">Generate image</Trans> </Button> @@ -191,31 +192,38 @@ export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme } icon={'download-alt'} variant="secondary" disabled={!image || loading || disabled} - data-testid={selector.downloadImageButton} + aria-describedby={!image && !loading ? 'download-button-disabled-help' : undefined} > <Trans i18nKey="link.share-panel.download-image">Download image</Trans> </Button> </Stack> + {disabled && ( + <Text variant="bodySmall" color="secondary" id="generate-button-disabled-help"> + <Trans i18nKey="share-panel-image.disabled-help">Save the dashboard to enable image generation</Trans> + </Text> + )} + {!image && !loading && ( + <Text variant="bodySmall" color="secondary" id="download-button-disabled-help"> + <Trans i18nKey="share-panel-image.download-disabled-help"> + Generate an image first to enable download + </Trans> + </Text> + )} </FieldSet> </form> - {loading && ( - <div> - <LoadingBar width={128} /> - <div className={styles.imageLoadingContainer}> - <Text variant="body">{title || ''}</Text> - </div> - </div> - )} - {image && !loading && <img src={URL.createObjectURL(image)} alt="panel-preview-img" className={styles.image} />} - {error && !loading && ( - <Alert severity="error" title={t('link.share-panel.render-image-error', 'Failed to render panel image')}> - {isFetchError(error) - ? error.statusText - : t('link.share-panel.render-image-error-description', 'An error occurred when generating the image')} - </Alert> - )} + + <ImagePreview + imageBlob={image || null} + isLoading={loading} + error={ + error + ? { title: t('share-panel-image.error-title', 'Failed to generate image'), message: error.message } + : null + } + title={title} + /> </Stack> - </div> + </section> ); } @@ -223,14 +231,4 @@ const getStyles = (theme: GrafanaTheme2) => ({ imageConfigurationField: css({ flex: 1, }), - image: css({ - maxWidth: '100%', - width: 'max-content', - }), - imageLoadingContainer: css({ - maxWidth: '100%', - height: 362, - border: `1px solid ${theme.components.input.borderColor}`, - padding: theme.spacing(1), - }), }); diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx index 1c2ab358818..99f55253598 100644 --- a/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx @@ -8,7 +8,7 @@ import { contextSrv } from 'app/core/core'; import { useDeletePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { ConfigPublicDashboardBase } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard'; import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; import { getDashboardSceneFor } from '../../utils/utils'; @@ -47,7 +47,10 @@ export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }: isOpen: true, title: t('dashboard-scene.config-public-dashboard.title.revoke-public-url', 'Revoke public URL'), icon: 'trash-alt', - confirmText: 'Revoke public URL', + confirmText: t( + 'dashboard-scene.config-public-dashboard.confirmText.revoke-public-url', + 'Revoke public URL' + ), body: ( <p className={styles.description}> <Trans i18nKey="public-dashboard.config.revoke-body"> diff --git a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx index 6de468e798b..e38fd63e5d1 100644 --- a/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx +++ b/public/app/features/dashboard-scene/solo/SoloPanelPage.tsx @@ -11,7 +11,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardPageRouteParams } from 'app/features/dashboard/containers/types'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; import { DashboardScene } from '../scene/DashboardScene'; diff --git a/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts b/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts index 9c5badd54f7..fe93bb0a479 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSessionState.test.ts @@ -1,6 +1,6 @@ import { config, locationService } from '@grafana/runtime'; import { CustomVariable, UrlSyncManager } from '@grafana/scenes'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; diff --git a/public/app/features/dashboard-scene/utils/interactions.ts b/public/app/features/dashboard-scene/utils/interactions.ts index c7a0791e513..8713caa4112 100644 --- a/public/app/features/dashboard-scene/utils/interactions.ts +++ b/public/app/features/dashboard-scene/utils/interactions.ts @@ -124,6 +124,14 @@ export const DashboardInteractions = { showMoreVersionsClicked: () => { reportDashboardInteraction('show_more_versions_clicked'); }, + + // Image export interactions + generateDashboardImageClicked: (properties?: Record<string, unknown>) => { + reportDashboardInteraction('dashboard_image_generated', properties); + }, + downloadDashboardImageClicked: (properties?: Record<string, unknown>) => { + reportDashboardInteraction('dashboard_image_downloaded', properties); + }, }; const reportDashboardInteraction: typeof reportInteraction = (name, properties) => { diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts index 40a547f68e1..7576d80f18c 100644 --- a/public/app/features/dashboard-scene/utils/test-utils.ts +++ b/public/app/features/dashboard-scene/utils/test-utils.ts @@ -14,7 +14,7 @@ import { } from '@grafana/scenes'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; diff --git a/public/app/features/dashboard-scene/utils/utils.test.ts b/public/app/features/dashboard-scene/utils/utils.test.ts new file mode 100644 index 00000000000..a80b385ddf5 --- /dev/null +++ b/public/app/features/dashboard-scene/utils/utils.test.ts @@ -0,0 +1,372 @@ +import { Dashboard, Panel, RowPanel } from '@grafana/schema'; + +import { isValidLibraryPanelRef, hasLibraryPanelsInV1Dashboard } from './utils'; + +describe('utils', () => { + describe('isValidLibraryPanelRef', () => { + it('should return true for valid library panel reference', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + libraryPanel: { + uid: 'lib-panel-uid', + name: 'Library Panel Name', + }, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(true); + }); + + it('should return false for panel without libraryPanel property', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(false); + }); + + it('should return false for panel with libraryPanel but missing uid', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + // @ts-expect-error - Testing invalid library panel ref without uid + libraryPanel: { + name: 'Library Panel Name', + }, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(false); + }); + + it('should return false for panel with libraryPanel but missing name', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + // @ts-expect-error - Testing invalid library panel ref without name + libraryPanel: { + uid: 'lib-panel-uid', + }, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(false); + }); + + it('should return false for panel with libraryPanel but empty uid', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + libraryPanel: { + uid: '', + name: 'Library Panel Name', + }, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(false); + }); + + it('should return false for panel with libraryPanel but empty name', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + libraryPanel: { + uid: 'lib-panel-uid', + name: '', + }, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(false); + }); + + it('should return false for panel with null libraryPanel', () => { + const panel: Panel = { + id: 1, + title: 'Test Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + // @ts-expect-error - Testing invalid library panel ref + libraryPanel: null, + }; + + expect(isValidLibraryPanelRef(panel)).toBe(false); + }); + }); + + describe('hasLibraryPanelsInV1Dashboard', () => { + it('should return false for undefined dashboard', () => { + expect(hasLibraryPanelsInV1Dashboard(undefined)).toBe(false); + }); + + it('should return false for dashboard without panels', () => { + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(false); + }); + + it('should return false for dashboard with no library panels', () => { + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [ + { + id: 1, + title: 'Regular Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + }, + ], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(false); + }); + + it('should return true for dashboard with library panels', () => { + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [ + { + id: 1, + title: 'Library Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + libraryPanel: { + uid: 'lib-panel-uid', + name: 'Library Panel Name', + }, + }, + ], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(true); + }); + + it('should return true for dashboard with library panels in collapsed row', () => { + const collapsedRowPanel: RowPanel = { + id: 1, + title: 'Row Panel', + type: 'row', + gridPos: { x: 0, y: 0, w: 24, h: 1 }, + collapsed: true, + panels: [ + { + id: 2, + title: 'Library Panel in Row', + type: 'graph', + gridPos: { x: 0, y: 1, w: 12, h: 8 }, + libraryPanel: { + uid: 'lib-panel-uid', + name: 'Library Panel Name', + }, + }, + ], + }; + + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [collapsedRowPanel], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(true); + }); + + it('should return false for dashboard with collapsed row but no library panels', () => { + const collapsedRowPanel: RowPanel = { + id: 1, + title: 'Row Panel', + type: 'row', + gridPos: { x: 0, y: 0, w: 24, h: 1 }, + collapsed: true, + panels: [ + { + id: 2, + title: 'Regular Panel in Row', + type: 'graph', + gridPos: { x: 0, y: 1, w: 12, h: 8 }, + }, + ], + }; + + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [collapsedRowPanel], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(false); + }); + + it('should return false for dashboard with expanded row (not collapsed)', () => { + const expandedRowPanel: RowPanel = { + id: 1, + title: 'Row Panel', + type: 'row', + gridPos: { x: 0, y: 0, w: 24, h: 1 }, + collapsed: false, + panels: [ + { + id: 2, + title: 'Library Panel in Row', + type: 'graph', + gridPos: { x: 0, y: 1, w: 12, h: 8 }, + libraryPanel: { + uid: 'lib-panel-uid', + name: 'Library Panel Name', + }, + }, + ], + }; + + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [expandedRowPanel], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(false); + }); + + it('should return false for dashboard with row panel without panels property', () => { + //@ts-expect-error - Testing invalid row panel + const rowPanel: RowPanel = { + id: 1, + title: 'Row Panel', + type: 'row', + gridPos: { x: 0, y: 0, w: 24, h: 1 }, + collapsed: true, + }; + + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [rowPanel], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(false); + }); + + it('should return true for dashboard with mixed panels and library panels', () => { + const dashboard: Dashboard = { + id: 1, + title: 'Test Dashboard', + tags: [], + timezone: 'browser', + panels: [ + { + id: 1, + title: 'Regular Panel', + type: 'graph', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + }, + { + id: 2, + title: 'Library Panel', + type: 'graph', + gridPos: { x: 12, y: 0, w: 12, h: 8 }, + libraryPanel: { + uid: 'lib-panel-uid', + name: 'Library Panel Name', + }, + }, + ], + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + templating: { list: [] }, + annotations: { list: [] }, + refresh: '', + schemaVersion: 30, + version: 1, + links: [], + }; + + expect(hasLibraryPanelsInV1Dashboard(dashboard)).toBe(true); + }); + }); +}); diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 84880a788b7..7a5c41a08cd 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -13,6 +13,7 @@ import { VizPanel, VizPanelMenu, } from '@grafana/scenes'; +import { Dashboard, Panel, RowPanel } from '@grafana/schema'; import { createLogger } from '@grafana/ui'; import { initialIntervalVariableModelState } from 'app/features/variables/interval/reducer'; @@ -32,6 +33,11 @@ import { containsCloneKey, getLastKeyFromClone, getOriginalKey, isInCloneChain } export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; +const V1_PANEL_PROPERTIES = { + LIBRARY_PANEL: 'libraryPanel', + COLLAPSED: 'collapsed', +} as const; + export function getVizPanelKeyForPanelId(panelId: number) { return `panel-${panelId}`; } @@ -470,4 +476,42 @@ export function getLayoutOrchestratorFor(scene: SceneObject): DashboardLayoutOrc return getDashboardSceneFor(scene).state.layoutOrchestrator; } +// @returns true if the panel is a valid library panel reference +// a valid library panel reference is a panel with this +// property: `libraryPanel: {name: string, uid: string}` + +export function isValidLibraryPanelRef(panel: Panel): boolean { + return ( + (V1_PANEL_PROPERTIES.LIBRARY_PANEL in panel && + panel.libraryPanel && + Boolean(panel.libraryPanel?.uid && panel.libraryPanel?.name)) || + false + ); +} + +/** + * Checks if a V1 dashboard contains library panels + * @returns true if the dashboard contains library panels + */ +export function hasLibraryPanelsInV1Dashboard(dashboard: Dashboard | undefined): boolean { + if (!dashboard?.panels) { + return false; + } + + return dashboard.panels.some((panel: Panel | RowPanel) => { + if (isValidLibraryPanelRef(panel)) { + return true; + } + // Check if this is a collapsed row containing library panels + const isCollapsedRow = + V1_PANEL_PROPERTIES.COLLAPSED in panel && panel.collapsed && 'panels' in panel && panel.panels; + + if (!isCollapsedRow) { + return false; + } + + return panel.panels.some(isValidLibraryPanelRef); + }); +} + export const dashboardLog = createLogger('Dashboard'); diff --git a/public/app/features/dashboard-scene/utils/variables.ts b/public/app/features/dashboard-scene/utils/variables.ts index 04d2f16e901..6b6eeae5e77 100644 --- a/public/app/features/dashboard-scene/utils/variables.ts +++ b/public/app/features/dashboard-scene/utils/variables.ts @@ -190,6 +190,11 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode hide: variable.hide, definition: variable.definition, allowCustomValue: variable.allowCustomValue, + staticOptions: variable.staticOptions?.map((option) => ({ + label: String(option.text), + value: String(option.value), + })), + staticOptionsOrder: variable.staticOptionsOrder, }); } else if (variable.type === 'datasource') { return new DataSourceVariable({ diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx index 0d7ccaea603..c5cd4e4d15f 100644 --- a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx @@ -10,7 +10,7 @@ import { Form } from 'app/core/components/Form/Form'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { clearLoadedDashboard } from 'app/features/manage-dashboards/state/actions'; -import { useDispatch, useSelector, StoreState } from 'app/types'; +import { useDispatch, useSelector, StoreState } from 'app/types/store'; import { ImportDashboardFormV2 } from './ImportDashboardFormV2'; diff --git a/public/app/features/dashboard/api/ResponseTransformers.test.ts b/public/app/features/dashboard/api/ResponseTransformers.test.ts index eda142c38e5..9187b6fc9c8 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.test.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.test.ts @@ -24,7 +24,7 @@ import { transformVariableHideToEnum, transformVariableRefreshToEnum, } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { getDefaultDatasource, diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts index c2b56fe51cd..509271f7e50 100644 --- a/public/app/features/dashboard/api/ResponseTransformers.ts +++ b/public/app/features/dashboard/api/ResponseTransformers.ts @@ -73,7 +73,7 @@ import { transformVariableHideToEnum, transformVariableRefreshToEnum, } from 'app/features/dashboard-scene/serialization/transformToV2TypesUtils'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from './types'; import { isDashboardResource, isDashboardV0Spec, isDashboardV2Resource } from './utils'; diff --git a/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts b/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts index 6dc00b3190f..8b9e3cd2f47 100644 --- a/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts +++ b/public/app/features/dashboard/api/UnifiedDashboardAPI.test.ts @@ -4,7 +4,7 @@ import { defaultSpec as defaultDashboardV2Spec, } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { ResourceList } from 'app/features/apiserver/types'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/UnifiedDashboardAPI.ts b/public/app/features/dashboard/api/UnifiedDashboardAPI.ts index 344f3ba9cd2..6851a6c0f76 100644 --- a/public/app/features/dashboard/api/UnifiedDashboardAPI.ts +++ b/public/app/features/dashboard/api/UnifiedDashboardAPI.ts @@ -2,7 +2,7 @@ import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { isResource } from 'app/features/apiserver/guards'; import { Resource, ResourceList } from 'app/features/apiserver/types'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index fb930516a0e..c4a73deccb7 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -1,6 +1,6 @@ import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { UnifiedDashboardAPI } from './UnifiedDashboardAPI'; import { LegacyDashboardAPI } from './legacy'; diff --git a/public/app/features/dashboard/api/legacy.test.ts b/public/app/features/dashboard/api/legacy.test.ts index b62450fd0ea..ce81f685afb 100644 --- a/public/app/features/dashboard/api/legacy.test.ts +++ b/public/app/features/dashboard/api/legacy.test.ts @@ -1,5 +1,5 @@ import { UrlQueryMap } from '@grafana/data'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { LegacyDashboardAPI } from './legacy'; diff --git a/public/app/features/dashboard/api/legacy.ts b/public/app/features/dashboard/api/legacy.ts index 5ce46cdf442..5a9e0b9707c 100644 --- a/public/app/features/dashboard/api/legacy.ts +++ b/public/app/features/dashboard/api/legacy.ts @@ -6,7 +6,7 @@ import appEvents from 'app/core/app_events'; import { Resource, ResourceList } from 'app/features/apiserver/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { SaveDashboardResponseDTO, DashboardDTO } from 'app/types'; +import { SaveDashboardResponseDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/types.ts b/public/app/features/dashboard/api/types.ts index 568fcfcf4a5..90c16d527dc 100644 --- a/public/app/features/dashboard/api/types.ts +++ b/public/app/features/dashboard/api/types.ts @@ -2,7 +2,7 @@ import { UrlQueryMap } from '@grafana/data'; import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; import { ListOptions, Resource, ResourceList } from 'app/features/apiserver/types'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { AnnotationsPermissions, SaveDashboardResponseDTO } from 'app/types'; +import { AnnotationsPermissions, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/api/utils.ts b/public/app/features/dashboard/api/utils.ts index f49fcb9de12..d027960bce7 100644 --- a/public/app/features/dashboard/api/utils.ts +++ b/public/app/features/dashboard/api/utils.ts @@ -3,7 +3,7 @@ import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Status } from '@grafana/schema/src/schema/dashboard/v2alpha1/types.status.gen'; import { Resource } from 'app/features/apiserver/types'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; @@ -74,6 +74,10 @@ export function isV1DashboardCommand( return !isDashboardV2Spec(cmd.dashboard); } +export function isV1ClassicDashboard(obj: Dashboard | DashboardV2Spec): obj is Dashboard { + return !isDashboardV2Spec(obj); +} + export function isV2DashboardCommand( cmd: SaveDashboardCommand<Dashboard | DashboardV2Spec> ): cmd is SaveDashboardCommand<DashboardV2Spec> { diff --git a/public/app/features/dashboard/api/v1.test.ts b/public/app/features/dashboard/api/v1.test.ts index a037136eeb9..7cdadd0995a 100644 --- a/public/app/features/dashboard/api/v1.test.ts +++ b/public/app/features/dashboard/api/v1.test.ts @@ -1,7 +1,7 @@ import { GrafanaConfig, locationUtil } from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; import { AnnoKeyFolder } from 'app/features/apiserver/types'; -import { DashboardDataDTO } from 'app/types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { DashboardWithAccessInfo } from './types'; import { K8sDashboardAPI } from './v1'; diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index 58edcc56d70..f196e9d71e2 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -21,7 +21,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; @@ -128,6 +128,7 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> { uid: dash.metadata.name, k8s: dash.metadata, version: dash.metadata.generation, + created: dash.metadata.creationTimestamp, }, dashboard: { ...dash.spec, diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 0a0830e4ffa..676f7b26e27 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -19,7 +19,7 @@ import { } from 'app/features/apiserver/types'; import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; -import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types'; +import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardCommand } from '../components/SaveDashboard/types'; diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx index 8b634dab7f0..75cedae0807 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.test.tsx @@ -13,8 +13,8 @@ import { import AddPanelMenu from './AddPanelMenu'; -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), useSelector: () => jest.fn(), })); diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx index 560f924a8a3..138f2bf285d 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelMenu.tsx @@ -13,7 +13,7 @@ import { onPasteCopiedPanel, } from 'app/features/dashboard/utils/dashboard'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { setInitialDatasource } from '../../state/reducers'; diff --git a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx index 0b3e783b73c..22c27294edb 100644 --- a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx +++ b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx @@ -241,7 +241,7 @@ export const AnnotationSettingsEdit = ({ editIdx, dashboard }: Props) => { > <> <Select - options={panelFilters} + options={getPanelFilters()} value={panelFilter} onChange={onFilterTypeChange} data-testid={selectors.components.Annotations.annotationsTypeInput} @@ -319,20 +319,29 @@ enum PanelFilterType { ExcludePanels, } -const panelFilters = [ +const getPanelFilters = () => [ { - label: 'All panels', + label: t('dashboard.get-panel-filters.label.all-panels', 'All panels'), value: PanelFilterType.AllPanels, - description: 'Send the annotation data to all panels that support annotations', + description: t( + 'dashboard.get-panel-filters.description.annotation-panels-support-annotations', + 'Send the annotation data to all panels that support annotations' + ), }, { - label: 'Selected panels', + label: t('dashboard.get-panel-filters.label.selected-panels', 'Selected panels'), value: PanelFilterType.IncludePanels, - description: 'Send the annotations to the explicitly listed panels', + description: t( + 'dashboard.get-panel-filters.description.annotations-explicitly-listed-panels', + 'Send the annotations to the explicitly listed panels' + ), }, { - label: 'All panels except', + label: t('dashboard.get-panel-filters.label.all-panels-except', 'All panels except'), value: PanelFilterType.ExcludePanels, - description: 'Do not send annotation data to the following panels', + description: t( + 'dashboard.get-panel-filters.description.annotation-following-panels', + 'Do not send annotation data to the following panels' + ), }, ]; diff --git a/public/app/features/dashboard/components/AnnotationSettings/index.tsx b/public/app/features/dashboard/components/AnnotationSettings/index.tsx deleted file mode 100644 index 5c5ebb6cb18..00000000000 --- a/public/app/features/dashboard/components/AnnotationSettings/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { AnnotationSettingsEdit, newAnnotationName } from './AnnotationSettingsEdit'; -export { AnnotationSettingsList } from './AnnotationSettingsList'; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index c7aef4cb7ac..78dd8c02a40 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -4,7 +4,6 @@ import { connect, ConnectedProps } from 'react-redux'; import { useLocation } from 'react-router-dom-v5-compat'; import { textUtil } from '@grafana/data'; -import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { @@ -14,7 +13,6 @@ import { useForceUpdate, ToolbarButtonRow, ConfirmModal, - Badge, } from '@grafana/ui'; import { updateNavIndex } from 'app/core/actions'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; @@ -29,11 +27,13 @@ import AddPanelButton from 'app/features/dashboard/components/AddPanelButton/Add import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +import { PublicDashboardBadgeLegacy } from 'app/features/dashboard-scene/scene/new-toolbar/actions/PublicDashboardBadge'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; -import { KioskMode, StoreState } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { DashboardMetaChangedEvent, ShowModalReactEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import { DynamicDashNavButtonModel, @@ -58,8 +58,6 @@ const mapStateToProps = (state: StoreState) => ({ const connector = connect(mapStateToProps, mapDispatchToProps); -const selectors = e2eSelectors.pages.Dashboard.DashNav; - export interface OwnProps { dashboard: DashboardModel; isFullscreen: boolean; @@ -117,7 +115,7 @@ export const DashNav = memo<Props>((props) => { </> ), confirmVariant: 'primary', - confirmText: 'Proceed', + confirmText: t('dashboard.dash-nav.on-open-snapshot-original.confirmText.proceed', 'Proceed'), onConfirm: gotoSnapshotOrigin, }, }) @@ -215,17 +213,8 @@ export const DashNav = memo<Props>((props) => { ); } - if (dashboard.meta.publicDashboardEnabled) { - // TODO: This will be replaced with the new badge component. Color is required but gets override by css - buttons.push( - <Badge - color="blue" - text={t('dashboard.dash-nav.render-left-actions.text-public', 'Public')} - key="public-dashboard-button-badge" - className={publicBadgeStyle} - data-testid={selectors.publicDashboardTag} - /> - ); + if (dashboard.uid) { + buttons.push(<PublicDashboardBadgeLegacy key="public-dashboard-badge" uid={dashboard.uid} />); } if (isDevEnv && config.featureToggles.dashboardScene) { @@ -377,9 +366,3 @@ const modalStyles = css({ width: 'max-content', maxWidth: '80vw', }); - -const publicBadgeStyle = css({ - color: 'grey', - backgroundColor: 'transparent', - border: '1px solid', -}); diff --git a/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx b/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx index 9601c1a026f..69fd629ccd6 100644 --- a/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx +++ b/public/app/features/dashboard/components/DashboardLoading/DashboardFailed.tsx @@ -2,7 +2,8 @@ import { css } from '@emotion/css'; import { Alert } from '@grafana/ui'; import { getMessageFromError } from 'app/core/utils/errors'; -import { DashboardInitError, AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; +import { DashboardInitError } from 'app/types/dashboard'; export interface Props { initError?: DashboardInitError; diff --git a/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx b/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx index 5dcb8d870f5..f5e279e5af1 100644 --- a/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx +++ b/public/app/features/dashboard/components/DashboardLoading/DashboardLoading.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Button, HorizontalGroup, Spinner, useStyles2, VerticalGroup } from '@grafana/ui'; -import { DashboardInitPhase } from 'app/types'; +import { DashboardInitPhase } from 'app/types/dashboard'; export interface Props { initPhase: DashboardInitPhase; diff --git a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx index 6b31090b447..479521c91bd 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx @@ -1,7 +1,7 @@ import { Permissions } from 'app/core/components/AccessControl'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { SettingsPageProps } from '../DashboardSettings/types'; diff --git a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx index 5514f11da67..96c63448806 100644 --- a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx +++ b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx @@ -10,8 +10,8 @@ import { Prompt } from 'app/core/components/FormPrompt/Prompt'; import { contextSrv } from 'app/core/services/context_srv'; import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal'; import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types'; -import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { DashboardModel } from '../../state/DashboardModel'; import { discardPanelChanges, exitPanelEditor } from '../PanelEditor/state/actions'; diff --git a/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx index ffcf53a4085..add625aa42c 100644 --- a/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx @@ -3,7 +3,8 @@ import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; import { DashboardModel } from '../../state/DashboardModel'; -import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from '../AnnotationSettings'; +import { AnnotationSettingsEdit, newAnnotationName } from '../AnnotationSettings/AnnotationSettingsEdit'; +import { AnnotationSettingsList } from '../AnnotationSettings/AnnotationSettingsList'; import { SettingsPageProps } from './types'; diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index 063eccf4ebc..26d52f52a63 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -10,7 +10,7 @@ import { Button, Stack, Text, ToolbarButtonRow } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { DashboardMetaChangedEvent } from 'app/types/events'; import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer'; diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx index 317b5734635..d7b32cfabb2 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx @@ -29,12 +29,6 @@ import { SettingsPageProps } from './types'; export type Props = SettingsPageProps & ConnectedProps<typeof connector>; -const GRAPH_TOOLTIP_OPTIONS = [ - { value: 0, label: 'Default' }, - { value: 1, label: 'Shared crosshair' }, - { value: 2, label: 'Shared Tooltip' }, -]; - export function GeneralSettingsUnconnected({ dashboard, updateTimeZone, @@ -44,6 +38,20 @@ export function GeneralSettingsUnconnected({ const [renderCounter, setRenderCounter] = useState(0); const [dashboardTitle, setDashboardTitle] = useState(dashboard.title); const [dashboardDescription, setDashboardDescription] = useState(dashboard.description); + const GRAPH_TOOLTIP_OPTIONS = [ + { value: 0, label: t('dashboard.general-settings-unconnected.graph_tooltip_options.label.default', 'Default') }, + { + value: 1, + label: t( + 'dashboard.general-settings-unconnected.graph_tooltip_options.label.shared-crosshair', + 'Shared crosshair' + ), + }, + { + value: 2, + label: t('dashboard.general-settings-unconnected.graph_tooltip_options.label.shared-tooltip', 'Shared tooltip'), + }, + ]; const pageNav = sectionNav.node.parentItem; diff --git a/public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx index 10f07a092d8..374c486497c 100644 --- a/public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx @@ -4,7 +4,8 @@ import { locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; import { NEW_LINK } from 'app/features/dashboard-scene/settings/links/utils'; -import { LinkSettingsEdit, LinkSettingsList } from '../LinksSettings'; +import { LinkSettingsEdit } from '../LinksSettings/LinkSettingsEdit'; +import { LinkSettingsList } from '../LinksSettings/LinkSettingsList'; import { SettingsPageProps } from './types'; diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx index 31ee8c9ba2e..fad6f79bffc 100644 --- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx @@ -4,12 +4,9 @@ import * as React from 'react'; import { config } from '@grafana/runtime'; import { Spinner, HorizontalGroup } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { - historySrv, - RevisionsModel, - VersionHistoryHeader, - VersionsHistoryButtons, -} from 'app/features/dashboard-scene/settings/version-history'; +import { historySrv, RevisionsModel } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; +import { VersionsHistoryButtons } from 'app/features/dashboard-scene/settings/version-history/VersionHistoryButtons'; +import { VersionHistoryHeader } from 'app/features/dashboard-scene/settings/version-history/VersionHistoryHeader'; import { VersionHistoryComparison } from '../VersionHistory/VersionHistoryComparison'; import { VersionHistoryTable } from '../VersionHistory/VersionHistoryTable'; diff --git a/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx b/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx index b7eefcba85a..ff10cbf0367 100644 --- a/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx +++ b/public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx @@ -23,7 +23,7 @@ import { } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowMessage, SnapshotTab, SupportSnapshotService } from './SupportSnapshotService'; diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index cbd6b6c4e3f..a60e02d2ae0 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -8,7 +8,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { InspectTab } from 'app/features/inspector/types'; import { getPanelStateForModel } from 'app/features/panel/state/selectors'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { GetDataOptions } from '../../../query/state/PanelQueryRunner'; import { HelpWizard } from '../HelpWizard/HelpWizard'; diff --git a/public/app/features/dashboard/components/LinksSettings/index.tsx b/public/app/features/dashboard/components/LinksSettings/index.tsx deleted file mode 100644 index 21e45f1e3ba..00000000000 --- a/public/app/features/dashboard/components/LinksSettings/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { LinkSettingsEdit } from './LinkSettingsEdit'; -export { LinkSettingsList } from './LinkSettingsList'; diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx index d44d699c978..cacdb672a11 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { OptionsPaneOptions } from './OptionsPaneOptions'; import { VisualizationButton } from './VisualizationButton'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 81e184b7e59..76c38e695d9 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -30,8 +30,8 @@ import { SaveLibraryPanelModal } from 'app/features/library-panels/components/Sa import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types'; import { getPanelStateForModel } from 'app/features/panel/state/selectors'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; -import { StoreState } from 'app/types'; import { PanelOptionsChangedEvent, ShowModalReactEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import { notifyApp } from '../../../../core/actions'; import { UnlinkModal } from '../../../dashboard-scene/scene/UnlinkModal'; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index 020eea8d8e8..b24344519db 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -5,7 +5,7 @@ import { locationService } from '@grafana/runtime'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { QueryGroup } from 'app/features/query/components/QueryGroup'; -import { QueryGroupDataSource, QueryGroupOptions } from 'app/types'; +import { QueryGroupDataSource, QueryGroupOptions } from 'app/types/query'; import { getDashboardSrv } from '../../services/DashboardSrv'; import { PanelModel } from '../../state/PanelModel'; diff --git a/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx b/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx index d98b0ae99c2..51a84846f1d 100644 --- a/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx +++ b/public/app/features/dashboard/components/PanelEditor/VisualizationButton.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { ToolbarButton, ButtonGroup } from '@grafana/ui'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { PanelModel } from '../../state/PanelModel'; import { getPanelPluginWithFallback } from '../../state/selectors'; diff --git a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx index 0edd8c13fe0..ffa32a42254 100644 --- a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx @@ -10,7 +10,7 @@ import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; import { PanelLibraryOptionsGroup } from 'app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup'; import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { VizTypePicker } from '../../../panel/components/VizTypePicker/VizTypePicker'; import { changePanelPlugin } from '../../../panel/state/actions'; diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index 00a96421aea..f1313156cb0 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -4,7 +4,7 @@ import store from 'app/core/store'; import { removePanel } from 'app/features/dashboard/utils/panel'; import { cleanUpPanelState } from 'app/features/panel/state/actions'; import { panelModelAndPluginReady } from 'app/features/panel/state/reducers'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { DashboardModel } from '../../../state/DashboardModel'; import { PanelModel } from '../../../state/PanelModel'; diff --git a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx index cafa4578540..de68a822e3a 100644 --- a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx +++ b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx @@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { SceneObject, sceneGraph } from '@grafana/scenes'; import { Combobox, ComboboxOption, Select } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors'; diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx index 72df57aeb96..ddf13763d79 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardErrorProxy.tsx @@ -49,7 +49,7 @@ export const SaveDashboardErrorProxy = ({ </Trans> </div> } - confirmText="Save and overwrite" + confirmText={t('dashboard.save-dashboard-error-proxy.confirmText-save-and-overwrite', 'Save and overwrite')} onConfirm={async () => { await onDashboardSave(dashboardSaveModel, { overwrite: true }, dashboard); onDismiss(); @@ -90,7 +90,10 @@ export const SaveDashboardErrorProxy = ({ </Trans> </div> } - confirmText="Save and overwrite" + confirmText={t( + 'dashboard.save-dashboard-error-proxy.confirmText-save-and-overwrite', + 'Save and overwrite' + )} onConfirm={async () => { await onDashboardSave(dashboardSaveModel, { overwrite: true }, dashboard); onDismiss(); diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx index 264a2640ebf..de5b14e257c 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardAsForm.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from 'test/test-utils'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import * as api from 'app/features/manage-dashboards/state/actions'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardAsForm, SaveDashboardAsFormProps } from './SaveDashboardAsForm'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx index 3c2ec091317..0f50238b88b 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { Dashboard } from '@grafana/schema'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { SaveDashboardOptions } from '../types'; diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx index 510222479bd..18f0c61f02e 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx @@ -8,7 +8,7 @@ import { config } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Button, Checkbox, TextArea, useStyles2, Stack } from '@grafana/ui'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; import { GenAIDashboardChangesButton } from '../../GenAI/GenAIDashboardChangesButton'; import { SaveDashboardData, SaveDashboardOptions } from '../types'; diff --git a/public/app/features/dashboard/components/SaveDashboard/types.ts b/public/app/features/dashboard/components/SaveDashboard/types.ts index 7e8eccc605f..05c6356f61f 100644 --- a/public/app/features/dashboard/components/SaveDashboard/types.ts +++ b/public/app/features/dashboard/components/SaveDashboard/types.ts @@ -2,7 +2,7 @@ import { Dashboard } from '@grafana/schema'; import { ObjectMeta } from 'app/features/apiserver/types'; import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; -import { SaveDashboardResponseDTO } from 'app/types'; +import { SaveDashboardResponseDTO } from 'app/types/dashboard'; export interface SaveDashboardData { clone: Dashboard; // cloned copy diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index bf03c12b87b..33b036e05a8 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash'; import { useAsyncFn } from 'react-use'; import { locationUtil } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import appEvents from 'app/core/app_events'; @@ -9,8 +10,8 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; -import { useDispatch } from 'app/types'; import { DashboardSavedEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { updateDashboardUidLastUsedDatasource } from '../../utils/dashboard'; @@ -54,7 +55,7 @@ export const useDashboardSave = (isCopy = false) => { // important that these happen before location redirect below appEvents.publish(new DashboardSavedEvent()); - notifyApp.success('Dashboard saved'); + notifyApp.success(t('dashboard.save-dashboard.message-dashboard-saved', 'Dashboard saved')); //Update local storage dashboard to handle things like last used datasource updateDashboardUidLastUsedDatasource(result.uid); diff --git a/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx index 3dd19e82a19..aad5b7554e6 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareEmbed.test.tsx @@ -30,7 +30,8 @@ jest.mock('app/core/services/context_srv', () => ({ })); function mockLocationHref(href: string) { - const location = window.location; + const win: typeof globalThis = window; + const location = win.location; let search = ''; const searchPos = href.indexOf('?'); @@ -39,8 +40,8 @@ function mockLocationHref(href: string) { } // @ts-ignore - delete window.location; - window.location = { + delete win.location; + win.location = { ...location, href, origin: new URL(href).origin, diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx index 03647498a00..780e71bc494 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx @@ -32,9 +32,10 @@ function mockLocationHref(href: string) { search = href.substring(searchPos); } + const win: typeof globalThis = window; //@ts-ignore - delete window.location; - window.location = { + delete win.location; + win.location = { ...location, href, origin: new URL(href).origin, diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.tsx index b044e6b6b96..6c578299f66 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.tsx @@ -163,7 +163,7 @@ export class ShareLink extends PureComponent<Props, State> { bottomSpacing={0} > <Trans i18nKey="share-modal.link.render-instructions"> - To render a panel image, you must install the{' '} + To render an image, you must install the{' '} <TextLink href="https://grafana.com/grafana/plugins/grafana-image-renderer" external> Grafana image renderer plugin </TextLink> diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 73597b197f2..16d6220adcc 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -10,7 +10,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShareEmbed } from './ShareEmbed'; import { ShareExport } from './ShareExport'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index b08aebc934e..ae88d932140 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -26,9 +26,10 @@ import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { DeletePublicDashboardModal } from 'app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { contextSrv } from '../../../../../../core/services/context_srv'; -import { AccessControlAction, useSelector } from '../../../../../../types'; import { useIsDesktop } from '../../../../utils/screen'; import { ShareModal } from '../../ShareModal'; import { shareDashboardType } from '../../utils'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx index 930953f859e..c63628c8d17 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/EmailSharingConfiguration.tsx @@ -17,7 +17,7 @@ import { import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PublicDashboard, PublicDashboardShareType, validEmailRegex } from '../SharePublicDashboardUtils'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx index 3d4d098023d..7cbc1b6b9f6 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx @@ -10,7 +10,8 @@ import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/pub import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert'; import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx index 1df31f7b635..9636e2dd6ef 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx @@ -6,7 +6,7 @@ import { Spinner, useStyles2 } from '@grafana/ui'; import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi'; import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup'; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx index f1a2eb97686..993fc55ae8e 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/utilsTest.tsx @@ -3,8 +3,9 @@ import { http, HttpResponse } from 'msw'; import * as React from 'react'; import { Provider } from 'react-redux'; +import { DashboardInitPhase } from 'app/types/dashboard'; + import { configureStore } from '../../../../../store/configureStore'; -import { DashboardInitPhase } from '../../../../../types'; import { DashboardModel } from '../../../state/DashboardModel'; import { PanelModel } from '../../../state/PanelModel'; import { createDashboardModelFixture } from '../../../state/__fixtures__/dashboardFixtures'; diff --git a/public/app/features/dashboard/components/ShareModal/utils.ts b/public/app/features/dashboard/components/ShareModal/utils.ts index bcf6925da56..9693afc8e0f 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.ts @@ -181,4 +181,5 @@ export const shareDashboardType: { report: 'report', publicDashboard: 'public_dashboard', inviteUser: 'invite_user', + image: 'image', }; diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index fdc83c449d8..3ec288df413 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -7,8 +7,8 @@ import { AnnotationQuery, DataQuery, TypedVariableModel, GrafanaTheme2 } from '@ import { t } from '@grafana/i18n'; import { DashboardLink } from '@grafana/schema'; import { stylesFactory, Themeable2, withTheme2 } from '@grafana/ui'; +import { StoreState } from 'app/types/store'; -import { StoreState } from '../../../../types'; import { getSubMenuVariables, getVariablesState } from '../../../variables/state/selectors'; import { DashboardModel } from '../../state/DashboardModel'; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx index 70910408020..bebae5de3e2 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx @@ -138,7 +138,17 @@ export const TransformationOperationRow = ({ .subscribe(setOutput); const prevOutputSubscription = transformDataFrame(prevInputTransforms, data.series, ctx) .pipe(mergeMap((before) => transformDataFrame(prevOutputTransforms, before, ctx))) - .subscribe(setPrevOutput); + .subscribe((result) => { + let mergedResult = [...result]; + // add refIds that were requested even if they did not return a result + data.request?.targets.forEach((series) => { + const refIdInResult = mergedResult.some((frame) => frame.refId === series.refId); + if (!refIdInResult) { + mergedResult.push({ refId: series.refId, fields: [], length: 0 }); + } + }); + setPrevOutput(mergedResult); + }); return function unsubscribe() { inputSubscription.unsubscribe(); @@ -197,8 +207,11 @@ export const TransformationOperationRow = ({ title={t('dashboard.transformation-operation-row.title-delete', 'Delete {{name}}?', { name: uiConfig.name, })} - body="Note that removing one transformation may break others. If there is only a single transformation, you will go back to the main selection screen." - confirmText="Delete" + body={t( + 'dashboard.transformation-operation-row.body-delete', + 'Note that removing one transformation may break others. If there is only a single transformation, you will go back to the main selection screen.' + )} + confirmText={t('dashboard.transformation-operation-row.render-actions.confirmText-delete', 'Delete')} onConfirm={() => { setShowDeleteModal(false); onRemove(index); diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx index ea710a890e5..dbc34734758 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx @@ -3,7 +3,6 @@ import { FormEventHandler, KeyboardEventHandler, ReactNode, useCallback } from ' import { DataFrame, - DataTransformerID, TransformerRegistryItem, TransformationApplicabilityLevels, GrafanaTheme2, @@ -12,20 +11,13 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2 } from '@grafana/ui'; -import config from 'app/core/config'; +import { Badge, Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2, useTheme2 } from '@grafana/ui'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; -import { categoriesLabels } from 'app/features/transformers/utils'; +import { getCategoriesLabels } from 'app/features/transformers/utils'; import { FilterCategory } from './TransformationsEditor'; -const viewAllLabel = 'View all'; const VIEW_ALL_VALUE = 'viewAll'; -const filterCategoriesLabels: Array<[FilterCategory, string]> = [ - [VIEW_ALL_VALUE, viewAllLabel], - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ...(Object.entries(categoriesLabels) as Array<[FilterCategory, string]>), -]; interface TransformationPickerNgProps { onTransformationAdd: (selectedItem: SelectableValue<string>) => void; @@ -60,6 +52,12 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) { onSelectedFilterChange, } = props; + const filterCategoriesLabels: Array<[FilterCategory, string]> = [ + [VIEW_ALL_VALUE, t('dashboard.transformation-picker-ng.view-all', 'View all')], + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + ...(Object.entries(getCategoriesLabels()) as Array<[FilterCategory, string]>), + ]; + // Use a callback ref to call "click" on the search input // This will focus it when it's opened const searchInputRef = useCallback((input: HTMLInputElement) => { @@ -146,6 +144,7 @@ function getTransformationPickerStyles(theme: GrafanaTheme2) { columnGap: '27px', rowGap: '16px', width: '100%', + paddingBottom: theme.spacing(1), }), searchInput: css({ flexGrow: '1', @@ -172,6 +171,7 @@ interface TransformationsGridProps { } function TransformationsGrid({ showIllustrations, transformations, onClick, data }: TransformationsGridProps) { + const theme = useTheme2(); const styles = useStyles2(getTransformationGridStyles); return ( @@ -200,6 +200,8 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data cardClasses = cx(styles.newCard, styles.cardDisabled); } + const imageUrl = theme.isDark ? transform.imageDark : transform.imageLight; + return ( <Card className={cardClasses} @@ -208,16 +210,25 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data key={transform.id} > <Card.Heading className={styles.heading}> - <span>{transform.name}</span> - <span className={styles.pluginStateInfoWrapper}> - <PluginStateInfo state={transform.state} /> - </span> + <div className={styles.titleRow}> + <span>{transform.name}</span> + <span className={styles.pluginStateInfoWrapper}> + <PluginStateInfo state={transform.state} /> + </span> + </div> + {transform.tags && transform.tags.size > 0 && ( + <div className={styles.tagsWrapper}> + {Array.from(transform.tags).map((tag) => ( + <Badge color="darkgrey" icon="tag-alt" key={tag} text={tag} /> + ))} + </div> + )} </Card.Heading> <Card.Description className={styles.description}> - <span>{getTransformationsRedesignDescriptions(transform.id)}</span> + <span>{standardTransformersRegistry.getIfExists(transform.id)?.description}</span> {showIllustrations && ( <span> - <img className={styles.image} src={getImagePath(transform.id, !isApplicable)} alt={transform.name} /> + <img className={styles.image} src={imageUrl} alt={transform.name} /> </span> )} {!isApplicable && applicabilityDescription !== null && ( @@ -242,11 +253,18 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { '> button': { width: '100%', display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flexWrap: 'nowrap', + flexDirection: 'column', + alignItems: 'flex-start', + gap: theme.spacing(1), }, }), + titleRow: css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'nowrap', + width: '100%', + }), description: css({ fontSize: '12px', display: 'flex', @@ -255,19 +273,19 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { }), image: css({ display: 'block', - maxEidth: '100%`', - marginTop: `${theme.spacing(2)}`, + maxWidth: '100%', + marginTop: theme.spacing(2), }), grid: css({ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gridAutoRows: '1fr', - gap: `${theme.spacing(2)} ${theme.spacing(1)}`, + gap: theme.spacing(1), width: '100%', + padding: `${theme.spacing(1)} 0`, }), cardDisabled: css({ - backgroundColor: 'rgb(204, 204, 220, 0.045)', - color: `${theme.colors.text.disabled} !important`, + backgroundColor: theme.colors.action.disabledBackground, img: { filter: 'grayscale(100%)', opacity: 0.33, @@ -275,44 +293,20 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { }), cardApplicableInfo: css({ position: 'absolute', - bottom: `${theme.spacing(1)}`, - right: `${theme.spacing(1)}`, + bottom: theme.spacing(1), + right: theme.spacing(1), }), newCard: css({ gridTemplateRows: 'min-content 0 1fr 0', + marginBottom: 0, }), pluginStateInfoWrapper: css({ - marginLeft: '5px', + marginLeft: theme.spacing(0.5), + }), + tagsWrapper: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(0.5), }), }; } - -const getImagePath = (id: string, disabled: boolean) => { - const folder = config.theme2.isDark ? 'dark' : 'light'; - return `public/build/img/transformations/${folder}/${id}.svg`; -}; - -const TransformationDescriptionOverrides: { [key: string]: string } = { - [DataTransformerID.concatenate]: 'Combine all fields into a single frame.', - [DataTransformerID.configFromData]: 'Set unit, min, max and more.', - [DataTransformerID.fieldLookup]: 'Use a field value to lookup countries, states, or airports.', - [DataTransformerID.filterFieldsByName]: 'Remove parts of the query results using a regex pattern.', - [DataTransformerID.filterByRefId]: 'Remove rows from the data based on origin query', - [DataTransformerID.filterByValue]: 'Remove rows from the query results using user-defined filters.', - [DataTransformerID.groupBy]: 'Group data by a field value and create aggregate data.', - [DataTransformerID.groupingToMatrix]: 'Summarize and reorganize data based on three fields.', - [DataTransformerID.joinByField]: 'Combine rows from 2+ tables, based on a related field.', - [DataTransformerID.labelsToFields]: 'Group series by time and return labels or tags as fields.', - [DataTransformerID.merge]: 'Merge multiple series. Values will be combined into one row.', - [DataTransformerID.organize]: 'Re-order, hide, or rename fields.', - [DataTransformerID.partitionByValues]: 'Split a one-frame dataset into multiple series.', - [DataTransformerID.prepareTimeSeries]: 'Stretch data frames from the wide format into the long format.', - [DataTransformerID.reduce]: 'Reduce all rows or data points to a single value (ex. max, mean).', - [DataTransformerID.renameByRegex]: - 'Rename parts of the query results using a regular expression and replacement pattern.', - [DataTransformerID.seriesToRows]: 'Merge multiple series. Return time, metric and values as a row.', -}; - -const getTransformationsRedesignDescriptions = (id: string): string => { - return TransformationDescriptionOverrides[id] || standardTransformersRegistry.getIfExists(id)?.description || ''; -}; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index f3cd3798045..afdf6b782ea 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -5,6 +5,7 @@ import { Unsubscribable } from 'rxjs'; import { DataFrame, + DataQueryRequest, DataTransformerConfig, PanelData, SelectableValue, @@ -44,6 +45,7 @@ export type viewAllType = 'viewAll'; export type FilterCategory = TransformerCategory | viewAllType; export interface TransformationData { + request?: DataQueryRequest; series: DataFrame[]; annotations?: DataFrame[]; } @@ -397,8 +399,11 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE 'dashboard.un-themed-transformations-editor.title-delete-all-transformations', 'Delete all transformations?' )} - body="By deleting all transformations, you will go back to the main selection screen." - confirmText="Delete all" + body={t( + 'dashboard.un-theme-transformations-editor.body-delete-all-transformations', + 'By deleting all transformations, you will go back to the main selection screen.' + )} + confirmText={t('dashboard.un-themed-transformations-editor.confirmText-delete-all', 'Delete all')} onConfirm={() => this.onTransformationRemoveAll()} onDismiss={() => this.setState({ showRemoveAllModal: false })} /> diff --git a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx index 61ee6af4cf6..9650d46f822 100644 --- a/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx +++ b/public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx @@ -34,7 +34,11 @@ export const RevertDashboardModal = ({ hideModal, id, version }: RevertDashboard </Trans> </p> } - confirmText={`Yes, restore to version ${version}`} + confirmText={t( + 'dashboard.revert-dashboard-modal.confirmText-restore-version', + 'Yes, restore to version {{version}}', + { version } + )} /> ); }; diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx index 7c25150f7b8..59c74d1d308 100644 --- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx +++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx @@ -63,7 +63,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsT <td>{version.message}</td> <td className="text-right"> {idx === 0 ? ( - <Tag name="Latest" colorIndex={17} /> + <Tag name={t('dashboard.version-history-table.name-latest', 'Latest')} colorIndex={17} /> ) : ( <ModalsController> {({ showModal, hideModal }) => ( diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx index fa192c5a7b8..53c7633fbaa 100644 --- a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx +++ b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx @@ -4,8 +4,8 @@ import { useAsyncFn } from 'react-use'; import { locationUtil } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { historySrv } from 'app/features/dashboard-scene/settings/version-history'; -import { useSelector } from 'app/types'; +import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv'; +import { useSelector } from 'app/types/store'; import { dashboardWatcher } from '../../../live/dashboard/dashboardWatcher'; import { DashboardModel } from '../../state/DashboardModel'; diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 19e245cf03b..da7454a6d02 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -14,7 +14,7 @@ import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps'; import { RouteDescriptor } from 'app/core/navigation/types'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; -import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; +import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types/dashboard'; import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; import { DashboardSrv, setDashboardSrv } from '../services/DashboardSrv'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index bb4ad2a995c..16ed48ea618 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -17,8 +17,9 @@ import { ID_PREFIX } from 'app/core/reducers/navBarTree'; import { getNavModel } from 'app/core/selectors/navModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; -import { KioskMode, StoreState } from 'app/types'; +import { KioskMode } from 'app/types/dashboard'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions'; import { findTemplateVarChanges } from '../../variables/utils'; diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx index 7b206a79e44..e26b1099d2d 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx @@ -13,7 +13,7 @@ import { setupLoadDashboardMockReject, setupLoadDashboardRuntimeErrorMock, } from 'app/features/dashboard-scene/utils/test-utils'; -import { DashboardDTO, DashboardRoutes } from 'app/types'; +import { DashboardDTO, DashboardRoutes } from 'app/types/dashboard'; import { DashboardLoaderSrv, setDashboardLoaderSrv } from '../services/DashboardLoaderSrv'; @@ -33,6 +33,7 @@ const dashMock: DashboardDTO = { }, meta: { canEdit: false, + created: 'Friday, 4 July 2025 07:56:41 GMT+05:30', }, }; diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.tsx index a574ceb1e7e..2e37c4b3749 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.tsx @@ -5,7 +5,7 @@ import { config } from '@grafana/runtime'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import DashboardScenePage from 'app/features/dashboard-scene/pages/DashboardScenePage'; import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { isDashboardV2Resource } from '../api/utils'; diff --git a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx index f247b8865d5..46064030ca2 100644 --- a/public/app/features/dashboard/containers/NewDashboardWithDS.tsx +++ b/public/app/features/dashboard/containers/NewDashboardWithDS.tsx @@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom-v5-compat'; import { Trans } from '@grafana/i18n'; import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { setInitialDatasource } from '../state/reducers'; diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx index 6b3ebdcbee8..d9038bf1f2b 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.test.tsx @@ -8,8 +8,8 @@ import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { Dashboard, DashboardCursorSync, FieldConfigSource, Panel, ThresholdsMode } from '@grafana/schema/src'; import { getRouteComponentProps } from 'app/core/navigation/mocks/routeProps'; -import * as appTypes from 'app/types'; -import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; +import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types/dashboard'; +import { StoreState } from 'app/types/store'; import { configureStore } from '../../../store/configureStore'; import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; @@ -45,12 +45,12 @@ jest.mock('app/features/dashboard/state/initDashboard', () => ({ initDashboard: jest.fn(), })); -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), })); -const setup = (propOverrides?: Partial<Props>, initialState?: Partial<appTypes.StoreState>) => { +const setup = (propOverrides?: Partial<Props>, initialState?: Partial<StoreState>) => { const store = configureStore(initialState); const props: Props = { ...getRouteComponentProps({ diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 4c3957431d8..b6f74800a76 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -14,7 +14,8 @@ import { PublicDashboardPageRouteSearchParams, } from 'app/features/dashboard/containers/types'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; -import { useSelector, useDispatch, DashboardInitError } from 'app/types'; +import { DashboardInitError } from 'app/types/dashboard'; +import { useDispatch, useSelector } from 'app/types/store'; import { DashNavTimeControls } from '../components/DashNav/DashNavTimeControls'; import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed'; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx index 04b3678a0a1..b8ca09b1b4e 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -5,8 +5,7 @@ import { render } from 'test/test-utils'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; - -import { DashboardRoutes } from '../../../types'; +import { DashboardRoutes } from 'app/types/dashboard'; import PublicDashboardPageProxy, { PublicDashboardPageProxyProps } from './PublicDashboardPageProxy'; diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx index 2a159054622..786a3e165e2 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx @@ -5,7 +5,7 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { Dashboard } from '@grafana/schema'; import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { DashboardMeta, DashboardRoutes } from 'app/types'; +import { DashboardMeta, DashboardRoutes } from 'app/types/dashboard'; import { getRouteComponentProps } from '../../../core/navigation/mocks/routeProps'; import { Props as DashboardPanelProps } from '../dashgrid/DashboardPanel'; @@ -127,8 +127,12 @@ describe('SoloPanelPage', () => { soloPanelPageScenario('Dashboard init completed ', (ctx) => { ctx.setup(() => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + })), + }); ctx.mount(); ctx.setDashboard(); diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 7fbb737de76..1904e5eb487 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -10,7 +10,7 @@ import { Alert, useStyles2 } from '@grafana/ui'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { useGrafana } from '../../../core/context/GrafanaContext'; import { DashboardPanel } from '../dashgrid/DashboardPanel'; diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx index d18120fc5f0..09370002336 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.test.tsx @@ -8,8 +8,8 @@ import { onCreateNewPanel, onImportDashboard, onAddLibraryPanel } from '../utils import DashboardEmpty, { Props } from './DashboardEmpty'; -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => jest.fn(), useSelector: () => jest.fn(), })); diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index 9c82b005616..38f57b344e5 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -14,7 +14,7 @@ import { import { buildPanelEditScene } from 'app/features/dashboard-scene/panel-edit/PanelEditor'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { setInitialDatasource } from '../state/reducers'; diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 164d49ba4b8..330b4df5a43 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -7,7 +7,7 @@ import { Dashboard } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { GetVariables } from 'app/features/variables/state/selectors'; import { VariablesChanged } from 'app/features/variables/types'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { DashboardModel } from '../state/DashboardModel'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index e1ba53b8142..db9f93bb92a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,7 +1,7 @@ import { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { initPanelState } from '../../panel/state/actions'; import { setPanelInstanceState } from '../../panel/state/reducers'; diff --git a/public/app/features/dashboard/routes.ts b/public/app/features/dashboard/routes.ts index 4c20ec7229f..84e7d514b99 100644 --- a/public/app/features/dashboard/routes.ts +++ b/public/app/features/dashboard/routes.ts @@ -1,7 +1,8 @@ +import { DashboardRoutes } from 'app/types/dashboard'; + import { SafeDynamicImport } from '../../core/components/DynamicImports/SafeDynamicImport'; import { config } from '../../core/config'; import { RouteDescriptor } from '../../core/navigation/types'; -import { DashboardRoutes } from '../../types'; export const getPublicDashboardRoutes = (): RouteDescriptor[] => { if (!config.publicDashboardsEnabled) { diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index 22bb842a761..d28f575ce86 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -10,7 +10,7 @@ import impressionSrv from 'app/core/services/impression_srv'; import kbn from 'app/core/utils/kbn'; import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { appEvents } from '../../../core/core'; import { ResponseTransformers } from '../api/ResponseTransformers'; diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 99d7423e389..0ae57780d81 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -5,7 +5,7 @@ import { Dashboard } from '@grafana/schema'; import { appEvents } from 'app/core/app_events'; import { getBackendSrv } from 'app/core/services/backend_srv'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { RemovePanelEvent } from '../../../types/events'; import { DashboardModel } from '../state/DashboardModel'; diff --git a/public/app/features/dashboard/services/SnapshotSrv.ts b/public/app/features/dashboard/services/SnapshotSrv.ts index 1ca4894de22..c9d240ee3b4 100644 --- a/public/app/features/dashboard/services/SnapshotSrv.ts +++ b/public/app/features/dashboard/services/SnapshotSrv.ts @@ -2,7 +2,7 @@ import { lastValueFrom, map } from 'rxjs'; import { config, getBackendSrv, FetchResponse } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { DashboardDataDTO, DashboardDTO } from 'app/types'; +import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; import { getAPINamespace } from '../../../api/utils'; diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index b7ed66b05c3..00ba2415455 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -1043,7 +1043,7 @@ describe('DashboardModel', () => { }); }); - it('should have 11 variables after migration', () => { + it('should have 14 variables after migration', () => { expect(model.templating.list.length).toBe(14); }); diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index eacc6be7378..cea82ff384a 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -27,8 +27,13 @@ import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; import { variableAdapters } from 'app/features/variables/adapters'; import { onTimeRangeUpdated } from 'app/features/variables/state/actions'; import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors'; -import { CoreEvents, DashboardMeta } from 'app/types'; -import { DashboardMetaChangedEvent, DashboardPanelsChangedEvent, RenderEvent } from 'app/types/events'; +import { DashboardMeta } from 'app/types/dashboard'; +import { + DashboardMetaChangedEvent, + DashboardPanelsChangedEvent, + RenderEvent, + templateVariableValueUpdated, +} from 'app/types/events'; import { appEvents } from '../../../core/core'; import { dispatch } from '../../../store/store'; @@ -1155,7 +1160,7 @@ export class DashboardModel implements TimeModel { templateVariableValueUpdated() { this.processRepeats(); - this.events.emit(CoreEvents.templateVariableValueUpdated); + this.events.emit(templateVariableValueUpdated); } getPanelByUrlId(panelUrlId: string) { diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 3002dd32134..8556006879d 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -25,13 +25,13 @@ import { getTemplateSrv, RefreshEvent } from '@grafana/runtime'; import { LibraryPanel, LibraryPanelRef } from '@grafana/schema'; import config from 'app/core/config'; import { safeStringifyValue } from 'app/core/utils/explore'; -import { QueryGroupOptions } from 'app/types'; import { PanelOptionsChangedEvent, PanelQueriesChangedEvent, PanelTransformationsChangedEvent, RenderEvent, } from 'app/types/events'; +import { QueryGroupOptions } from 'app/types/query'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { TimeOverrideResult } from '../utils/panel'; diff --git a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts index 8dec4d28339..28bc7897330 100644 --- a/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts +++ b/public/app/features/dashboard/state/__fixtures__/dashboardFixtures.ts @@ -8,7 +8,7 @@ import { VariableModel, } from '@grafana/schema'; import { GetVariables } from 'app/features/variables/state/selectors'; -import { DashboardMeta } from 'app/types'; +import { DashboardMeta } from 'app/types/dashboard'; import { DashboardModel } from '../DashboardModel'; diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index ed28663f745..0395c6e9201 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -7,7 +7,7 @@ import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { removeAllPanels } from 'app/features/panel/state/reducers'; import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { loadPluginDashboards } from '../../plugins/admin/state/actions'; import { cancelVariables } from '../../variables/state/actions'; diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts index d291b62f379..123552c6984 100644 --- a/public/app/features/dashboard/state/initDashboard.test.ts +++ b/public/app/features/dashboard/state/initDashboard.test.ts @@ -9,7 +9,7 @@ import { KeybindingSrv } from 'app/core/services/keybindingSrv'; import { variableAdapters } from 'app/features/variables/adapters'; import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter'; import { constantBuilder } from 'app/features/variables/shared/testing/builders'; -import { DashboardInitPhase, DashboardRoutes } from 'app/types'; +import { DashboardInitPhase, DashboardRoutes } from 'app/types/dashboard'; import { Echo } from '../../../core/services/echo/Echo'; import { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 92feb552f55..348bb55616c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -27,10 +27,8 @@ import { DashboardRoutes, HomeDashboardRedirectDTO, isRedirectResponse, - StoreState, - ThunkDispatch, - ThunkResult, -} from 'app/types'; +} from 'app/types/dashboard'; +import { StoreState, ThunkDispatch, ThunkResult } from 'app/types/store'; import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner'; import { initVariablesTransaction } from '../../variables/state/actions'; diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts index 0cb8ee5cd32..f2d44fa61fb 100644 --- a/public/app/features/dashboard/state/reducers.test.ts +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -1,4 +1,4 @@ -import { DashboardInitPhase, DashboardState } from 'app/types'; +import { DashboardInitPhase, DashboardState } from 'app/types/dashboard'; import { createDashboardModelFixture, createPanelSaveModel } from './__fixtures__/dashboardFixtures'; import { diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index ddc842fc568..c96d82d8bef 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -3,7 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; import { defaultDashboard } from '@grafana/schema'; -import { DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types'; +import { DashboardInitError, DashboardInitPhase, DashboardState } from 'app/types/dashboard'; import { DashboardModel } from './DashboardModel'; import { PanelModel } from './PanelModel'; diff --git a/public/app/features/dashboard/state/selectors.ts b/public/app/features/dashboard/state/selectors.ts index 59d486d3a73..d45630812fd 100644 --- a/public/app/features/dashboard/state/selectors.ts +++ b/public/app/features/dashboard/state/selectors.ts @@ -1,5 +1,5 @@ import { PanelPlugin } from '@grafana/data'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { getPanelPluginNotFound } from '../../panel/components/PanelPluginError'; diff --git a/public/app/features/dashboard/utils/getPanelMenu.test.ts b/public/app/features/dashboard/utils/getPanelMenu.test.ts index 496e825cb69..3a18ce049dc 100644 --- a/public/app/features/dashboard/utils/getPanelMenu.test.ts +++ b/public/app/features/dashboard/utils/getPanelMenu.test.ts @@ -6,7 +6,7 @@ import config from 'app/core/config'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import * as actions from 'app/features/explore/state/main'; import { setStore } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PanelModel } from '../state/PanelModel'; import { createDashboardModelFixture } from '../state/__fixtures__/dashboardFixtures'; diff --git a/public/app/features/dataframe-import/index.ts b/public/app/features/dataframe-import/index.ts deleted file mode 100644 index 3f51aca9553..00000000000 --- a/public/app/features/dataframe-import/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './utils'; -export * from './constants'; diff --git a/public/app/features/datasources/components/DashboardsTable.test.tsx b/public/app/features/datasources/components/DashboardsTable.test.tsx index 3aac4b5a6a9..46b8148c992 100644 --- a/public/app/features/datasources/components/DashboardsTable.test.tsx +++ b/public/app/features/datasources/components/DashboardsTable.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { PluginDashboard } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; import DashboardsTable, { Props } from './DashboardsTable'; diff --git a/public/app/features/datasources/components/DashboardsTable.tsx b/public/app/features/datasources/components/DashboardsTable.tsx index 0f3424a05df..145114ed6d5 100644 --- a/public/app/features/datasources/components/DashboardsTable.tsx +++ b/public/app/features/datasources/components/DashboardsTable.tsx @@ -1,6 +1,6 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Icon } from '@grafana/ui'; -import { PluginDashboard } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; export interface Props { // List of plugin dashboards to show in the table diff --git a/public/app/features/datasources/components/DataSourceAddButton.tsx b/public/app/features/datasources/components/DataSourceAddButton.tsx index f41ad1a657b..627c3695da0 100644 --- a/public/app/features/datasources/components/DataSourceAddButton.tsx +++ b/public/app/features/datasources/components/DataSourceAddButton.tsx @@ -5,7 +5,7 @@ import { config } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ROUTES } from 'app/features/connections/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { trackAddNewDsClicked } from '../tracking'; diff --git a/public/app/features/datasources/components/DataSourceCategories.tsx b/public/app/features/datasources/components/DataSourceCategories.tsx index 58fa0e3783f..45eb2623e45 100644 --- a/public/app/features/datasources/components/DataSourceCategories.tsx +++ b/public/app/features/datasources/components/DataSourceCategories.tsx @@ -5,7 +5,7 @@ import { DataSourcePluginMeta, GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { LinkButton, useStyles2 } from '@grafana/ui'; -import { DataSourcePluginCategory } from 'app/types'; +import { DataSourcePluginCategory } from 'app/types/datasources'; import { ROUTES } from '../../connections/constants'; diff --git a/public/app/features/datasources/components/DataSourceDashboards.tsx b/public/app/features/datasources/components/DataSourceDashboards.tsx index 7b954ae9d03..0a16c265a02 100644 --- a/public/app/features/datasources/components/DataSourceDashboards.tsx +++ b/public/app/features/datasources/components/DataSourceDashboards.tsx @@ -3,10 +3,11 @@ import { useEffect } from 'react'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { importDashboard, removeDashboard } from 'app/features/dashboard/state/actions'; import { loadPluginDashboards } from 'app/features/plugins/admin/state/actions'; -import { PluginDashboard, StoreState, useDispatch, useSelector } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; +import { StoreState, useDispatch, useSelector } from 'app/types/store'; import DashboardTable from '../components/DashboardsTable'; -import { useInitDataSourceSettings } from '../state'; +import { useInitDataSourceSettings } from '../state/hooks'; export type Props = { // The UID of the data source diff --git a/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx b/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx index 1c42b13b080..dec9658d72f 100644 --- a/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx +++ b/public/app/features/datasources/components/DataSourceTestingStatus.test.tsx @@ -1,9 +1,14 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; + +import { PluginExtensionTypes } from '@grafana/data'; +import { setPluginLinksHook } from '@grafana/runtime'; import { getMockDataSource } from '../mocks/dataSourcesMocks'; import { DataSourceTestingStatus, Props } from './DataSourceTestingStatus'; +setPluginLinksHook(() => ({ links: [], isLoading: false })); + const getProps = (partialProps?: Partial<Props>): Props => ({ testingStatus: { status: 'success', @@ -89,4 +94,106 @@ describe('<DataSourceTestingStatus />', () => { expect(() => screen.getByTestId('data-testid Alert success')).toThrow(); expect(() => screen.getByTestId('data-testid Alert error')).toThrow(); }); + + describe('Plugin links', () => { + // Helper function to create mock plugin link extensions with all required properties + const createMockPluginLink = ( + overrides: Partial<{ + id: string; + path: string; + onClick: jest.Mock; + title: string; + description: string; + pluginId: string; + }> = {} + ) => ({ + id: 'test-link', + type: PluginExtensionTypes.link as const, + title: 'Test Link', + description: 'Test link description', + pluginId: 'test-plugin', + path: '/test', + onClick: jest.fn(), + ...overrides, + }); + + afterEach(() => { + // Reset the hook to default empty state + setPluginLinksHook(() => ({ links: [], isLoading: false })); + }); + + it('should render plugin links when severity is error and links exist', () => { + const mockLinks = [ + createMockPluginLink({ id: 'link1', path: 'http://example.com/help', title: 'Help Documentation' }), + createMockPluginLink({ id: 'link2', path: 'http://example.com/troubleshoot', title: 'Troubleshooting Guide' }), + ]; + + setPluginLinksHook(() => ({ links: mockLinks, isLoading: false })); + + const props = getProps({ + testingStatus: { + status: 'error', + message: 'Data source connection failed', + }, + }); + + render(<DataSourceTestingStatus {...props} />); + + expect(screen.getByText('Help Documentation')).toBeInTheDocument(); + expect(screen.getByText('Troubleshooting Guide')).toBeInTheDocument(); + + const helpLink = screen.getByText('Help Documentation').closest('a'); + const troubleshootLink = screen.getByText('Troubleshooting Guide').closest('a'); + + expect(helpLink).toHaveAttribute('href', 'http://example.com/help'); + expect(troubleshootLink).toHaveAttribute('href', 'http://example.com/troubleshoot'); + }); + + it('should call onClick handler when plugin link is clicked', () => { + const mockOnClick = jest.fn(); + const mockLinks = [ + createMockPluginLink({ + id: 'link1', + path: 'http://example.com/help', + onClick: mockOnClick, + title: 'Help Documentation', + }), + ]; + + setPluginLinksHook(() => ({ links: mockLinks, isLoading: false })); + + const props = getProps({ + testingStatus: { + status: 'error', + message: 'Data source connection failed', + }, + }); + + render(<DataSourceTestingStatus {...props} />); + + const helpLink = screen.getByText('Help Documentation'); + fireEvent.click(helpLink); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + it('should NOT render plugin links when severity is not error even if links exist', () => { + const mockLinks = [ + createMockPluginLink({ id: 'link1', path: 'http://example.com/help', title: 'Help Documentation' }), + ]; + + setPluginLinksHook(() => ({ links: mockLinks, isLoading: false })); + + const props = getProps({ + testingStatus: { + status: 'success', + message: 'Data source is working', + }, + }); + + render(<DataSourceTestingStatus {...props} />); + + expect(screen.queryByText('Help Documentation')).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/datasources/components/DataSourceTestingStatus.tsx b/public/app/features/datasources/components/DataSourceTestingStatus.tsx index f1e6463e019..813e2e55aa8 100644 --- a/public/app/features/datasources/components/DataSourceTestingStatus.tsx +++ b/public/app/features/datasources/components/DataSourceTestingStatus.tsx @@ -1,10 +1,11 @@ import { css, cx } from '@emotion/css'; import { HTMLAttributes } from 'react'; -import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2 } from '@grafana/data'; +import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2, PluginExtensionPoints } from '@grafana/data'; +import { sanitizeUrl } from '@grafana/data/internal'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { TestingStatus, config } from '@grafana/runtime'; +import { TestingStatus, config, usePluginLinks } from '@grafana/runtime'; import { AlertVariant, Alert, useTheme2, Link, useStyles2 } from '@grafana/ui'; import { contextSrv } from '../../../core/core'; @@ -147,6 +148,18 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource }); }; const styles = useStyles2(getTestingStatusStyles); + const { links } = usePluginLinks({ + extensionPointId: PluginExtensionPoints.DataSourceConfigErrorStatus, + context: { + dataSource: { + type: dataSource.type, + uid: dataSource.uid, + name: dataSource.name, + }, + testingStatus, + }, + limitPerPlugin: 3, + }); if (message) { return ( @@ -169,6 +182,22 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource ) : null} </> )} + {severity === 'error' && links.length > 0 && ( + <div className={styles.linksContainer}> + {links.map((link) => { + return ( + <a + key={link.id} + href={link.path ? sanitizeUrl(link.path) : undefined} + onClick={link.onClick} + className={styles.pluginLink} + > + {link.title} + </a> + ); + })} + </div> + )} </Alert> </div> ); @@ -184,4 +213,23 @@ const getTestingStatusStyles = (theme: GrafanaTheme2) => ({ moreLink: css({ marginBlock: theme.spacing(1), }), + linksContainer: css({ + display: 'flex', + justifyContent: 'flex-end', + marginTop: theme.spacing(1), + }), + pluginLink: css({ + color: theme.colors.text.link, + textDecoration: 'none', + marginLeft: theme.spacing(2), + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightMedium, + '&:hover': { + color: theme.colors.text.primary, + textDecoration: 'underline', + }, + '&:first-child': { + marginLeft: 0, + }, + }), }); diff --git a/public/app/features/datasources/components/DataSourcesList.tsx b/public/app/features/datasources/components/DataSourcesList.tsx index 00f5ee5ae31..b4337792ebc 100644 --- a/public/app/features/datasources/components/DataSourcesList.tsx +++ b/public/app/features/datasources/components/DataSourcesList.tsx @@ -7,10 +7,12 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { EmptyState, LinkButton, TextLink, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { StoreState, AccessControlAction, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState, useSelector } from 'app/types/store'; import { ROUTES } from '../../connections/constants'; -import { getDataSources, getDataSourcesCount, useLoadDataSources } from '../state'; +import { useLoadDataSources } from '../state/hooks'; +import { getDataSources, getDataSourcesCount } from '../state/selectors'; import { trackDataSourcesListViewed } from '../tracking'; import { DataSourcesListCard } from './DataSourcesListCard'; diff --git a/public/app/features/datasources/components/DataSourcesListHeader.tsx b/public/app/features/datasources/components/DataSourcesListHeader.tsx index cf0a97883d1..26db52a7a33 100644 --- a/public/app/features/datasources/components/DataSourcesListHeader.tsx +++ b/public/app/features/datasources/components/DataSourcesListHeader.tsx @@ -3,9 +3,10 @@ import { useCallback, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; -import { StoreState, useSelector, useDispatch } from 'app/types'; +import { StoreState, useSelector, useDispatch } from 'app/types/store'; -import { getDataSourcesSearchQuery, getDataSourcesSort, setDataSourcesSearchQuery, setIsSortAscending } from '../state'; +import { setDataSourcesSearchQuery, setIsSortAscending } from '../state/reducers'; +import { getDataSourcesSearchQuery, getDataSourcesSort } from '../state/selectors'; import { trackDsSearched } from '../tracking'; const ascendingSortValue = 'alpha-asc'; diff --git a/public/app/features/datasources/components/EditDataSource.test.tsx b/public/app/features/datasources/components/EditDataSource.test.tsx index 3631edc2e0a..6a1f6832485 100644 --- a/public/app/features/datasources/components/EditDataSource.test.tsx +++ b/public/app/features/datasources/components/EditDataSource.test.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { Provider } from 'react-redux'; import { DataSourceJsonData, PluginExtensionDataSourceConfigContext, PluginState } from '@grafana/data'; -import { setPluginComponentsHook } from '@grafana/runtime'; +import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; import { createComponentWithMeta } from 'app/features/plugins/extensions/usePluginComponents'; import { configureStore } from 'app/store/configureStore'; @@ -27,6 +27,8 @@ jest.mock('@grafana/runtime', () => { }; }); +setPluginLinksHook(() => ({ links: [], isLoading: false })); + const setup = (props?: Partial<ViewProps>) => { const store = configureStore(); diff --git a/public/app/features/datasources/components/EditDataSource.tsx b/public/app/features/datasources/components/EditDataSource.tsx index 077e66f931f..06877e8ad72 100644 --- a/public/app/features/datasources/components/EditDataSource.tsx +++ b/public/app/features/datasources/components/EditDataSource.tsx @@ -13,12 +13,10 @@ import { import { getDataSourceSrv, usePluginComponents, UsePluginComponentsResult } from '@grafana/runtime'; import appEvents from 'app/core/app_events'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; -import { DataSourceSettingsState, useDispatch } from 'app/types'; +import { DataSourceSettingsState } from 'app/types/datasources'; +import { useDispatch } from 'app/types/store'; import { - dataSourceLoaded, - setDataSourceName, - setIsDefault, useDataSource, useDataSourceExploreUrl, useDataSourceMeta, @@ -28,7 +26,8 @@ import { useInitDataSourceSettings, useTestDataSource, useUpdateDatasource, -} from '../state'; +} from '../state/hooks'; +import { setIsDefault, setDataSourceName, dataSourceLoaded } from '../state/reducers'; import { trackDsConfigClicked, trackDsConfigUpdated } from '../tracking'; import { DataSourceRights } from '../types'; diff --git a/public/app/features/datasources/components/EditDataSourceActions.tsx b/public/app/features/datasources/components/EditDataSourceActions.tsx index b60f3774f0a..a2d18bb11f5 100644 --- a/public/app/features/datasources/components/EditDataSourceActions.tsx +++ b/public/app/features/datasources/components/EditDataSourceActions.tsx @@ -3,7 +3,7 @@ import { config } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { useDataSource } from '../state'; +import { useDataSource } from '../state/hooks'; import { trackCreateDashboardClicked, trackDsConfigClicked, trackExploreClicked } from '../tracking'; import { constructDataSourceExploreUrl } from '../utils'; diff --git a/public/app/features/datasources/components/NewDataSource.tsx b/public/app/features/datasources/components/NewDataSource.tsx index db92959af19..8db925f5e87 100644 --- a/public/app/features/datasources/components/NewDataSource.tsx +++ b/public/app/features/datasources/components/NewDataSource.tsx @@ -5,17 +5,15 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, FilterInput } from '@grafana/ui'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { PluginsErrorsInfo } from 'app/features/plugins/components/PluginsErrorsInfo'; -import { DataSourcePluginCategory, StoreState, useDispatch, useSelector } from 'app/types'; +import { DataSourcePluginCategory } from 'app/types/datasources'; +import { StoreState, useDispatch, useSelector } from 'app/types/store'; import { ROUTES } from '../../connections/constants'; import { DataSourceCategories } from '../components/DataSourceCategories'; import { DataSourceTypeCardList } from '../components/DataSourceTypeCardList'; -import { - useAddDatasource, - useLoadDataSourcePlugins, - getFilteredDataSourcePlugins, - setDataSourceTypeSearchQuery, -} from '../state'; +import { useAddDatasource, useLoadDataSourcePlugins } from '../state/hooks'; +import { setDataSourceTypeSearchQuery } from '../state/reducers'; +import { getFilteredDataSourcePlugins } from '../state/selectors'; export function NewDataSource() { useLoadDataSourcePlugins(); diff --git a/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx b/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx index b5489e59cb1..7e03ccf2c8b 100644 --- a/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx +++ b/public/app/features/datasources/components/picker/AddNewDataSourceButton.tsx @@ -3,7 +3,7 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, ButtonVariant } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; interface AddNewDataSourceButtonProps { onClick?: () => void; diff --git a/public/app/features/datasources/components/picker/DataSourceModal.tsx b/public/app/features/datasources/components/picker/DataSourceModal.tsx index 89f8226b3a9..0bfa3ca2744 100644 --- a/public/app/features/datasources/components/picker/DataSourceModal.tsx +++ b/public/app/features/datasources/components/picker/DataSourceModal.tsx @@ -15,7 +15,7 @@ import { Icon, ScrollContainer, } from '@grafana/ui'; -import * as DFImport from 'app/features/dataframe-import'; +import { acceptedFiles, maxFileSize } from 'app/features/dataframe-import/constants'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { getFileDropToQueryHandler } from 'app/plugins/datasource/grafana/utils'; @@ -203,9 +203,9 @@ export function DataSourceModal({ readAs="readAsArrayBuffer" fileListRenderer={() => undefined} options={{ - maxSize: DFImport.maxFileSize, + maxSize: maxFileSize, multiple: false, - accept: DFImport.acceptedFiles, + accept: acceptedFiles, onDrop: onFileDrop, }} > diff --git a/public/app/features/datasources/components/picker/utils.ts b/public/app/features/datasources/components/picker/utils.ts index c626047631c..ff987c26f5b 100644 --- a/public/app/features/datasources/components/picker/utils.ts +++ b/public/app/features/datasources/components/picker/utils.ts @@ -5,7 +5,7 @@ import { setLastUsedDatasourceKeyForDashboard, } from 'app/features/dashboard/utils/dashboard'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { QueryGroupDataSource } from 'app/types'; +import { QueryGroupDataSource } from 'app/types/query'; export function isDataSourceMatch( ds: DataSourceInstanceSettings | undefined, diff --git a/public/app/features/datasources/mocks/dataSourcesMocks.ts b/public/app/features/datasources/mocks/dataSourcesMocks.ts index b58dec19453..807082bd843 100644 --- a/public/app/features/datasources/mocks/dataSourcesMocks.ts +++ b/public/app/features/datasources/mocks/dataSourcesMocks.ts @@ -2,7 +2,8 @@ import { merge } from 'lodash'; import { DataSourceSettings, DataSourcePluginMeta, DataSourceJsonData } from '@grafana/data'; import amazonWebServicesPng from 'app/plugins/datasource/cloudwatch/img/amazon-web-services.png'; -import { DataSourceSettingsState, PluginDashboard } from 'app/types'; +import { DataSourceSettingsState } from 'app/types/datasources'; +import { PluginDashboard } from 'app/types/plugins'; export const getMockDashboard = (override?: Partial<PluginDashboard>) => ({ uid: 'G1btqkgkK', diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts index f3b99a49bbc..79aaf70a62c 100644 --- a/public/app/features/datasources/state/actions.test.ts +++ b/public/app/features/datasources/state/actions.test.ts @@ -4,7 +4,7 @@ import { AppPluginMeta, DataSourceSettings, PluginMetaInfo, PluginType } from '@ import { DataSourceSrv, FetchError } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { ThunkResult, ThunkDispatch } from 'app/types'; +import { ThunkResult, ThunkDispatch } from 'app/types/store'; import * as api from '../api'; import { DATASOURCES_ROUTES } from '../constants'; @@ -105,9 +105,10 @@ describe('loadDataSource()', () => { const dispatch = jest.fn(); const getState = jest.fn(); + const win: typeof globalThis = window; // @ts-ignore - delete window.location; - window.location = {} as Location; + delete win.location; + win.location = {} as Location; (api.getDataSourceByIdOrUid as jest.Mock).mockResolvedValueOnce(dataSourceMock); @@ -126,9 +127,10 @@ describe('loadDataSource()', () => { const dispatch = jest.fn(); const getState = jest.fn(); + const win: typeof globalThis = window; // @ts-ignore - delete window.location; - window.location = {} as Location; + delete win.location; + win.location = {} as Location; (api.getDataSourceByIdOrUid as jest.Mock).mockResolvedValueOnce(dataSourceMock); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 7cd1411b9eb..09835ce72b2 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -23,9 +23,11 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceAPIVersions } from 'app/features/apiserver/client'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { importDataSourcePlugin } from 'app/features/plugins/pluginLoader'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; -import { AccessControlAction, DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { DataSourcePluginCategory } from 'app/types/datasources'; +import { ThunkDispatch, ThunkResult } from 'app/types/store'; import * as api from '../api'; import { DATASOURCES_ROUTES } from '../constants'; diff --git a/public/app/features/datasources/state/buildCategories.ts b/public/app/features/datasources/state/buildCategories.ts index b357798fdaa..ec813a6e1e6 100644 --- a/public/app/features/datasources/state/buildCategories.ts +++ b/public/app/features/datasources/state/buildCategories.ts @@ -1,7 +1,7 @@ import { DataSourcePluginMeta, PluginType } from '@grafana/data'; import { t } from '@grafana/i18n'; import { featureEnabled } from '@grafana/runtime'; -import { DataSourcePluginCategory } from 'app/types'; +import { DataSourcePluginCategory } from 'app/types/datasources'; import grafanaIconSvg from 'img/grafana_icon.svg'; import adobeAnalyticsSvg from 'img/plugins/adobe-analytics.svg'; import appdynamicsSvg from 'img/plugins/appdynamics.svg'; diff --git a/public/app/features/datasources/state/hooks.ts b/public/app/features/datasources/state/hooks.ts index af0c5dd7e6b..61e7be7ae09 100644 --- a/public/app/features/datasources/state/hooks.ts +++ b/public/app/features/datasources/state/hooks.ts @@ -5,8 +5,9 @@ import { t } from '@grafana/i18n'; import { cleanUpAction } from 'app/core/actions/cleanUp'; import appEvents from 'app/core/app_events'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, useDispatch, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { useDispatch, useSelector } from 'app/types/store'; import { ROUTES } from '../../connections/constants'; import { DataSourceRights } from '../types'; diff --git a/public/app/features/datasources/state/index.ts b/public/app/features/datasources/state/index.ts deleted file mode 100644 index 42fae7a84a7..00000000000 --- a/public/app/features/datasources/state/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './actions'; -export * from './buildCategories'; -export * from './hooks'; -export * from './navModel'; -export * from './reducers'; -export * from './selectors'; diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index c71bff78248..2c685beae68 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -5,7 +5,7 @@ import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { highlightTrial } from 'app/features/admin/utils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import icnDatasourceSvg from 'img/icn-datasource.svg'; import { GenericDataSourcePlugin } from '../types'; diff --git a/public/app/features/datasources/state/reducers.test.ts b/public/app/features/datasources/state/reducers.test.ts index 85f6455e837..43555d3208b 100644 --- a/public/app/features/datasources/state/reducers.test.ts +++ b/public/app/features/datasources/state/reducers.test.ts @@ -1,7 +1,7 @@ import { reducerTester } from 'test/core/redux/reducerTester'; import { PluginMeta, PluginMetaInfo, PluginType, LayoutModes } from '@grafana/data'; -import { DataSourceSettingsState, DataSourcesState } from 'app/types'; +import { DataSourceSettingsState, DataSourcesState } from 'app/types/datasources'; import { getMockDataSource, getMockDataSources } from '../mocks/dataSourcesMocks'; import { GenericDataSourcePlugin } from '../types'; diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index e5b2588499c..700f06e6133 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -3,7 +3,7 @@ import { AnyAction, createAction } from '@reduxjs/toolkit'; import { DataSourcePluginMeta, DataSourceSettings, LayoutMode, LayoutModes } from '@grafana/data'; import { t } from '@grafana/i18n'; import { TestingStatus } from '@grafana/runtime'; -import { DataSourcesState, DataSourceSettingsState } from 'app/types'; +import { DataSourcesState, DataSourceSettingsState } from 'app/types/datasources'; import { GenericDataSourcePlugin } from '../types'; diff --git a/public/app/features/dimensions/editors/ColorDimensionEditor.tsx b/public/app/features/dimensions/editors/ColorDimensionEditor.tsx index 70f750c3cf3..a8c7d5af8c5 100644 --- a/public/app/features/dimensions/editors/ColorDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ColorDimensionEditor.tsx @@ -74,7 +74,7 @@ export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionCo value={selectedOption} options={selectOptions} onChange={onSelectChange} - noOptionsMessage="No fields found" + noOptionsMessage={t('dimensions.color-dimension-editor.noOptionsMessage-no-fields-found', 'No fields found')} isClearable={item.settings?.isClearable} placeholder={item.settings?.placeholder} /> diff --git a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx index c56f6d1f2be..1b5d63348e6 100644 --- a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx @@ -7,8 +7,8 @@ import { ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema' import { InlineField, InlineFieldRow, RadioButtonGroup } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/internal'; -import { getPublicOrAbsoluteUrl, ResourceFolderName } from '..'; -import { MediaType, ResourceDimensionOptions, ResourcePickerSize } from '../types'; +import { getPublicOrAbsoluteUrl } from '../resource'; +import { MediaType, ResourceDimensionOptions, ResourceFolderName, ResourcePickerSize } from '../types'; import { ResourcePicker } from './ResourcePicker'; diff --git a/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx b/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx index 89b8f319c8e..a88c7334571 100644 --- a/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx @@ -107,7 +107,7 @@ export const ScalarDimensionEditor = ({ value, context, onChange, item }: Props) value={selectedOption} options={selectOptions} onChange={onSelectChange} - noOptionsMessage="No fields found" + noOptionsMessage={t('dimensions.scalar-dimension-editor.noOptionsMessage-no-fields-found', 'No fields found')} /> </div> <div className={styles.range}> diff --git a/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx b/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx index f2b69690c62..d5eb712d47d 100644 --- a/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ScaleDimensionEditor.tsx @@ -11,16 +11,19 @@ import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { validateScaleOptions, validateScaleConfig } from '../scale'; import { ScaleDimensionOptions } from '../types'; -const fixedValueOption: SelectableValue<string> = { - label: 'Fixed value', - value: '_____fixed_____', -}; - export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionConfig, ScaleDimensionOptions>) => { const { value, context, onChange, item } = props; const { settings } = item; const styles = useStyles2(getStyles); + const fixedValueOption: SelectableValue<string> = useMemo( + () => ({ + label: t('dimensions.scale-dimension-editor.fixed-value-option.label.fixed-value', 'Fixed value'), + value: '_____fixed_____', + }), + [] + ); + const fieldName = value?.field; const isFixed = Boolean(!fieldName); const names = useFieldDisplayNames(context.data); @@ -53,7 +56,7 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo }); } }, - [validateAndDoChange, value] + [validateAndDoChange, value, fixedValueOption.value] ); const onMinChange = useCallback( @@ -101,7 +104,7 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo value={selectedOption} options={selectOptions} onChange={onSelectChange} - noOptionsMessage="No fields found" + noOptionsMessage={t('dimensions.scale-dimension-editor.noOptionsMessage-no-fields-found', 'No fields found')} /> </div> <div className={styles.range}> diff --git a/public/app/features/dimensions/editors/index.ts b/public/app/features/dimensions/editors/index.ts deleted file mode 100644 index fe6418b3609..00000000000 --- a/public/app/features/dimensions/editors/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './ColorDimensionEditor'; -export * from './IconSelector'; -export * from './ResourceDimensionEditor'; -export * from './ScaleDimensionEditor'; -export * from './ScalarDimensionEditor'; -export * from './TextDimensionEditor'; diff --git a/public/app/features/dimensions/index.ts b/public/app/features/dimensions/index.ts deleted file mode 100644 index df2598082e9..00000000000 --- a/public/app/features/dimensions/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './types'; - -export * from './color'; -export * from './scale'; -export * from './text'; -export * from './utils'; -export * from './resource'; -export * from './context'; -export * from './scalar'; diff --git a/public/app/features/dimensions/utils.ts b/public/app/features/dimensions/utils.ts index 3e762cc5841..359e21bbf4c 100644 --- a/public/app/features/dimensions/utils.ts +++ b/public/app/features/dimensions/utils.ts @@ -7,15 +7,13 @@ import { ColorDimensionConfig, ScalarDimensionConfig, } from '@grafana/schema'; -import { - getColorDimension, - getScaledDimension, - getTextDimension, - getResourceDimension, - DimensionSupplier, -} from 'app/features/dimensions'; +import { getColorDimension } from './color'; +import { getResourceDimension } from './resource'; import { getScalarDimension } from './scalar'; +import { getScaledDimension } from './scale'; +import { getTextDimension } from './text'; +import { DimensionSupplier } from './types'; export function getColorDimensionFromData( data: PanelData | undefined, diff --git a/public/app/features/explore/CorrelationEditorModeBar.tsx b/public/app/features/explore/CorrelationEditorModeBar.tsx index 87d531eb41d..25739ad1b5c 100644 --- a/public/app/features/explore/CorrelationEditorModeBar.tsx +++ b/public/app/features/explore/CorrelationEditorModeBar.tsx @@ -7,7 +7,8 @@ import { Trans, t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { Prompt } from 'app/core/components/FormPrompt/Prompt'; -import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState, useDispatch, useSelector } from 'app/types'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState } from 'app/types/explore'; +import { useDispatch, useSelector } from 'app/types/store'; import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal'; import { showModalMessage } from './correlationEditLogic'; diff --git a/public/app/features/explore/CorrelationHelper.tsx b/public/app/features/explore/CorrelationHelper.tsx index 2444a4dd7b6..bf57509bf63 100644 --- a/public/app/features/explore/CorrelationHelper.tsx +++ b/public/app/features/explore/CorrelationHelper.tsx @@ -19,7 +19,7 @@ import { Icon, Stack, } from '@grafana/ui'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { getTransformationVars } from '../correlations/transformations'; import { generateDefaultLabel } from '../correlations/utils'; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index bbaf27f3524..57ef823de4d 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -33,7 +33,7 @@ import { import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/internal'; import { supportedFeatures } from 'app/core/history/richHistoryStorageProvider'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { getTimeZone } from '../profile/state/selectors'; diff --git a/public/app/features/explore/ExploreActions.tsx b/public/app/features/explore/ExploreActions.tsx index de35e644b4e..376cc25d715 100644 --- a/public/app/features/explore/ExploreActions.tsx +++ b/public/app/features/explore/ExploreActions.tsx @@ -4,7 +4,8 @@ import { useEffect, useState } from 'react'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { AccessControlAction, useDispatch, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useDispatch, useSelector } from 'app/types/store'; import { splitOpen, splitClose, changeCorrelationEditorDetails } from './state/main'; import { runQueries } from './state/query'; diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 6799eddec3b..d5974011bbb 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -9,8 +9,8 @@ import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPane import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { useSelector } from 'app/types'; import { ExploreQueryParams } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { CorrelationEditorModeBar } from './CorrelationEditorModeBar'; import { ExploreActions } from './ExploreActions'; diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index 07aba1a5a41..d6d9831bbc2 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { EventBusSrv, getTimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { stopQueryState } from 'app/core/utils/explore'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import Explore from './Explore'; import ExploreQueryInspector from './ExploreQueryInspector'; diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 24f73322bc1..70badc6c689 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -15,7 +15,8 @@ import { InspectJSONTab } from 'app/features/inspector/InspectJSONTab'; import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab'; import { QueryInspector } from 'app/features/inspector/QueryInspector'; import { mixedRequestId } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { StoreState, ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { GetDataOptions } from '../query/state/PanelQueryRunner'; diff --git a/public/app/features/explore/ExploreRunQueryButton.test.tsx b/public/app/features/explore/ExploreRunQueryButton.test.tsx index e2b518654e9..0cb9290f62c 100644 --- a/public/app/features/explore/ExploreRunQueryButton.test.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.test.tsx @@ -6,7 +6,7 @@ import { DatasourceSrvMock, MockDataSourceApi } from 'test/mocks/datasource_srv' import { DataSourceApi } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; import { configureStore } from 'app/store/configureStore'; -import { ExploreItemState, ExploreState } from 'app/types'; +import { ExploreItemState, ExploreState } from 'app/types/explore'; import { Props, ExploreRunQueryButton } from './ExploreRunQueryButton'; import { makeExplorePaneState } from './state/utils'; diff --git a/public/app/features/explore/ExploreRunQueryButton.tsx b/public/app/features/explore/ExploreRunQueryButton.tsx index 49a2fe3acaa..78f54525705 100644 --- a/public/app/features/explore/ExploreRunQueryButton.tsx +++ b/public/app/features/explore/ExploreRunQueryButton.tsx @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { changeDatasource } from './state/datasource'; import { setQueries } from './state/query'; diff --git a/public/app/features/explore/Graph/ExploreGraph.tsx b/public/app/features/explore/Graph/ExploreGraph.tsx index cd69a11af37..e4dedb3f76c 100644 --- a/public/app/features/explore/Graph/ExploreGraph.tsx +++ b/public/app/features/explore/Graph/ExploreGraph.tsx @@ -31,7 +31,7 @@ import { import { PanelContext, PanelContextProvider, SeriesVisibilityChangeMode, useTheme2 } from '@grafana/ui'; import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config'; import { Options as TimeSeriesOptions } from 'app/plugins/panel/timeseries/panelcfg.gen'; -import { ExploreGraphStyle } from 'app/types'; +import { ExploreGraphStyle } from 'app/types/explore'; import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory'; import { useExploreDataLinkPostProcessor } from '../hooks/useExploreDataLinkPostProcessor'; diff --git a/public/app/features/explore/Graph/ExploreGraphLabel.tsx b/public/app/features/explore/Graph/ExploreGraphLabel.tsx index a8be709f53b..9b2069546eb 100644 --- a/public/app/features/explore/Graph/ExploreGraphLabel.tsx +++ b/public/app/features/explore/Graph/ExploreGraphLabel.tsx @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; import { RadioButtonGroup } from '@grafana/ui'; -import { EXPLORE_GRAPH_STYLES, ExploreGraphStyle } from 'app/types'; +import { EXPLORE_GRAPH_STYLES, ExploreGraphStyle } from 'app/types/explore'; const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<ExploreGraphStyle>> = EXPLORE_GRAPH_STYLES.map((style) => ({ value: style, diff --git a/public/app/features/explore/Graph/GraphContainer.tsx b/public/app/features/explore/Graph/GraphContainer.tsx index b9a9fe44a5f..7cbf420dccf 100644 --- a/public/app/features/explore/Graph/GraphContainer.tsx +++ b/public/app/features/explore/Graph/GraphContainer.tsx @@ -13,7 +13,7 @@ import { } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { GraphThresholdsStyleConfig, PanelChrome, PanelChromeProps } from '@grafana/ui'; -import { ExploreGraphStyle } from 'app/types'; +import { ExploreGraphStyle } from 'app/types/explore'; import { LimitedDataDisclaimer } from '../LimitedDataDisclaimer'; import { storeGraphStyle } from '../state/utils'; diff --git a/public/app/features/explore/Graph/exploreGraphStyleUtils.ts b/public/app/features/explore/Graph/exploreGraphStyleUtils.ts index 224ece64100..6206d387058 100644 --- a/public/app/features/explore/Graph/exploreGraphStyleUtils.ts +++ b/public/app/features/explore/Graph/exploreGraphStyleUtils.ts @@ -2,7 +2,7 @@ import { produce } from 'immer'; import { FieldConfigSource, ThresholdsConfig } from '@grafana/data'; import { GraphDrawStyle, GraphFieldConfig, GraphThresholdsStyleConfig, StackingMode } from '@grafana/schema'; -import { ExploreGraphStyle } from 'app/types'; +import { ExploreGraphStyle } from 'app/types/explore'; export type FieldConfig = FieldConfigSource<GraphFieldConfig>; diff --git a/public/app/features/explore/Graph/utils.ts b/public/app/features/explore/Graph/utils.ts index 3b42e342c1f..38ee661969b 100644 --- a/public/app/features/explore/Graph/utils.ts +++ b/public/app/features/explore/Graph/utils.ts @@ -1,5 +1,5 @@ import store from 'app/core/store'; -import { ExploreGraphStyle, EXPLORE_GRAPH_STYLES } from 'app/types'; +import { ExploreGraphStyle, EXPLORE_GRAPH_STYLES } from 'app/types/explore'; const GRAPH_STYLE_KEY = 'grafana.explore.style.graph'; export const storeGraphStyle = (graphStyle: string): void => { diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 5135821c28f..958a8c589f8 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -10,13 +10,12 @@ import { LoadingState, LogLevel, LogRowModel, - standardTransformersRegistry, toUtc, createDataFrame, ExploreLogsPanelState, DataQuery, } from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; @@ -80,18 +79,7 @@ describe('Logs', () => { }); beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); }); afterAll(() => { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index c51cbfb7d4d..4507fa9e028 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -60,7 +60,8 @@ import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { getState } from 'app/store/store'; -import { ExploreItemState, useDispatch } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { useDispatch } from 'app/types/store'; import { contentOutlineTrackPinAdded, @@ -1144,6 +1145,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => { permalinkedLogId={panelState?.logs?.id} pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} pinnedLogs={pinnedLogs} + setDisplayedFields={setDisplayedFields} showControls showTime={showTime} sortOrder={logsSortOrder} diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 8ca38ab007d..6ff6c9a248c 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -27,8 +27,8 @@ import { DataQuery } from '@grafana/schema'; import { Collapse } from '@grafana/ui'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; -import { StoreState } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { getTimeZone } from '../../profile/state/selectors'; import { diff --git a/public/app/features/explore/Logs/LogsMetaRow.test.tsx b/public/app/features/explore/Logs/LogsMetaRow.test.tsx index 618be846511..598b7cebb7a 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.test.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.test.tsx @@ -3,17 +3,8 @@ import userEvent from '@testing-library/user-event'; import saveAs from 'file-saver'; import { ComponentProps } from 'react'; -import { - FieldType, - LogLevel, - LogsDedupStrategy, - LogsMetaItem, - LogsMetaKind, - standardTransformersRegistry, - store, - toDataFrame, -} from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { FieldType, LogLevel, LogsDedupStrategy, LogsMetaItem, LogsMetaKind, store, toDataFrame } from '@grafana/data'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { logRowsToReadableJson } from '../../logs/utils'; @@ -177,18 +168,7 @@ describe('LogsMetaRow', () => { it('renders a button to download CSV', async () => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); const rows = [ { diff --git a/public/app/features/explore/Logs/LogsTable.test.tsx b/public/app/features/explore/Logs/LogsTable.test.tsx index 373bd38504a..2c9cdedb95a 100644 --- a/public/app/features/explore/Logs/LogsTable.test.tsx +++ b/public/app/features/explore/Logs/LogsTable.test.tsx @@ -1,8 +1,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import { ComponentProps } from 'react'; -import { DataFrame, FieldType, LogsSortOrder, standardTransformersRegistry, toUtc } from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { DataFrame, FieldType, LogsSortOrder, toUtc } from '@grafana/data'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; @@ -91,18 +91,7 @@ const setup = (partialProps?: Partial<ComponentProps<typeof LogsTable>>, logs?: describe('LogsTable', () => { beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); }); let originalVisualisationTypeValue = config.featureToggles.logsExploreTableVisualisation; diff --git a/public/app/features/explore/Logs/LogsTableWrap.test.tsx b/public/app/features/explore/Logs/LogsTableWrap.test.tsx index 0698f141043..b14690a32a9 100644 --- a/public/app/features/explore/Logs/LogsTableWrap.test.tsx +++ b/public/app/features/explore/Logs/LogsTableWrap.test.tsx @@ -1,8 +1,8 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { ComponentProps } from 'react'; -import { createTheme, ExploreLogsPanelState, LogsSortOrder, standardTransformersRegistry, toUtc } from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { createTheme, ExploreLogsPanelState, LogsSortOrder, toUtc } from '@grafana/data'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from '../../transformers/extractFields/extractFields'; @@ -39,18 +39,7 @@ const setup = (partialProps?: Partial<ComponentProps<typeof LogsTableWrap>>) => describe('LogsTableWrap', () => { beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); }); it('should render 4 table rows', async () => { diff --git a/public/app/features/explore/NoDataSourceCallToAction.tsx b/public/app/features/explore/NoDataSourceCallToAction.tsx index 822da87b1c6..5d67b2d1475 100644 --- a/public/app/features/explore/NoDataSourceCallToAction.tsx +++ b/public/app/features/explore/NoDataSourceCallToAction.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { LinkButton, CallToActionCard, Icon, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; function getCardStyles(theme: GrafanaTheme2) { return css({ diff --git a/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx index c0e15131f0d..5c73e85776f 100644 --- a/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraph/NodeGraphContainer.tsx @@ -8,11 +8,11 @@ import { Trans, t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { useStyles2, useTheme2, PanelChrome } from '@grafana/ui'; import { layeredLayoutThreshold } from 'app/plugins/panel/nodeGraph/NodeGraph'; +import { StoreState } from 'app/types/store'; import { NodeGraph } from '../../../plugins/panel/nodeGraph'; import { LayoutAlgorithm } from '../../../plugins/panel/nodeGraph/panelcfg.gen'; import { useCategorizeFrames } from '../../../plugins/panel/nodeGraph/useCategorizeFrames'; -import { StoreState } from '../../../types'; import { useLinks } from '../utils/links'; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx b/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx index abcbc468bd5..67cf1055912 100644 --- a/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx +++ b/public/app/features/explore/QueriesDrawer/QueriesDrawerContext.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useState, createContext, useContext, useEffect } from 'react'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { selectRichHistorySettings } from '../state/selectors'; diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 8acb5e80b71..03989ec2950 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -5,7 +5,7 @@ import { DataSourceApi } from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { configureStore } from 'app/store/configureStore'; -import { ExploreState } from 'app/types'; +import { ExploreState } from 'app/types/explore'; import { UserState } from '../profile/state/reducers'; diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 42b39a20f6b..084c180544f 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'; import { CoreApp, getNextRefId } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { DataQuery, DataSourceRef } from '@grafana/schema'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { getDatasourceSrv } from '../plugins/datasource_srv'; import { QueryEditorRows } from '../query/components/QueryEditorRows'; diff --git a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx index 7efd5f866e9..c1f0ed2aad2 100644 --- a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx +++ b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame, LoadingState } from '@grafana/data'; import { getTemplateSrv } from 'app/features/templating/template_srv'; -import { TABLE_RESULTS_STYLE } from 'app/types'; +import { TABLE_RESULTS_STYLE } from 'app/types/explore'; import { RawPrometheusContainer } from './RawPrometheusContainer'; diff --git a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx index 4269c1d7724..3d0af16edc4 100644 --- a/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx +++ b/public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx @@ -8,8 +8,8 @@ import { TimeZone } from '@grafana/schema'; import { RadioButtonGroup, Table, AdHocFilterItem, PanelChrome } from '@grafana/ui'; import { config } from 'app/core/config'; import { PANEL_BORDER } from 'app/core/constants'; -import { StoreState, TABLE_RESULTS_STYLE } from 'app/types'; -import { ExploreItemState, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore'; +import { ExploreItemState, TABLE_RESULTS_STYLE, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { MetaInfoText } from '../MetaInfoText'; import RawListContainer from '../PrometheusListView/RawListContainer'; @@ -32,12 +32,11 @@ interface PrometheusContainerState { function mapStateToProps(state: StoreState, { exploreId }: RawPrometheusContainerProps) { const explore = state.explore; const item: ExploreItemState = explore.panes[exploreId]!; - const { tableResult, rawPrometheusResult, range, queryResponse } = item; + const { rawPrometheusResult, range, queryResponse } = item; const rawPrometheusFrame: DataFrame[] = rawPrometheusResult ? [rawPrometheusResult] : []; - const result = (tableResult?.length ?? 0) > 0 && rawPrometheusResult ? tableResult : rawPrometheusFrame; const loading = queryResponse.state; - return { loading, tableResult: result, range }; + return { loading, tableResult: rawPrometheusFrame, range }; } const connector = connect(mapStateToProps, {}); diff --git a/public/app/features/explore/ResponseErrorContainer.tsx b/public/app/features/explore/ResponseErrorContainer.tsx index e0724c1a666..8124d2e68d1 100644 --- a/public/app/features/explore/ResponseErrorContainer.tsx +++ b/public/app/features/explore/ResponseErrorContainer.tsx @@ -1,5 +1,5 @@ import { LoadingState } from '@grafana/data'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { ErrorContainer } from './ErrorContainer'; diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index d78a69d14f5..b4fdc2b28e6 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -11,8 +11,8 @@ import { RichHistorySettings, createDatasourcesList, } from 'app/core/utils/richHistory'; -import { useSelector } from 'app/types'; import { RichHistoryQuery } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { Tabs } from '../QueriesDrawer/QueriesDrawerContext'; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index f0a9d5410f3..669f7d0dd4a 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -6,8 +6,8 @@ import { DataSourceApi, DataSourceInstanceSettings, DataSourcePluginMeta } from import { DataQuery, DataSourceRef } from '@grafana/schema'; import { MixedDatasource } from 'app/plugins/datasource/mixed/MixedDataSource'; import { configureStore } from 'app/store/configureStore'; -import { ExploreState, RichHistoryQuery } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { ExploreState, RichHistoryQuery } from 'app/types/explore'; import { RichHistoryCard, Props } from './RichHistoryCard'; diff --git a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx index a08c439aa33..6d622353d38 100644 --- a/public/app/features/explore/RichHistory/RichHistoryContainer.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryContainer.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { useTheme2 } from '@grafana/ui'; // Types -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; // Components, enums import { useQueriesDrawerContext } from '../QueriesDrawer/QueriesDrawerContext'; diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 542f3b3d9e1..51c8f36851b 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -12,8 +12,8 @@ import { RichHistorySearchFilters, RichHistorySettings, } from 'app/core/utils/richHistory'; -import { useSelector } from 'app/types'; import { RichHistoryQuery } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { selectExploreDSMaps } from '../state/selectors'; diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index e1f378bf262..cd9d25b860b 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { ToolbarButton, useTheme2 } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { createDatasourcesList } from '../../core/utils/richHistory'; import { MIXED_DATASOURCE_NAME } from '../../plugins/datasource/mixed/MixedDataSource'; diff --git a/public/app/features/explore/ShortLinkButtonMenu.tsx b/public/app/features/explore/ShortLinkButtonMenu.tsx index 11ba51875e8..6764e007e3b 100644 --- a/public/app/features/explore/ShortLinkButtonMenu.tsx +++ b/public/app/features/explore/ShortLinkButtonMenu.tsx @@ -6,7 +6,7 @@ import { reportInteraction, config } from '@grafana/runtime'; import { Dropdown, Menu, MenuGroup, ButtonGroup, Button } from '@grafana/ui'; import { copyStringToClipboard } from 'app/core/utils/explore'; import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { selectPanes } from './state/selectors'; import { constructAbsoluteUrl } from './utils/links'; diff --git a/public/app/features/explore/Table/TableContainer.tsx b/public/app/features/explore/Table/TableContainer.tsx index 802c5602861..b0a203bd22a 100644 --- a/public/app/features/explore/Table/TableContainer.tsx +++ b/public/app/features/explore/Table/TableContainer.tsx @@ -12,8 +12,8 @@ import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames, } from 'app/plugins/panel/table/migrations'; -import { StoreState } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import { LimitedDataDisclaimer } from '../LimitedDataDisclaimer'; import { MetaInfoText } from '../MetaInfoText'; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 4df8b52f4cf..ecb71ee92a8 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -23,21 +23,19 @@ import { useStyles2 } from '@grafana/ui'; import { TempoQuery } from '@grafana-plugins/tempo/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getTimeZone } from 'app/features/profile/state/selectors'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { changePanelState } from '../state/explorePane'; -import { - SpanBarOptionsData, - SpanLinkFunc, - Trace, - TracePageHeader, - TraceTimelineViewer, - TTraceTimeline, -} from './components'; import memoizedTraceCriticalPath from './components/CriticalPath'; +import { TracePageHeader } from './components/TracePageHeader'; import SpanGraph from './components/TracePageHeader/SpanGraph'; +import TraceTimelineViewer from './components/TraceTimelineViewer'; import { TraceFlameGraphs } from './components/TraceTimelineViewer/SpanDetail'; +import { SpanBarOptionsData } from './components/settings/SpanBarSettings'; +import TTraceTimeline from './components/types/TTraceTimeline'; +import { SpanLinkFunc } from './components/types/links'; +import { Trace } from './components/types/trace'; import { createSpanLinkFactory } from './createSpanLink'; import { useChildrenState } from './useChildrenState'; import { useDetailState } from './useDetailState'; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 5fbcd3ba4a5..9eff3de76f2 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { DataFrame, SplitOpen, TimeRange } from '@grafana/data'; import { t } from '@grafana/i18n'; import { PanelChrome } from '@grafana/ui'; -import { StoreState, useSelector } from 'app/types'; +import { StoreState, useSelector } from 'app/types/store'; import { TraceView } from './TraceView'; import { transformDataFrames } from './utils/transform'; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/index.tsx b/public/app/features/explore/TraceView/components/CriticalPath/index.tsx index 7d8a8d3dc99..6a485972c0e 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/index.tsx +++ b/public/app/features/explore/TraceView/components/CriticalPath/index.tsx @@ -14,7 +14,7 @@ import memoizeOne from 'memoize-one'; -import { CriticalPathSection, Trace, TraceSpan } from '../types'; +import { TraceSpan, CriticalPathSection, Trace } from '../types/trace'; import findLastFinishingChildSpan from './utils/findLastFinishingChildSpan'; import getChildOfSpans from './utils/getChildOfSpans'; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts index d2f0cf757fd..4e37e81ef21 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test1.ts @@ -27,7 +27,9 @@ Here +++++ are critical path sections */ -import { Trace, TraceResponse, transformTraceData } from '../../index'; + +import transformTraceData from '../../model/transform-trace-data'; +import { Trace, TraceResponse } from '../../types/trace'; const testTrace: TraceResponse = { traceID: 'test1-trace', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts index 1f445a9339e..896519af4bd 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test2.ts @@ -29,7 +29,8 @@ | Here ++++++ is critical path | */ -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; const happyTrace: TraceResponse = { traceID: 'trace-123', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts index ab13f8d12fb..04ff9908b09 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test3.ts @@ -26,7 +26,8 @@ Span B will be dropped. | span A is on critical path(+++++) | */ -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; const trace: TraceResponse = { traceID: '006c3cf93508f205', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts index dfab4611889..28d0abfc196 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test4.ts @@ -29,7 +29,8 @@ Both spanB and spanC will be dropped. | span A is on critical path(+++++) | */ -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; const trace: TraceResponse = { traceID: 'trace-abc', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts index 0f057fe60e1..88d35296afd 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test5.ts @@ -28,7 +28,8 @@ Here span B is ref-type is 'FOLLOWS_FROM' | */ -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; const trace: TraceResponse = { traceID: 'trace-abc', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts index 3d35d416f85..faa703c4885 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test6.ts @@ -26,7 +26,8 @@ └──────────────┘ | (parent-child tree) */ -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; const trace: TraceResponse = { traceID: 'trace-abc', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts index ef67a541817..d4256c95888 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test7.ts @@ -26,7 +26,8 @@ └──────────────┘ | */ -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; const trace: TraceResponse = { traceID: 'trace-abc', diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts index 4be57aa68db..068d852c965 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test8.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; /* ┌─────────────────┐ | diff --git a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts index b1e52d18f22..0233d4b721c 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/testCases/test9.ts @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceResponse, transformTraceData } from '../../index'; +import transformTraceData from '../../model/transform-trace-data'; +import { TraceResponse } from '../../types/trace'; /* ┌──────────┐ | diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx b/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx index 21bdc7edd22..b9cb5423f62 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/findLastFinishingChildSpan.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../../types'; +import { TraceSpan } from '../../types/trace'; /** * @returns - Returns the span that finished last among the remaining child spans. diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx b/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx index 850c59c04c6..a42cb801506 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/getChildOfSpans.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../../types'; +import { TraceSpan } from '../../types/trace'; /** * Removes child spans whose refType is FOLLOWS_FROM and their descendants. diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts index c27a6eab732..100de12c3aa 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../../types'; +import { TraceSpan } from '../../types/trace'; import test3 from '../testCases/test3'; import test4 from '../testCases/test4'; import test6 from '../testCases/test6'; diff --git a/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx index 2b0b93eef8e..a52a7d8966f 100644 --- a/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx +++ b/public/app/features/explore/TraceView/components/CriticalPath/utils/sanitizeOverFlowingChildren.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../../types'; +import { TraceSpan } from '../../types/trace'; /** * This function resolves overflowing child spans for each span. diff --git a/public/app/features/explore/TraceView/components/ScrollManager.tsx b/public/app/features/explore/TraceView/components/ScrollManager.tsx index e996b3b5dd3..2190062e190 100644 --- a/public/app/features/explore/TraceView/components/ScrollManager.tsx +++ b/public/app/features/explore/TraceView/components/ScrollManager.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TNil } from './types'; +import TNil from './types/TNil'; import { TraceSpan, TraceSpanReference, Trace } from './types/trace'; /** diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx index 48b0b0f8f69..e1448a77f69 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/Actions/TracePageActions.tsx @@ -88,7 +88,7 @@ export default function TracePageActions(props: TracePageActionsProps) { <ActionButton onClick={copyTraceId} - ariaLabel={'Copy Trace ID'} + ariaLabel={t('explore.trace-page-actions.ariaLabel-copy-trace-id', 'Copy Trace ID')} label={ copyTraceIdClicked ? t('explore.trace-page-actions.label-copied', 'Copied!') @@ -98,7 +98,7 @@ export default function TracePageActions(props: TracePageActionsProps) { /> <ActionButton onClick={exportTrace} - ariaLabel={'Export Trace'} + ariaLabel={t('explore.trace-page-actions.ariaLabel-export-trace', 'Export Trace')} label={t('explore.trace-page-actions.label-export', 'Export')} icon={'save'} /> diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx index f16b2cc2fbf..44453f8dd1b 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx @@ -23,7 +23,7 @@ import { config, reportInteraction } from '@grafana/runtime'; import { Icon, PopoverContent, Tooltip, useTheme2 } from '@grafana/ui'; import { getButtonStyles } from '@grafana/ui/internal'; -import { Trace } from '../../types'; +import { Trace } from '../../types/trace'; export type NextPrevResultProps = { trace: Trace; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx index 8889408b7eb..954b57cb039 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx @@ -21,7 +21,7 @@ import { Button, Switch, useStyles2 } from '@grafana/ui'; import { getButtonStyles } from '@grafana/ui/internal'; import { SearchProps } from '../../../useSearch'; -import { Trace } from '../../types'; +import { Trace } from '../../types/trace'; import { convertTimeFilter } from '../../utils/filter-spans'; import NextPrevResult from './NextPrevResult'; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx index 7cb9997fcc8..0708a574c17 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx @@ -23,7 +23,7 @@ import { Collapse, Icon, InlineField, InlineFieldRow, Select, Stack, Tooltip, us import { defaultFilters, SearchProps } from '../../../useSearch'; import { getTraceServiceNames, getTraceSpanNames } from '../../../utils/tags'; import SearchBarInput from '../../common/SearchBarInput'; -import { Trace } from '../../types'; +import { Trace } from '../../types/trace'; import NextPrevResult from '../SearchBar/NextPrevResult'; import TracePageSearchBar from '../SearchBar/TracePageSearchBar'; @@ -211,7 +211,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => { /> <div className={styles.intervalInput}> <IntervalInput - ariaLabel="Select min span duration" + ariaLabel={t('explore.span-filters.ariaLabel-select-min-span-duration', 'Select min span duration')} onChange={(val) => setSpanFiltersSearch({ ...search, from: val })} isInvalidError="Invalid duration" // eslint-disable-next-line @grafana/i18n/no-untranslated-strings @@ -228,7 +228,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => { value={search.toOperator} /> <IntervalInput - ariaLabel="Select max span duration" + ariaLabel={t('explore.span-filters.ariaLabel-select-max-span-duration', 'Select max span duration')} onChange={(val) => setSpanFiltersSearch({ ...search, to: val })} isInvalidError="Invalid duration" // eslint-disable-next-line @grafana/i18n/no-untranslated-strings diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx index a5ffb81d12c..ce36acf89ea 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx @@ -9,7 +9,7 @@ import { Input, Select, Stack, useStyles2 } from '@grafana/ui'; import { randomId, SearchProps, Tag } from '../../../useSearch'; import { getTraceTagKeys, getTraceTagValues } from '../../../utils/tags'; -import { Trace } from '../../types'; +import { Trace } from '../../types/trace'; interface Props { search: SearchProps; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx index 6173ac3534d..dd638f44f8a 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx @@ -19,7 +19,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { withTheme2, stylesFactory } from '@grafana/ui'; import { autoColor } from '../../Theme'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import { getRgbColorByKey } from '../../utils/color-generator'; import renderIntoCanvas from './render-into-canvas'; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx index 4cdf8088c1c..ac031caa160 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/ViewingLayer.tsx @@ -21,8 +21,11 @@ import { Trans } from '@grafana/i18n'; import { withTheme2, stylesFactory, Button } from '@grafana/ui'; import { autoColor } from '../../Theme'; -import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate, TNil } from '../../index'; -import DraggableManager, { DraggableBounds, DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; +import { TUpdateViewRangeTimeFunction, ViewRangeTimeUpdate, ViewRange } from '../../TraceTimelineViewer/types'; +import TNil from '../../types/TNil'; +import DraggableManager from '../../utils/DraggableManager/DraggableManager'; +import EUpdateTypes from '../../utils/DraggableManager/EUpdateTypes'; +import { DraggableBounds, DraggingUpdate } from '../../utils/DraggableManager/types'; import GraphTicks from './GraphTicks'; import Scrubber from './Scrubber'; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx index 64595920cf5..78a4c151f88 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx @@ -16,8 +16,8 @@ import { css } from '@emotion/css'; import memoizeOne from 'memoize-one'; import * as React from 'react'; -import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '../../index'; -import { TraceSpan, Trace } from '../../types'; +import { ViewRange, TUpdateViewRangeTimeFunction, ViewRangeTimeUpdate } from '../../TraceTimelineViewer/types'; +import { Trace, TraceSpan } from '../../types/trace'; import CanvasSpanGraph from './CanvasSpanGraph'; import TickLabels from './TickLabels'; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx index e339120e0fe..fc23e53f517 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/render-into-canvas.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TNil } from '../../index'; +import TNil from '../../types/TNil'; // exported for tests export const ITEM_ALPHA = 0.8; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index 504c4782a84..076edfccc46 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -27,7 +27,7 @@ import ExternalLinks from '../common/ExternalLinks'; import TraceName from '../common/TraceName'; import { getTraceLinks } from '../model/link-patterns'; import { getHeaderTags, getTraceName } from '../model/trace-viewer'; -import { Trace } from '../types'; +import { Trace } from '../types/trace'; import { formatDuration } from '../utils/date'; import TracePageActions from './Actions/TracePageActions'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx index 91a57172395..7fcbe3553bd 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx @@ -14,7 +14,7 @@ import * as React from 'react'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import Positions from './Positions'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx index 03663851799..0e447aa0ce0 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx @@ -25,7 +25,8 @@ import { Tooltip, useStyles2 } from '@grafana/ui'; import { autoColor } from '../Theme'; import { Popover } from '../common/Popover'; -import { TraceSpan, TNil, CriticalPathSection } from '../types'; +import TNil from '../types/TNil'; +import { TraceSpan, CriticalPathSection } from '../types/trace'; import AccordianLogs from './SpanDetail/AccordianLogs'; import { ViewedBoundsFunctionType } from './utils'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx index ea39758763f..39262e91b21 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx @@ -17,7 +17,8 @@ import userEvent from '@testing-library/user-event'; import { DURATION, NONE, TAG } from '@grafana/o11y-ds-frontend'; -import { SpanLinkDef, TraceSpan } from '../types'; +import { SpanLinkDef } from '../types/links'; +import { TraceSpan } from '../types/trace'; import SpanBarRow, { SpanBarRowProps } from './SpanBarRow'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx index 09db59d0e01..c01bbbe4abb 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx @@ -21,7 +21,10 @@ import { DURATION, NONE, TAG } from '@grafana/o11y-ds-frontend'; import { Icon, stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; -import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil, CriticalPathSection } from '../types'; +import { SpanBarOptions } from '../settings/SpanBarSettings'; +import TNil from '../types/TNil'; +import { SpanLinkFunc } from '../types/links'; +import { TraceSpan, CriticalPathSection } from '../types/trace'; import SpanBar from './SpanBar'; import { SpanLinksMenu } from './SpanLinks'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx index 1932b8d2c96..b182fa5df41 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx @@ -20,7 +20,7 @@ import { GrafanaTheme2, TraceKeyValuePair } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import * as markers from './AccordianKeyValues.markers'; import KeyValuesTable, { KeyValuesTableLink } from './KeyValuesTable'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx index 7e331151c99..f0efc64b10a 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx @@ -20,7 +20,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import { getStyles as getAccordianKeyValuesStyles } from './AccordianKeyValues'; import TextList from './TextList'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index 60c00367373..83632c9351d 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -21,7 +21,7 @@ import { Icon, useStyles2 } from '@grafana/ui'; import { autoColor } from '../../Theme'; import CopyIcon from '../../common/CopyIcon'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import jsonMarkup from './jsonMarkup'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx index 1e210dd0508..36810a370df 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx @@ -9,8 +9,7 @@ import { DataLinkButton, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; -import { SpanLinkFunc } from '../../types'; -import { SpanLinkDef, SpanLinkType } from '../../types/links'; +import { SpanLinkDef, SpanLinkFunc, SpanLinkType } from '../../types/links'; import { TraceSpan } from '../../types/trace'; export type ProfilesButtonContext = { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 815f5f68974..0b8065b09c6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -39,7 +39,7 @@ import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import { autoColor } from '../../Theme'; import LabeledList from '../../common/LabeledList'; import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span'; -import { SpanLinkFunc } from '../../types'; +import { SpanLinkFunc } from '../../types/links'; import { TraceProcess, TraceSpan, TraceSpanReference } from '../../types/trace'; import { formatDuration } from '../utils'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx index 051fecb2e6b..a6e8219214d 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx @@ -22,7 +22,7 @@ import { TimeZone } from '@grafana/schema'; import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; -import { SpanLinkFunc } from '../types'; +import { SpanLinkFunc } from '../types/links'; import { TraceSpan, TraceSpanReference } from '../types/trace'; import SpanDetail, { TraceFlameGraphs } from './SpanDetail'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx index 9994b36dd37..463b2035841 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.test.tsx @@ -17,7 +17,7 @@ import userEvent from '@testing-library/user-event'; import { createTheme } from '@grafana/data'; -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import spanAncestorIdsSpy from '../utils/span-ancestor-ids'; import SpanTreeOffset, { getStyles, TProps } from './SpanTreeOffset'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx index 786edc7a183..72a985faad4 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx @@ -21,7 +21,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Icon, stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import spanAncestorIds from '../utils/span-ancestor-ids'; export const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx index 8c1341b3193..81c255bcba6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx @@ -20,7 +20,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { autoColor } from '../Theme'; -import { TNil } from '../types'; +import TNil from '../types/TNil'; import { formatDuration } from './utils'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx index 91b3365f695..4fb9b184dcc 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx @@ -16,8 +16,9 @@ import { css } from '@emotion/css'; import cx from 'classnames'; import * as React from 'react'; -import { TNil } from '../../types'; -import DraggableManager, { DraggableBounds, DraggingUpdate } from '../../utils/DraggableManager'; +import TNil from '../../types/TNil'; +import DraggableManager from '../../utils/DraggableManager/DraggableManager'; +import { DraggableBounds, DraggingUpdate } from '../../utils/DraggableManager/types'; export const getStyles = () => ({ TimelineColumnResizer: css({ diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx index f7ec95ced7b..0afaeaa0ce2 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx @@ -17,8 +17,9 @@ import * as React from 'react'; import { stylesFactory } from '@grafana/ui'; -import { TNil } from '../../types'; -import DraggableManager, { DraggableBounds, DraggingUpdate } from '../../utils/DraggableManager'; +import TNil from '../../types/TNil'; +import DraggableManager from '../../utils/DraggableManager/DraggableManager'; +import { DraggableBounds, DraggingUpdate } from '../../utils/DraggableManager/types'; import { TUpdateViewRangeTimeFunction, ViewRangeTime, ViewRangeTimeUpdate } from '../types'; // exported for testing diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx index fd93ab261f2..74d25ed8c1f 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.test.tsx @@ -15,7 +15,7 @@ import { render, screen } from '@testing-library/react'; import traceGenerator from '../demo/trace-generators'; import transformTraceData from '../model/transform-trace-data'; -import { Trace } from '../types'; +import { Trace } from '../types/trace'; import { formatDuration } from '../utils/date'; import SpanTreeOffset from './SpanTreeOffset'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx index 2e38d68ebcf..23113a16c24 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -26,9 +26,11 @@ import { TimeZone } from '@grafana/schema'; import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; import { PEER_SERVICE } from '../constants/tag-keys'; -import { CriticalPathSection, SpanBarOptions, SpanLinkFunc, TNil } from '../types'; +import { SpanBarOptions } from '../settings/SpanBarSettings'; +import TNil from '../types/TNil'; import TTraceTimeline from '../types/TTraceTimeline'; -import { TraceSpan, Trace, TraceSpanReference } from '../types/trace'; +import { SpanLinkFunc } from '../types/links'; +import { TraceSpan, Trace, TraceSpanReference, CriticalPathSection } from '../types/trace'; import { getColorByKey } from '../utils/color-generator'; import ListView from './ListView'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx index e2840a15db2..4d475aa8430 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/index.tsx @@ -23,9 +23,10 @@ import { stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; import { merge as mergeShortcuts } from '../keyboard-shortcuts'; -import { CriticalPathSection, SpanLinkFunc, TNil } from '../types'; +import TNil from '../types/TNil'; import TTraceTimeline from '../types/TTraceTimeline'; -import { TraceSpan, Trace, TraceSpanReference } from '../types/trace'; +import { SpanLinkFunc } from '../types/links'; +import { TraceSpan, Trace, TraceSpanReference, CriticalPathSection } from '../types/trace'; import { TraceFlameGraphs } from './SpanDetail'; import TimelineHeaderRow from './TimelineHeaderRow'; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/types.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/types.tsx index c427b0ecd3d..c4038f337f6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/types.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/types.tsx @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TNil } from '../types'; - +import TNil from '../types/TNil'; interface TimeCursorUpdate { cursor: number | TNil; } diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts index 9306391774f..a3a634b79d3 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts @@ -13,7 +13,7 @@ // limitations under the License. import traceGenerator from '../demo/trace-generators'; -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import { findServerChildSpan, diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx index 1fa55b1d957..3a03cbffdf6 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; export type ViewedBoundsFunctionType = (start: number, end: number) => { start: number; end: number }; /** diff --git a/public/app/features/explore/TraceView/components/Tween.tsx b/public/app/features/explore/TraceView/components/Tween.tsx index e4c3b8271e7..0f839636b3d 100644 --- a/public/app/features/explore/TraceView/components/Tween.tsx +++ b/public/app/features/explore/TraceView/components/Tween.tsx @@ -14,7 +14,7 @@ import ease from 'tween-functions'; -import { TNil } from './types'; +import TNil from './types/TNil'; export interface TweenState { done: boolean; diff --git a/public/app/features/explore/TraceView/components/common/TraceName.tsx b/public/app/features/explore/TraceView/components/common/TraceName.tsx index 128a297d7eb..0c17a814b87 100644 --- a/public/app/features/explore/TraceView/components/common/TraceName.tsx +++ b/public/app/features/explore/TraceView/components/common/TraceName.tsx @@ -19,7 +19,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { FALLBACK_TRACE_NAME } from '../constants'; -import { TNil } from '../types'; +import TNil from '../types/TNil'; import BreakableText from './BreakableText'; diff --git a/public/app/features/explore/TraceView/components/index.ts b/public/app/features/explore/TraceView/components/index.ts deleted file mode 100644 index 51b22738e75..00000000000 --- a/public/app/features/explore/TraceView/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { default as TraceTimelineViewer } from './TraceTimelineViewer'; -export { TracePageHeader } from './TracePageHeader'; -export { default as SpanBarSettings } from './settings/SpanBarSettings'; -export * from './types'; -export * from './TraceTimelineViewer/types'; -export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState'; -export { default as transformTraceData } from './model/transform-trace-data'; -export { filterSpans } from './utils/filter-spans'; -export * from './Theme'; diff --git a/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts b/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts index 8dedcd277a9..68d6b60cbe7 100644 --- a/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts +++ b/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import { getHeaderTags, _getTraceNameImpl as getTraceName } from './trace-viewer'; diff --git a/public/app/features/explore/TraceView/components/model/link-patterns.test.ts b/public/app/features/explore/TraceView/components/model/link-patterns.test.ts index e3d5a98ffe0..2e9ca895b4c 100644 --- a/public/app/features/explore/TraceView/components/model/link-patterns.test.ts +++ b/public/app/features/explore/TraceView/components/model/link-patterns.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Trace } from '../types'; +import { Trace } from '../types/trace'; import { processTemplate, diff --git a/public/app/features/explore/TraceView/components/model/link-patterns.tsx b/public/app/features/explore/TraceView/components/model/link-patterns.tsx index 8964b969a0d..2c73aae342e 100644 --- a/public/app/features/explore/TraceView/components/model/link-patterns.tsx +++ b/public/app/features/explore/TraceView/components/model/link-patterns.tsx @@ -15,7 +15,7 @@ import { uniq as _uniq } from 'lodash'; import memoize from 'lru-memoize'; -import { Trace } from '../types'; +import { Trace } from '../types/trace'; import { getConfigValue } from '../utils/config/get-config'; const parameterRegExp = /#\{([^{}]*)\}/g; diff --git a/public/app/features/explore/TraceView/components/model/trace-viewer.test.ts b/public/app/features/explore/TraceView/components/model/trace-viewer.test.ts index 2895b6c0013..0eaafaaa596 100644 --- a/public/app/features/explore/TraceView/components/model/trace-viewer.test.ts +++ b/public/app/features/explore/TraceView/components/model/trace-viewer.test.ts @@ -1,4 +1,4 @@ -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import { findHeaderTags } from './trace-viewer'; diff --git a/public/app/features/explore/TraceView/components/model/trace-viewer.ts b/public/app/features/explore/TraceView/components/model/trace-viewer.ts index 14a359a33b9..0a3b2533bb9 100644 --- a/public/app/features/explore/TraceView/components/model/trace-viewer.ts +++ b/public/app/features/explore/TraceView/components/model/trace-viewer.ts @@ -14,7 +14,7 @@ import { memoize } from 'lodash'; -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; export function _getTraceNameImpl(spans: TraceSpan[]) { // Use a span with no references to another span in given array diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts index 53d3f3b696d..fda614cc40c 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceResponse } from '../types'; +import { TraceResponse } from '../types/trace'; import transformTraceData, { orderTags, deduplicateTags } from './transform-trace-data'; diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx index 8220af49732..f85f99f8e0c 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx @@ -18,7 +18,7 @@ import { isEqual as _isEqual } from 'lodash'; import { TraceKeyValuePair } from '@grafana/data'; import { getTraceSpanIdsAsTree } from '../selectors/trace'; -import { TraceSpan, Trace, TraceResponse, TraceProcess } from '../types'; +import { TraceResponse, Trace, TraceSpan, TraceProcess } from '../types/trace'; // @ts-ignore import TreeNode from '../utils/TreeNode'; import { getConfigValue } from '../utils/config/get-config'; diff --git a/public/app/features/explore/TraceView/components/selectors/span.test.ts b/public/app/features/explore/TraceView/components/selectors/span.test.ts index 89ebd46732c..a805c45891e 100644 --- a/public/app/features/explore/TraceView/components/selectors/span.test.ts +++ b/public/app/features/explore/TraceView/components/selectors/span.test.ts @@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceResponse } from 'app/features/explore/TraceView/components/types'; - import traceGenerator from '../demo/trace-generators'; +import { TraceResponse } from '../types/trace'; import * as spanSelectors from './span'; diff --git a/public/app/features/explore/TraceView/components/types/index.tsx b/public/app/features/explore/TraceView/components/types/index.tsx deleted file mode 100644 index e91e53e601b..00000000000 --- a/public/app/features/explore/TraceView/components/types/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -export type { TraceSpan, TraceResponse, Trace, TraceProcess, TraceLink, CriticalPathSection } from './trace'; -export type { SpanBarOptions, SpanBarOptionsData } from '../settings/SpanBarSettings'; -export type { default as TTraceTimeline } from './TTraceTimeline'; -export type { default as TNil } from './TNil'; -export type { SpanLinkFunc, SpanLinkDef } from './links'; diff --git a/public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.tsx index 44a0820a011..242edfbc161 100644 --- a/public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/DraggableManager.tsx @@ -14,7 +14,7 @@ import { get as _get } from 'lodash'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import EUpdateTypes from './EUpdateTypes'; import { DraggableBounds, DraggingUpdate } from './types'; diff --git a/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx index 835573a5fa5..ff9a9f6a0b1 100644 --- a/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DividerDemo.tsx @@ -16,7 +16,7 @@ import { PureComponent } from 'react'; import TNil from '../../../types/TNil'; import DraggableManager from '../DraggableManager'; -import { DraggableBounds, DraggingUpdate } from '../index'; +import { DraggableBounds, DraggingUpdate } from '../types'; import './DividerDemo.css'; diff --git a/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx index 5a524ddf53a..0d4c5dde159 100644 --- a/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/DraggableManagerDemo.tsx @@ -16,7 +16,7 @@ import { PureComponent } from 'react'; import { Trans } from '@grafana/i18n'; -import { TNil } from '../../../types'; +import TNil from '../../../types/TNil'; import DividerDemo from './DividerDemo'; import RegionDemo from './RegionDemo'; diff --git a/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx index 793428fcce5..e92f883060b 100644 --- a/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/demo/RegionDemo.tsx @@ -14,8 +14,9 @@ import { PureComponent } from 'react'; -import { TNil } from '../../../types'; -import DraggableManager, { DraggableBounds, DraggingUpdate } from '../index'; +import TNil from '../../../types/TNil'; +import DraggableManager from '../DraggableManager'; +import { DraggableBounds, DraggingUpdate } from '../types'; import './RegionDemo.css'; diff --git a/public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx deleted file mode 100644 index 150607a3925..00000000000 --- a/public/app/features/explore/TraceView/components/utils/DraggableManager/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// 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. - -export * from './types'; -export { default as EUpdateTypes } from './EUpdateTypes'; -export { default } from './DraggableManager'; diff --git a/public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx b/public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx index 79e213f8d7e..dfcd0abefa6 100644 --- a/public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx +++ b/public/app/features/explore/TraceView/components/utils/DraggableManager/types.tsx @@ -14,7 +14,7 @@ import * as React from 'react'; -import { TNil } from '../../types'; +import TNil from '../../types/TNil'; import DraggableManager from './DraggableManager'; import EUpdateTypes from './EUpdateTypes'; diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts index 4bae04a74a2..833ee607c3a 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts @@ -13,7 +13,7 @@ // limitations under the License. import { defaultFilters, defaultTagFilter } from '../../useSearch'; -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import { filterSpans } from './filter-spans'; diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx index 31eaf008d67..82664ded026 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx @@ -18,7 +18,8 @@ import { TraceKeyValuePair } from '@grafana/data'; import { SearchProps, Tag } from '../../useSearch'; import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../constants/span'; -import { TNil, TraceSpan } from '../types'; +import TNil from '../types/TNil'; +import { TraceSpan } from '../types/trace'; // filter spans where all filters added need to be true for each individual span that is returned // i.e. the more filters added -> the more specific that the returned results are diff --git a/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts index d8532b74151..2a9d2c6de48 100644 --- a/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts +++ b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { TraceSpan } from '../types'; +import { TraceSpan } from '../types/trace'; import spanAncestorIdsSpy from './span-ancestor-ids'; diff --git a/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx index 5ddb037aa2a..af9ac5282e0 100644 --- a/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx +++ b/public/app/features/explore/TraceView/components/utils/span-ancestor-ids.tsx @@ -14,7 +14,8 @@ import { find as _find, get as _get } from 'lodash'; -import { TNil, TraceSpan } from '../types'; +import TNil from '../types/TNil'; +import { TraceSpan } from '../types/trace'; function getFirstAncestor(span: TraceSpan): TraceSpan | TNil { return _get( diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 6d2ee26d684..7574434861c 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -14,8 +14,8 @@ import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv'; import { TemplateSrv } from '../../templating/template_srv'; -import { Trace, TraceSpan } from './components'; import { SpanLinkType } from './components/types/links'; +import { Trace, TraceSpan } from './components/types/trace'; import { createSpanLinkFactory, pyroscopeProfileIdTagKey } from './createSpanLink'; const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace; @@ -1515,6 +1515,91 @@ describe('createSpanLinkFactory', () => { expect(decodeURIComponent(links![0].href)).toContain('spanName=\\"operation\\"'); }); }); + + describe('should return victorialogs link', () => { + const victoriaLogsUID = 'victoriaLogsUID'; + + beforeAll(() => { + setDataSourceSrv({ + getInstanceSettings() { + return { + uid: victoriaLogsUID, + name: 'VictoriaLogs', + type: 'victoriametrics-logs-datasource', + } as unknown as DataSourceInstanceSettings; + }, + } as unknown as DataSourceSrv); + + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + }); + + it('with default keys when tags not configured', () => { + const createLink = setupSpanLinkFactory({}, victoriaLogsUID); + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"cluster:=\\"cluster1\\" AND hostname:=\\"hostname1\\" AND service_namespace:=\\"namespace1\\"","refId":""}]}' + )}` + ); + }); + + it('formats query correctly if filterByTraceID and filterBySpanID is true', () => { + const createLink = setupSpanLinkFactory( + { + datasourceUid: victoriaLogsUID, + filterByTraceID: true, + filterBySpanID: true, + }, + victoriaLogsUID + ); + + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"span_id:=\\"6605c7b08e715d6c\\" AND trace_id:=\\"7946b05c2e2e4e5a\\" AND cluster:=\\"cluster1\\" AND hostname:=\\"hostname1\\" AND service_namespace:=\\"namespace1\\"","refId":""}]}' + )}` + ); + }); + + it('should format multiple tags correctly', () => { + const createLink = setupSpanLinkFactory( + { + tags: [{ key: 'ip' }, { key: 'hostname' }], + }, + victoriaLogsUID + ); + + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'hostname', value: 'hostname1' }, + { key: 'ip', value: '192.168.0.1' }, + ], + }, + }) + ); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"hostname:=\\"hostname1\\" AND ip:=\\"192.168.0.1\\"","refId":""}]}' + )}` + ); + }); + }); }); describe('dataFrame links', () => { diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 7059ebac9b3..68b2dba6485 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -28,9 +28,8 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { LokiQuery } from '../../../plugins/datasource/loki/types'; import { ExploreFieldLinkModel, getFieldLinksForExplore, getVariableUsageInfo } from '../utils/links'; -import { SpanLinkDef, SpanLinkFunc, Trace, TraceSpan } from './components'; -import { SpanLinkType } from './components/types/links'; -import { TraceSpanReference } from './components/types/trace'; +import { SpanLinkDef, SpanLinkFunc, SpanLinkType } from './components/types/links'; +import { Trace, TraceSpan, TraceSpanReference } from './components/types/trace'; /** * This is a factory for the link creator. It returns the function mainly so it can return undefined in which case @@ -197,6 +196,13 @@ function legacyCreateSpanLinkFactory( case 'googlecloud-logging-datasource': tags = getFormattedTags(span, tagsToUse, { joinBy: ' AND ' }); query = getQueryForGoogleCloudLogging(span, traceToLogsOptions, tags, customQuery); + break; + case 'victoriametrics-logs-datasource': + // Build tag selector using strict equality (":=") required by LogsQL + // See https://docs.victoriametrics.com/victorialogs/logsql/#exact-filter + tags = getFormattedTags(span, tagsToUse, { labelValueSign: ':=', joinBy: ' AND ' }); + query = getQueryForVictoriaLogs(span, traceToLogsOptions, tags, customQuery); + break; } // query can be false in case the simple UI tag mapping is used but none of them are present in the span. @@ -569,6 +575,45 @@ function getQueryForFalconLogScale(span: TraceSpan, options: TraceToLogsOptionsV }; } +/** + * Builds a LogsQL expression for victoria‑metrics‑logs‑datasource. + * Uses := for exact‑match filters and joins parts with AND. + * See https://docs.victoriametrics.com/victorialogs/logsql/#exact-filter + */ +function getQueryForVictoriaLogs(span: TraceSpan, options: TraceToLogsOptionsV2, tags: string, customQuery?: string) { + const { filterByTraceID, filterBySpanID } = options; + + // Custom user query has priority + if (customQuery) { + return { + expr: customQuery, + refId: '', + }; + } + + const parts: string[] = []; + + if (filterBySpanID && span.spanID) { + parts.push('span_id:="${__span.spanId}"'); + } + if (filterByTraceID && span.traceID) { + parts.push('trace_id:="${__span.traceId}"'); + } + if (tags) { + parts.push('${__tags}'); + } + + // Nothing to match against – do not create the link + if (!parts.length) { + return undefined; + } + + return { + expr: parts.join(' AND '), + refId: '', + }; +} + /** * Creates a string representing all the tags already formatted for use in the query. The tags are filtered so that * only intersection of tags that exist in a span and tags that you want are serialized into the string. diff --git a/public/app/features/explore/TraceView/useChildrenState.test.ts b/public/app/features/explore/TraceView/useChildrenState.test.ts index 7b17d67eaaf..ff14e7f9a01 100644 --- a/public/app/features/explore/TraceView/useChildrenState.test.ts +++ b/public/app/features/explore/TraceView/useChildrenState.test.ts @@ -1,6 +1,6 @@ import { renderHook, act } from '@testing-library/react'; -import { TraceSpan } from './components'; +import { TraceSpan } from './components/types/trace'; import { useChildrenState } from './useChildrenState'; describe('useChildrenState', () => { diff --git a/public/app/features/explore/TraceView/useChildrenState.ts b/public/app/features/explore/TraceView/useChildrenState.ts index 1bfca3611d6..7b9c34070eb 100644 --- a/public/app/features/explore/TraceView/useChildrenState.ts +++ b/public/app/features/explore/TraceView/useChildrenState.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; -import { TraceSpan } from './components'; +import { TraceSpan } from './components/types/trace'; /** * Children state means whether spans are collapsed or not. Also provides some functions to manipulate that state. diff --git a/public/app/features/explore/TraceView/useDetailState.ts b/public/app/features/explore/TraceView/useDetailState.ts index 48414154995..aea1548d7b4 100644 --- a/public/app/features/explore/TraceView/useDetailState.ts +++ b/public/app/features/explore/TraceView/useDetailState.ts @@ -2,7 +2,7 @@ import { useCallback, useState, useEffect } from 'react'; import { DataFrame, TraceLog } from '@grafana/data'; -import { DetailState } from './components'; +import DetailState from './components/TraceTimelineViewer/SpanDetail/DetailState'; import { TraceSpanReference } from './components/types/trace'; /** * Keeps state of the span detail. This means whether span details are open but also state of each detail subitem diff --git a/public/app/features/explore/TraceView/useSearch.test.ts b/public/app/features/explore/TraceView/useSearch.test.ts index 138710c8b79..0379aabba75 100644 --- a/public/app/features/explore/TraceView/useSearch.test.ts +++ b/public/app/features/explore/TraceView/useSearch.test.ts @@ -1,6 +1,6 @@ import { act, renderHook } from '@testing-library/react'; -import { TraceSpan } from './components'; +import { TraceSpan } from './components/types/trace'; import { defaultFilters, useSearch } from './useSearch'; describe('useSearch', () => { diff --git a/public/app/features/explore/TraceView/useSearch.ts b/public/app/features/explore/TraceView/useSearch.ts index 834dbfb0714..779a318fec2 100644 --- a/public/app/features/explore/TraceView/useSearch.ts +++ b/public/app/features/explore/TraceView/useSearch.ts @@ -4,7 +4,8 @@ import { v4 as uuidv4 } from 'uuid'; import { InterpolateFunction } from '@grafana/data'; -import { filterSpans, TraceSpan } from './components'; +import { TraceSpan } from './components/types/trace'; +import { filterSpans } from './components/utils/filter-spans'; export interface SearchProps { serviceName?: string; diff --git a/public/app/features/explore/TraceView/useViewRange.ts b/public/app/features/explore/TraceView/useViewRange.ts index f577beb0b38..54cb8397a72 100644 --- a/public/app/features/explore/TraceView/useViewRange.ts +++ b/public/app/features/explore/TraceView/useViewRange.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; -import { ViewRangeTimeUpdate, ViewRange } from './components'; +import { ViewRange, ViewRangeTimeUpdate } from './components/TraceTimelineViewer/types'; /** * Controls state of the zoom function that can be used through minimap in header or on the timeline. ViewRange contains diff --git a/public/app/features/explore/TraceView/utils/tags.ts b/public/app/features/explore/TraceView/utils/tags.ts index fb3670573ab..293326e359e 100644 --- a/public/app/features/explore/TraceView/utils/tags.ts +++ b/public/app/features/explore/TraceView/utils/tags.ts @@ -1,7 +1,6 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { uniq } from 'lodash'; -import { Trace } from '../components'; import { ID, KIND, @@ -11,6 +10,7 @@ import { STATUS_MESSAGE, TRACE_STATE, } from '../components/constants/span'; +import { Trace } from '../components/types/trace'; export const getTraceServiceNames = (trace: Trace) => { const serviceNames = trace.spans.map((span) => { diff --git a/public/app/features/explore/TraceView/utils/transform.ts b/public/app/features/explore/TraceView/utils/transform.ts index f731ac653da..4688bc4b94b 100644 --- a/public/app/features/explore/TraceView/utils/transform.ts +++ b/public/app/features/explore/TraceView/utils/transform.ts @@ -1,6 +1,7 @@ import { DataFrame, DataFrameView, TraceSpanRow } from '@grafana/data'; -import { Trace, TraceProcess, TraceResponse, transformTraceData } from '../components'; +import transformTraceData from '../components/model/transform-trace-data'; +import { Trace, TraceProcess, TraceResponse } from '../components/types/trace'; export function transformDataFrames(frame?: DataFrame): Trace | null { if (!frame) { diff --git a/public/app/features/explore/correlationEditLogic.test.ts b/public/app/features/explore/correlationEditLogic.test.ts index dd3c37e95c6..fa80b340226 100644 --- a/public/app/features/explore/correlationEditLogic.test.ts +++ b/public/app/features/explore/correlationEditLogic.test.ts @@ -1,4 +1,4 @@ -import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types/explore'; import { showModalMessage } from './correlationEditLogic'; diff --git a/public/app/features/explore/correlationEditLogic.ts b/public/app/features/explore/correlationEditLogic.ts index 326389505c7..0e507453306 100644 --- a/public/app/features/explore/correlationEditLogic.ts +++ b/public/app/features/explore/correlationEditLogic.ts @@ -1,6 +1,6 @@ import { template } from 'lodash'; -import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types/explore'; enum CONSEQUENCES { SOURCE_TARGET_CHANGE = 'cause the query in the right pane to be re-ran and links added to that data', diff --git a/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx b/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx index 87dbc64f5ac..a3c5f1a22fb 100644 --- a/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/ExploreToDashboardPanel.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { AddToDashboardForm } from 'app/features/dashboard-scene/addToDashboard/AddToDashboardForm'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getExploreItemSelector } from '../../state/selectors'; diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts index 1c7c8912a17..2f42709c2b0 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.test.ts @@ -1,6 +1,6 @@ import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data'; import { DataQuery, LoadingState } from '@grafana/schema'; -import { ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types/explore'; import { buildDashboardPanelFromExploreState } from './addToDashboard'; diff --git a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts index ac2e9cca741..ea78a8f81ff 100644 --- a/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/extensions/AddToDashboard/addToDashboard.ts @@ -2,7 +2,7 @@ import { DataFrame, ExplorePanelsState } from '@grafana/data'; import { t } from '@grafana/i18n'; import { DataQuery, DataSourceRef, Panel } from '@grafana/schema'; import { DataTransformerConfig } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen'; -import { ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types/explore'; interface ExploreToDashboardPanelOptions { queries: DataQuery[]; diff --git a/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts b/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts index d528956cb9f..ed90ef5aff7 100644 --- a/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts +++ b/public/app/features/explore/extensions/AddToDashboard/getAddToDashboardTitle.ts @@ -1,5 +1,5 @@ import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export function getAddToDashboardTitle(): string { const canCreateDashboard = contextSrv.hasPermission(AccessControlAction.DashboardsCreate); diff --git a/public/app/features/explore/extensions/AddToDashboard/index.test.tsx b/public/app/features/explore/extensions/AddToDashboard/index.test.tsx index 79a51c929cb..32d84a9466b 100644 --- a/public/app/features/explore/extensions/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/index.test.tsx @@ -8,7 +8,7 @@ import { DataQuery } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { Echo } from 'app/core/services/echo/Echo'; import { configureStore } from 'app/store/configureStore'; -import { ExploreState } from 'app/types'; +import { ExploreState } from 'app/types/explore'; import { createEmptyQueryResponse } from '../../state/utils'; diff --git a/public/app/features/explore/extensions/AddToDashboard/index.tsx b/public/app/features/explore/extensions/AddToDashboard/index.tsx index 32277343c17..ef3278a58e9 100644 --- a/public/app/features/explore/extensions/AddToDashboard/index.tsx +++ b/public/app/features/explore/extensions/AddToDashboard/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import { t } from '@grafana/i18n'; import { Modal, ToolbarButton } from '@grafana/ui'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { getExploreItemSelector } from '../../state/selectors'; diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx index 3adcc8ca6fb..0a72d621269 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx @@ -8,7 +8,7 @@ import { usePluginLinks } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; -import { ExplorePanelData, ExploreState } from 'app/types'; +import { ExplorePanelData, ExploreState } from 'app/types/explore'; import { createEmptyQueryResponse } from '../state/utils'; diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx index df78195ab3d..3c3a5f2286a 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx @@ -4,7 +4,9 @@ import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange, getTimeZ import { config, reportInteraction, usePluginLinks } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, ExplorePanelData, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ExplorePanelData } from 'app/types/explore'; +import { useSelector } from 'app/types/store'; import { getExploreItemSelector, isLeftPaneSelector, selectCorrelationDetails } from '../state/selectors'; diff --git a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx index 9be348955cf..7bbade45ad9 100644 --- a/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx +++ b/public/app/features/explore/extensions/getExploreExtensionConfigs.tsx @@ -1,7 +1,7 @@ import { PluginExtensionAddedLinkConfig, PluginExtensionPoints } from '@grafana/data'; import { contextSrv } from 'app/core/core'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { log } from '../../plugins/extensions/logs/log'; import { createAddedLinkConfig } from '../../plugins/extensions/utils'; diff --git a/public/app/features/explore/hooks/useExplorePageTitle.ts b/public/app/features/explore/hooks/useExplorePageTitle.ts index 804b2cf69d9..564064ef8dc 100644 --- a/public/app/features/explore/hooks/useExplorePageTitle.ts +++ b/public/app/features/explore/hooks/useExplorePageTitle.ts @@ -4,7 +4,7 @@ import { getDataSourceSrv } from '@grafana/runtime'; import { Branding } from 'app/core/components/Branding/Branding'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { ExploreQueryParams } from 'app/types'; +import { ExploreQueryParams } from 'app/types/explore'; import { isFulfilled, hasKey } from './utils'; diff --git a/public/app/features/explore/hooks/useKeyboardShortcuts.ts b/public/app/features/explore/hooks/useKeyboardShortcuts.ts index bd90b13e3f2..9ff1e32e5ed 100644 --- a/public/app/features/explore/hooks/useKeyboardShortcuts.ts +++ b/public/app/features/explore/hooks/useKeyboardShortcuts.ts @@ -3,8 +3,8 @@ import { Unsubscribable } from 'rxjs'; import { getAppEvents } from '@grafana/runtime'; import { useGrafana } from 'app/core/context/GrafanaContext'; -import { useDispatch } from 'app/types'; import { AbsoluteTimeEvent, CopyTimeEvent, PasteTimeEvent, ShiftTimeEvent, ZoomOutEvent } from 'app/types/events'; +import { useDispatch } from 'app/types/store'; import { copyTimeRangeToClipboard, diff --git a/public/app/features/explore/hooks/useSplitSizeUpdater.ts b/public/app/features/explore/hooks/useSplitSizeUpdater.ts index ea2f6279b8a..81c9fa2edeb 100644 --- a/public/app/features/explore/hooks/useSplitSizeUpdater.ts +++ b/public/app/features/explore/hooks/useSplitSizeUpdater.ts @@ -2,7 +2,7 @@ import { inRange } from 'lodash'; import { useState } from 'react'; import { useWindowSize } from 'react-use'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { splitSizeUpdateAction } from '../state/main'; import { isSplit, selectPanesEntries } from '../state/selectors'; diff --git a/public/app/features/explore/hooks/useStateSync/external.utils.ts b/public/app/features/explore/hooks/useStateSync/external.utils.ts index e691ba13b33..2df25f0dfde 100644 --- a/public/app/features/explore/hooks/useStateSync/external.utils.ts +++ b/public/app/features/explore/hooks/useStateSync/external.utils.ts @@ -2,7 +2,7 @@ import { isEmpty, isObject, mapValues, omitBy } from 'lodash'; import { ExploreUrlState, toURLRange } from '@grafana/data'; import { clearQueryKeys } from 'app/core/utils/explore'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState { return { diff --git a/public/app/features/explore/hooks/useStateSync/index.ts b/public/app/features/explore/hooks/useStateSync/index.ts index 348a12b0ac4..44462fa0d35 100644 --- a/public/app/features/explore/hooks/useStateSync/index.ts +++ b/public/app/features/explore/hooks/useStateSync/index.ts @@ -2,7 +2,8 @@ import { useEffect, useRef } from 'react'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { addListener, ExploreQueryParams, useDispatch, useSelector } from 'app/types'; +import { ExploreQueryParams } from 'app/types/explore'; +import { addListener, useDispatch, useSelector } from 'app/types/store'; import { selectPanes } from '../../state/selectors'; diff --git a/public/app/features/explore/hooks/useStateSync/parseURL.ts b/public/app/features/explore/hooks/useStateSync/parseURL.ts index 4325630c1a9..ca40e024e0f 100644 --- a/public/app/features/explore/hooks/useStateSync/parseURL.ts +++ b/public/app/features/explore/hooks/useStateSync/parseURL.ts @@ -1,4 +1,4 @@ -import { ExploreQueryParams } from 'app/types'; +import { ExploreQueryParams } from 'app/types/explore'; import { v0Migrator } from './migrators/v0'; import { ExploreURLV1, v1Migrator } from './migrators/v1'; diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts index 5497f481407..c196320a859 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/fromURL.ts @@ -8,7 +8,8 @@ import { cancelQueries, runQueries, setQueriesAction } from 'app/features/explor import { updateTime } from 'app/features/explore/state/time'; import { fromURLRange } from 'app/features/explore/state/utils'; import { withUniqueRefIds } from 'app/features/explore/utils/queries'; -import { ExploreItemState, ThunkDispatch } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { ThunkDispatch } from 'app/types/store'; import { getUrlStateFromPaneState } from '../index'; import { urlDiff } from '../internal.utils'; diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts index dae7cd105d3..f4dec0dcecb 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/init.ts @@ -9,7 +9,7 @@ import { clearPanes, syncTimesAction } from 'app/features/explore/state/main'; import { fromURLRange } from 'app/features/explore/state/utils'; import { withUniqueRefIds } from 'app/features/explore/utils/queries'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { ThunkDispatch } from 'app/types'; +import { ThunkDispatch } from 'app/types/store'; import { getUrlStateFromPaneState } from '../index'; import { diff --git a/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts b/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts index ba164419822..9485eef2446 100644 --- a/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts +++ b/public/app/features/explore/hooks/useStateSync/synchronizer/toURL.ts @@ -9,7 +9,7 @@ import { changePanelsStateAction } from 'app/features/explore/state/explorePane' import { splitClose, splitOpen } from 'app/features/explore/state/main'; import { runQueries } from 'app/features/explore/state/query'; import { changeRangeAction } from 'app/features/explore/state/time'; -import { ExploreState } from 'app/types'; +import { ExploreState } from 'app/types/explore'; import { getUrlStateFromPaneState } from '../index'; import { InitState } from '../internal.utils'; diff --git a/public/app/features/explore/mocks/data.ts b/public/app/features/explore/mocks/data.ts index 46f9355c104..3cf601b623f 100644 --- a/public/app/features/explore/mocks/data.ts +++ b/public/app/features/explore/mocks/data.ts @@ -1,8 +1,7 @@ import { Observable, of } from 'rxjs'; import { getDefaultTimeRange, LoadingState, LogsModel } from '@grafana/data'; - -import { ExplorePanelData } from '../../../types'; +import { ExplorePanelData } from 'app/types/explore'; type MockProps = { logsResult?: Partial<LogsModel>; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index de530b584cc..647284ad25c 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -39,11 +39,11 @@ import { Echo } from 'app/core/services/echo/Echo'; import { setLastUsedDatasourceUID } from 'app/core/utils/explore'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { configureStore } from 'app/store/configureStore'; +import { ExploreQueryParams } from 'app/types/explore'; import { RichHistoryRemoteStorageDTO } from '../../../../core/history/RichHistoryRemoteStorage'; import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource'; import { LokiQuery } from '../../../../plugins/datasource/loki/types'; -import { ExploreQueryParams } from '../../../../types'; import { initialUserState } from '../../../profile/state/reducers'; import ExplorePage from '../../ExplorePage'; import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext'; diff --git a/public/app/features/explore/state/correlations.ts b/public/app/features/explore/state/correlations.ts index 4e093ca3669..cc99f6388b7 100644 --- a/public/app/features/explore/state/correlations.ts +++ b/public/app/features/explore/state/correlations.ts @@ -7,7 +7,7 @@ import { createErrorNotification } from 'app/core/copy/appNotification'; import { CreateCorrelationParams } from 'app/features/correlations/types'; import { getCorrelationsBySourceUIDs, createCorrelation, generateDefaultLabel } from 'app/features/correlations/utils'; import { store } from 'app/store/store'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { saveCorrelationsAction } from './explorePane'; import { splitClose } from './main'; diff --git a/public/app/features/explore/state/datasource.test.ts b/public/app/features/explore/state/datasource.test.ts index b024fdb35dd..44e6d7c74e5 100644 --- a/public/app/features/explore/state/datasource.test.ts +++ b/public/app/features/explore/state/datasource.test.ts @@ -1,6 +1,6 @@ import { DataSourceApi } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; import { updateDatasourceInstanceAction, datasourceReducer } from './datasource'; import { createEmptyQueryResponse } from './utils'; diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index 762aebe3ee8..0cea9e1126c 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -7,7 +7,8 @@ import { DataSourceRef } from '@grafana/schema'; import { RefreshPicker } from '@grafana/ui'; import { stopQueryState } from 'app/core/utils/explore'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; -import { ExploreItemState, createAsyncThunk } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { createAsyncThunk } from 'app/types/store'; import { loadSupplementaryQueries } from '../utils/supplementaryQueries'; diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 601ceea2dae..9835b9f3e09 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -16,8 +16,8 @@ import { DataQuery, DataSourceRef } from '@grafana/schema'; import { getQueryKeys } from 'app/core/utils/explore'; import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils'; import { getTimeZone } from 'app/features/profile/state/selectors'; -import { createAsyncThunk, ThunkResult } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; +import { createAsyncThunk, ThunkResult } from 'app/types/store'; import { datasourceReducer } from './datasource'; import { queryReducer, runQueries } from './query'; diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index d582ef42878..4168668091d 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -12,7 +12,8 @@ import { updateRichHistorySettings, updateStarredInRichHistory, } from 'app/core/utils/richHistory'; -import { RichHistoryQuery, ThunkResult } from 'app/types'; +import { RichHistoryQuery } from 'app/types/explore'; +import { ThunkResult } from 'app/types/store'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; diff --git a/public/app/features/explore/state/main.test.ts b/public/app/features/explore/state/main.test.ts index d52a1be7211..7d504613831 100644 --- a/public/app/features/explore/state/main.test.ts +++ b/public/app/features/explore/state/main.test.ts @@ -3,11 +3,12 @@ import { thunkTester } from 'test/core/thunk/thunkTester'; import { dateTime, ExploreUrlState, serializeStateToUrlParam } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; +import { ExploreItemState, ExploreState } from 'app/types/explore'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { configureStore } from '../../../store/configureStore'; -import { ExploreItemState, ExploreState, StoreState, ThunkDispatch } from '../../../types'; import { exploreReducer, navigateToExplore, splitClose, splitOpen } from './main'; diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 42a3a054d7f..d64ec7eca95 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -8,10 +8,10 @@ import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explor import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getTemplateSrv } from 'app/features/templating/template_srv'; import { CorrelationEditorDetailsUpdate, ExploreItemState, ExploreState } from 'app/types/explore'; +import { createAsyncThunk, ThunkResult } from 'app/types/store'; import { RichHistoryResults } from '../../../core/history/RichHistoryStorage'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; -import { createAsyncThunk, ThunkResult } from '../../../types'; import { withUniqueRefIds } from '../utils/queries'; import { DEFAULT_RANGE } from './constants'; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 8a4f3b96be0..42e486fa978 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -18,7 +18,8 @@ import { import { DataQuery, DataSourceRef } from '@grafana/schema'; import config from 'app/core/config'; import { queryLogsSample, queryLogsVolume } from 'app/features/logs/logsModel'; -import { createAsyncThunk, ExploreItemState, StoreState, ThunkDispatch } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { createAsyncThunk, StoreState, ThunkDispatch } from 'app/types/store'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; import * as richHistory from '../../../core/utils/richHistory'; diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index a070253ebc3..63091242c47 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -39,15 +39,14 @@ import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state import { SupportingQueryType } from 'app/plugins/datasource/loki/types'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { - createAsyncThunk, ExploreItemState, ExplorePanelData, + ExploreState, + QueryOptions, QueryTransaction, - StoreState, - ThunkDispatch, - ThunkResult, -} from 'app/types'; -import { ExploreState, QueryOptions, SupplementaryQueries } from 'app/types/explore'; + SupplementaryQueries, +} from 'app/types/explore'; +import { createAsyncThunk, StoreState, ThunkDispatch, ThunkResult } from 'app/types/store'; import { notifyApp } from '../../../core/actions'; import { createErrorNotification } from '../../../core/copy/appNotification'; diff --git a/public/app/features/explore/state/selectors.test.ts b/public/app/features/explore/state/selectors.test.ts index 7e3bd990fe1..12531a34fec 100644 --- a/public/app/features/explore/state/selectors.test.ts +++ b/public/app/features/explore/state/selectors.test.ts @@ -1,7 +1,7 @@ import { DataSourceApi, DataSourceJsonData } from '@grafana/data'; import { DataQuery } from '@grafana/schema/dist/esm/index'; import { configureStore } from 'app/store/configureStore'; -import { StoreState, ThunkDispatch } from 'app/types'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { selectExploreDSMaps } from './selectors'; import { createDefaultInitialState } from './testHelpers'; diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index 9f5ea7f86df..c184d3745c5 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -2,7 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { flatten, uniqBy } from 'lodash'; import { DataSourceRef } from '@grafana/schema'; -import { ExploreItemState, StoreState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes; export const selectExploreRoot = (state: Pick<StoreState, 'explore'>) => state.explore; diff --git a/public/app/features/explore/state/time.test.ts b/public/app/features/explore/state/time.test.ts index 1a382acbf17..43e0643ab9b 100644 --- a/public/app/features/explore/state/time.test.ts +++ b/public/app/features/explore/state/time.test.ts @@ -2,7 +2,7 @@ import { reducerTester } from 'test/core/redux/reducerTester'; import { dateTime } from '@grafana/data'; import { configureStore } from 'app/store/configureStore'; -import { ExploreItemState } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; import { createDefaultInitialState } from './testHelpers'; import { changeRangeAction, timeReducer, updateTime } from './time'; diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index f975a59d44f..536c51f9f96 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -17,7 +17,8 @@ import { getCopiedTimeRange, getShiftedTimeRange, getZoomedTimeRange } from 'app import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { sortLogsResult } from 'app/features/logs/utils'; import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors'; -import { ExploreItemState, ThunkDispatch, ThunkResult } from 'app/types'; +import { ExploreItemState } from 'app/types/explore'; +import { ThunkDispatch, ThunkResult } from 'app/types/store'; import { syncTimesAction } from './main'; import { runLoadMoreLogsQueries, runQueries } from './query'; diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 45b8b39e0e4..4aba4a85f35 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -25,8 +25,8 @@ import { DataQuery, DataSourceJsonData, DataSourceRef, TimeZone } from '@grafana import { getLocalRichHistoryStorage } from 'app/core/history/richHistoryStorageProvider'; import { SortOrder } from 'app/core/utils/richHistoryTypes'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; -import { ExplorePanelData, StoreState } from 'app/types'; -import { ExploreItemState, RichHistoryQuery } from 'app/types/explore'; +import { ExploreItemState, ExplorePanelData, RichHistoryQuery } from 'app/types/explore'; +import { StoreState } from 'app/types/store'; import store from '../../../core/store'; import { setLastUsedDatasourceUID } from '../../../core/utils/explore'; diff --git a/public/app/features/explore/useLiveTailControls.ts b/public/app/features/explore/useLiveTailControls.ts index 42837521e05..e78c1272ad7 100644 --- a/public/app/features/explore/useLiveTailControls.ts +++ b/public/app/features/explore/useLiveTailControls.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import * as React from 'react'; import { RefreshPicker } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { setPausedStateAction, runQueries, clearLogs } from './state/query'; import { changeRefreshInterval } from './state/time'; diff --git a/public/app/features/explore/utils/decorators.test.ts b/public/app/features/explore/utils/decorators.test.ts index 5e2e1517eee..3be590a31f5 100644 --- a/public/app/features/explore/utils/decorators.test.ts +++ b/public/app/features/explore/utils/decorators.test.ts @@ -13,7 +13,7 @@ import { import { CorrelationData } from '@grafana/runtime'; import { DataSourceJsonData, DataQuery } from '@grafana/schema'; import TableModel from 'app/core/TableModel'; -import { ExplorePanelData } from 'app/types'; +import { ExplorePanelData } from 'app/types/explore'; import { decorateWithCorrelations, diff --git a/public/app/features/explore/utils/decorators.ts b/public/app/features/explore/utils/decorators.ts index 08dc24b6a7d..5a877294649 100644 --- a/public/app/features/explore/utils/decorators.ts +++ b/public/app/features/explore/utils/decorators.ts @@ -16,9 +16,9 @@ import { } from '@grafana/data'; import { config, CorrelationData } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; +import { ExplorePanelData } from 'app/types/explore'; import { refreshIntervalToSortOrder } from '../../../core/utils/explore'; -import { ExplorePanelData } from '../../../types'; import { attachCorrelationsToDataFrames } from '../../correlations/utils'; import { dataFrameToLogsModel } from '../../logs/logsModel'; import { sortLogsResult } from '../../logs/utils'; diff --git a/public/app/features/explore/utils/supplementaryQueries.test.ts b/public/app/features/explore/utils/supplementaryQueries.test.ts index 8d274f162b1..ebc1be38292 100644 --- a/public/app/features/explore/utils/supplementaryQueries.test.ts +++ b/public/app/features/explore/utils/supplementaryQueries.test.ts @@ -17,10 +17,10 @@ import { DataQueryResponse, } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; +import { ExplorePanelData } from 'app/types/explore'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query'; -import { ExplorePanelData } from '../../../types'; import { mockExplorePanelData } from '../mocks/data'; import { getSupplementaryQueryProvider } from './supplementaryQueries'; diff --git a/public/app/features/explore/utils/supplementaryQueries.ts b/public/app/features/explore/utils/supplementaryQueries.ts index ae25b8902b4..a705c8193fb 100644 --- a/public/app/features/explore/utils/supplementaryQueries.ts +++ b/public/app/features/explore/utils/supplementaryQueries.ts @@ -16,7 +16,7 @@ import { SupplementaryQueryType, } from '@grafana/data'; import store from 'app/core/store'; -import { ExplorePanelData, SupplementaryQueries } from 'app/types'; +import { ExplorePanelData, SupplementaryQueries } from 'app/types/explore'; import { makeDataFramesForLogs, queryLogsSample, queryLogsVolume } from '../../logs/logsModel'; diff --git a/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts b/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts index 6d6735a12c5..da43739db79 100644 --- a/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts +++ b/public/app/features/explore/utils/supplementaryQueries_legacy.test.ts @@ -22,10 +22,10 @@ import { } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; +import { ExplorePanelData } from 'app/types/explore'; import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv'; import { MockDataQueryRequest, MockQuery } from '../../../../test/mocks/query'; -import { ExplorePanelData } from '../../../types'; import { mockExplorePanelData } from '../mocks/data'; import { getSupplementaryQueryProvider } from './supplementaryQueries'; diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index 75261904c60..b1f07c253bb 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -1,10 +1,12 @@ +import { css } from '@emotion/css'; import { useCallback, useEffect, useRef } from 'react'; -import { DataSourceApi, QueryEditorProps, SelectableValue } from '@grafana/data'; -import { t } from '@grafana/i18n'; -import { InlineField, Select } from '@grafana/ui'; +import { DataSourceApi, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { Button, IconButton, InlineField, PopoverContent, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from './components/ClassicConditions'; +import { ExpressionTypeDropdown } from './components/ExpressionTypeDropdown'; import { Math } from './components/Math'; import { Reduce } from './components/Reduce'; import { Resample } from './components/Resample'; @@ -20,6 +22,24 @@ const labelWidth = 15; type NonClassicExpressionType = Exclude<ExpressionQueryType, ExpressionQueryType.classic>; type ExpressionTypeConfigStorage = Partial<Record<NonClassicExpressionType, string>>; +// Help text for each expression type - can be expanded with more detailed content +const getExpressionHelpText = (type: ExpressionQueryType): PopoverContent | string => { + const description = expressionTypes.find(({ value }) => value === type)?.description; + + switch (type) { + case ExpressionQueryType.sql: + return ( + <Trans i18nKey="expressions.expression-query-editor.helper-text-sql"> + Run MySQL-dialect SQL against the tables returned from your data sources. Data source queries (ie "A", "B") + are available as tables and referenced by query-name. Fields are available as columns, as returned from the + data source. + </Trans> + ); + default: + return description ?? ''; + } +}; + function useExpressionsCache() { const expressionCache = useRef<ExpressionTypeConfigStorage>({}); @@ -62,14 +82,16 @@ export function ExpressionQueryEditor(props: Props) { const { query, queries, onRunQuery, onChange, app } = props; const { getCachedExpression, setCachedExpression } = useExpressionsCache(); + const styles = useStyles2(getStyles); + useEffect(() => { setCachedExpression(query.type, query.expression); }, [query.expression, query.type, setCachedExpression]); const onSelectExpressionType = useCallback( - (item: SelectableValue<ExpressionQueryType>) => { - const cachedExpression = getCachedExpression(item.value!); - const defaults = getDefaults({ ...query, type: item.value! }); + (value: ExpressionQueryType) => { + const cachedExpression = getCachedExpression(value!); + const defaults = getDefaults({ ...query, type: value! }); onChange({ ...defaults, expression: cachedExpression ?? defaults.expression }); }, @@ -100,17 +122,35 @@ export function ExpressionQueryEditor(props: Props) { } }; - const selected = expressionTypes.find((o) => o.value === query.type); + const helperText = getExpressionHelpText(query.type); return ( <div> - <InlineField - label={t('expressions.expression-query-editor.label-operation', 'Operation')} - labelWidth={labelWidth} - > - <Select options={expressionTypes} value={selected} onChange={onSelectExpressionType} width={25} /> - </InlineField> + <div className={styles.operationRow}> + <InlineField + label={t('expressions.expression-query-editor.label-operation', 'Operation')} + labelWidth={labelWidth} + > + <ExpressionTypeDropdown handleOnSelect={onSelectExpressionType}> + <Button fill="outline" icon="angle-down" iconPlacement="right" variant="secondary"> + {expressionTypes.find(({ value }) => value === query.type)?.label} + </Button> + </ExpressionTypeDropdown> + </InlineField> + {helperText && <IconButton className={styles.infoIcon} name="info-circle" tooltip={helperText} />} + </div> {renderExpressionType()} </div> ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + operationRow: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + infoIcon: css({ + marginBottom: theme.spacing(0.5), // Align with the select field + }), +}); diff --git a/public/app/features/expressions/components/ExpressionTypeDropdown.tsx b/public/app/features/expressions/components/ExpressionTypeDropdown.tsx new file mode 100644 index 00000000000..2fc78ba53d2 --- /dev/null +++ b/public/app/features/expressions/components/ExpressionTypeDropdown.tsx @@ -0,0 +1,100 @@ +import { css } from '@emotion/css'; +import { ReactElement, useCallback, useMemo, memo } from 'react'; + +import { FeatureState, GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { Dropdown, FeatureBadge, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui'; +import { ExpressionQueryType, expressionTypes } from 'app/features/expressions/types'; + +const EXPRESSION_ICON_MAP = { + [ExpressionQueryType.math]: 'calculator-alt', + [ExpressionQueryType.reduce]: 'compress-arrows', + [ExpressionQueryType.resample]: 'sync', + [ExpressionQueryType.classic]: 'cog', + [ExpressionQueryType.threshold]: 'sliders-v-alt', + [ExpressionQueryType.sql]: 'database', +} as const satisfies Record<ExpressionQueryType, string>; + +interface ExpressionTypeDropdownProps { + children: ReactElement; + handleOnSelect: (value: ExpressionQueryType) => void; +} + +interface ExpressionMenuItemProps { + item: SelectableValue<ExpressionQueryType>; + onSelect: (value: ExpressionQueryType) => void; +} + +const ExpressionMenuItem = memo<ExpressionMenuItemProps>(({ item, onSelect }) => { + const { value, label, description } = item; + const styles = useStyles2(getStyles); + + const handleClick = useCallback(() => onSelect(value!), [value, onSelect]); + + return ( + <Menu.Item + component={() => ( + <div className={styles.expressionTypeItem} role="menuitem"> + <div className={styles.expressionTypeItemContent} data-testid={`expression-type-${value}`}> + <Icon className={styles.icon} name={EXPRESSION_ICON_MAP[value!]} aria-hidden="true" /> + {label} + {value === ExpressionQueryType.sql && <FeatureBadge featureState={FeatureState.new} />} + </div> + <Tooltip placement="right" content={description!}> + <Icon className={styles.infoIcon} name="info-circle" /> + </Tooltip> + </div> + )} + key={value} + label="" + onClick={handleClick} + /> + ); +}); + +ExpressionMenuItem.displayName = 'ExpressionMenuItem'; + +export const ExpressionTypeDropdown = memo<ExpressionTypeDropdownProps>(({ handleOnSelect, children }) => { + const menuItems = useMemo( + () => expressionTypes.map((item) => <ExpressionMenuItem key={item.value} item={item} onSelect={handleOnSelect} />), + [handleOnSelect] + ); + + const menuOverlay = useMemo(() => <Menu role="menu">{menuItems}</Menu>, [menuItems]); + + return ( + <Dropdown placement="bottom-start" overlay={menuOverlay}> + {children} + </Dropdown> + ); +}); + +ExpressionTypeDropdown.displayName = 'ExpressionTypeDropdown'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + expressionTypeItem: css({ + width: '100%', + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + + expressionTypeItemContent: css({ + flexGrow: 1, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }), + + icon: css({ + color: theme.colors.text.secondary, + flexShrink: 0, + }), + + infoIcon: css({ + opacity: 0.7, + color: theme.colors.text.secondary, + flexShrink: 0, + }), + }; +}; diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx index 09c43551de4..1f61a558471 100644 --- a/public/app/features/expressions/components/SqlExpr.tsx +++ b/public/app/features/expressions/components/SqlExpr.tsx @@ -30,12 +30,10 @@ interface Props { export const SqlExpr = ({ onChange, refIds, query, alerting = false }: Props) => { const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]); - const initialQuery = `-- Run MySQL-dialect SQL against the tables returned from your data sources. --- Data source queries (ie "${vars[0]}") are available as tables and referenced by query-name --- Fields are available as columns, as returned from the data source. -SELECT * -FROM ${vars[0]} -LIMIT 10`; + const initialQuery = `SELECT * + FROM ${vars[0]} + LIMIT 10`; + const styles = useStyles2(getStyles); const containerRef = useRef<HTMLDivElement>(null); const [dimensions, setDimensions] = useState({ height: 0 }); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index e79c97f97f5..27b22a3102d 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,6 +1,7 @@ import { updateNavIndex } from 'app/core/actions'; import { backendSrv } from 'app/core/services/backend_srv'; -import { FolderDTO, ThunkResult } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; +import { ThunkResult } from 'app/types/store'; import { buildNavModel } from './navModel'; import { loadFolder } from './reducers'; diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 7db065815b2..49a5167652f 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -3,7 +3,8 @@ import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { getNavSubTitle } from 'app/core/utils/navBarItem-translations'; -import { AccessControlAction, FolderDTO, FolderParent } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { FolderDTO, FolderParent } from 'app/types/folders'; export const FOLDER_ID = 'manage-folder'; diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index 2665316f39e..33f9fe0e4e9 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,4 +1,4 @@ -import { FolderDTO, FolderState } from 'app/types'; +import { FolderDTO, FolderState } from 'app/types/folders'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index f544fde391a..849f57e7631 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { endpoints } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { FolderDTO, FolderState } from 'app/types'; +import { FolderState, FolderDTO } from 'app/types/folders'; export const initialState: FolderState = { id: 0, diff --git a/public/app/features/geo/editor/locationEditor.ts b/public/app/features/geo/editor/locationEditor.ts index a1deb38f8a3..ad71e5ced6d 100644 --- a/public/app/features/geo/editor/locationEditor.ts +++ b/public/app/features/geo/editor/locationEditor.ts @@ -1,4 +1,5 @@ import { Field, FieldType, PanelOptionsEditorBuilder, DataFrame } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { GazetteerPathEditor } from 'app/features/geo/editor/GazetteerPathEditor'; @@ -14,7 +15,7 @@ export function addLocationFields<TOptions>( builder.addCustomEditor({ id: 'modeEditor', path: `${prefix}mode`, - name: 'Location Mode', + name: t('geo.location-editor.name-location-mode', 'Location Mode'), editor: LocationModeEditor, settings: { data, source }, }); @@ -25,18 +26,18 @@ export function addLocationFields<TOptions>( builder .addFieldNamePicker({ path: `${prefix}latitude`, - name: 'Latitude field', + name: t('geo.location-editor.name-latitude-field', 'Latitude field'), settings: { filter: (f: Field) => f.type === FieldType.number, - noFieldsMessage: 'No numeric fields found', + noFieldsMessage: t('geo.location-editor.latitude-field.no-fields-message', 'No numeric fields found'), }, }) .addFieldNamePicker({ path: `${prefix}longitude`, - name: 'Longitude field', + name: t('geo.location-editor.name-longitude-field', 'Longitude field'), settings: { filter: (f: Field) => f.type === FieldType.number, - noFieldsMessage: 'No numeric fields found', + noFieldsMessage: t('geo.location-editor.longitude-field.no-fields-message', 'No numeric fields found'), }, }); break; @@ -44,10 +45,10 @@ export function addLocationFields<TOptions>( case FrameGeometrySourceMode.Geohash: builder.addFieldNamePicker({ path: `${prefix}geohash`, - name: 'Geohash field', + name: t('geo.location-editor.name-geohash-field', 'Geohash field'), settings: { filter: (f: Field) => f.type === FieldType.string, - noFieldsMessage: 'No strings fields found', + noFieldsMessage: t('geo.location-editor.geohash-field.no-fields-message', 'No strings fields found'), }, }); break; @@ -56,16 +57,16 @@ export function addLocationFields<TOptions>( builder .addFieldNamePicker({ path: `${prefix}lookup`, - name: 'Lookup field', + name: t('geo.location-editor.name-lookup-field', 'Lookup field'), settings: { filter: (f: Field) => f.type === FieldType.string, - noFieldsMessage: 'No strings fields found', + noFieldsMessage: t('geo.location-editor.lookup-field.no-fields-message', 'No strings fields found'), }, }) .addCustomEditor({ id: 'gazetteer', path: `${prefix}gazetteer`, - name: 'Gazetteer', + name: t('geo.location-editor.name-gazetteer', 'Gazetteer'), editor: GazetteerPathEditor, }); } diff --git a/public/app/features/geo/editor/locationModeEditor.tsx b/public/app/features/geo/editor/locationModeEditor.tsx index 6ded40a3abe..aacc913123c 100644 --- a/public/app/features/geo/editor/locationModeEditor.tsx +++ b/public/app/features/geo/editor/locationModeEditor.tsx @@ -3,38 +3,12 @@ import { useEffect, useState } from 'react'; import { StandardEditorProps, DataFrame, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { t } from '@grafana/i18n'; import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { Alert, HorizontalGroup, Icon, Select, useStyles2 } from '@grafana/ui'; import { FrameGeometryField, getGeometryField, getLocationMatchers } from '../utils/location'; -const MODE_OPTIONS = [ - { - value: FrameGeometrySourceMode.Auto, - label: 'Auto', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.autoOption, - description: 'Automatically identify location data based on default field names', - }, - { - value: FrameGeometrySourceMode.Coords, - label: 'Coords', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.coords.option, - description: 'Specify latitude and longitude fields', - }, - { - value: FrameGeometrySourceMode.Geohash, - label: 'Geohash', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.geohash.option, - description: 'Specify geohash field', - }, - { - value: FrameGeometrySourceMode.Lookup, - label: 'Lookup', - ariaLabel: selectors.components.Transforms.SpatialOperations.location.lookup.option, - description: 'Specify Gazetteer and lookup field', - }, -]; - interface ModeEditorSettings { data?: DataFrame[]; source?: FrameGeometrySource; @@ -50,6 +24,39 @@ export const LocationModeEditor = ({ }: StandardEditorProps<string, ModeEditorSettings, unknown, unknown>) => { const [info, setInfo] = useState<FrameGeometryField>(); + const MODE_OPTIONS = [ + { + value: FrameGeometrySourceMode.Auto, + label: t('geo.location-more-editor.mode-options.label-auto', 'Auto'), + ariaLabel: selectors.components.Transforms.SpatialOperations.location.autoOption, + description: t( + 'geo.location-more-editor.mode-options.description-auto', + 'Automatically identify location data based on default field names' + ), + }, + { + value: FrameGeometrySourceMode.Coords, + label: t('geo.location-more-editor.mode-options.label-coords', 'Coords'), + ariaLabel: selectors.components.Transforms.SpatialOperations.location.coords.option, + description: t( + 'geo.location-more-editor.mode-options.description-coords', + 'Specify latitude and longitude fields' + ), + }, + { + value: FrameGeometrySourceMode.Geohash, + label: t('geo.location-more-editor.mode-options.label-geohash', 'Geohash'), + ariaLabel: selectors.components.Transforms.SpatialOperations.location.geohash.option, + description: t('geo.location-more-editor.mode-options.description-geohash', 'Specify geohash field'), + }, + { + value: FrameGeometrySourceMode.Lookup, + label: t('geo.location-more-editor.mode-options.label-lookup', 'Lookup'), + ariaLabel: selectors.components.Transforms.SpatialOperations.location.lookup.option, + description: t('geo.location-more-editor.mode-options.description-lookup', 'Specify Gazetteer and lookup field'), + }, + ]; + useEffect(() => { if (item.settings?.source && item.settings?.data?.length && item.settings.data[0]) { getLocationMatchers(item.settings.source).then((location) => { diff --git a/public/app/features/geo/utils/frameVectorSource.ts b/public/app/features/geo/utils/frameVectorSource.ts index a996aa55f88..7642c2afd48 100644 --- a/public/app/features/geo/utils/frameVectorSource.ts +++ b/public/app/features/geo/utils/frameVectorSource.ts @@ -1,14 +1,24 @@ -import { Feature } from 'ol'; -import { Geometry, LineString, Point } from 'ol/geom'; +import Feature from 'ol/Feature'; +import { Geometry } from 'ol/geom'; import VectorSource from 'ol/source/Vector'; -import { DataFrame, Field } from '@grafana/data'; +import { DataFrame } from '@grafana/data'; import { getGeometryField, LocationFieldMatchers } from './location'; export interface FrameVectorSourceOptions {} -export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSource<T> { +// Helper function to create properly typed Features +function createFeature<T extends Geometry>(properties: { + frame: DataFrame; + rowIndex: number; + geometry: T; +}): Feature<T> { + const feature = new Feature(properties); + return feature; +} + +export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSource<Feature<T>> { constructor(public location: LocationFieldMatchers) { super({}); } @@ -22,11 +32,12 @@ export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSour } for (let i = 0; i < frame.length; i++) { + const geometry = info.field.values[i] as T; this.addFeatureInternal( - new Feature({ + createFeature({ frame, rowIndex: i, - geometry: info.field.values[i] as T, + geometry, }) ); } @@ -34,27 +45,4 @@ export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSour // only call this at the end this.changed(); } - - updateLineString(frame: DataFrame) { - this.clear(true); - const info = getGeometryField(frame, this.location); - if (!info.field) { - this.changed(); - return; - } - - //eslint-disable-next-line - const field = info.field as unknown as Field<Point>; - const geometry: Geometry = new LineString(field.values.map((p) => p.getCoordinates())); - this.addFeatureInternal( - new Feature({ - frame, - rowIndex: 0, - geometry: geometry as T, - }) - ); - - // only call this at the end - this.changed(); - } } diff --git a/public/app/features/geo/utils/location.ts b/public/app/features/geo/utils/location.ts index 91f009d54b6..96e92929cdd 100644 --- a/public/app/features/geo/utils/location.ts +++ b/public/app/features/geo/utils/location.ts @@ -9,6 +9,7 @@ import { getFieldDisplayName, FieldType, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { getGeoFieldFromGazetteer, pointFieldFromGeohash, pointFieldFromLonLat } from '../format/utils'; @@ -178,7 +179,7 @@ export function getGeometryField(frame: DataFrame, location: LocationFieldMatche }; } return { - warning: 'Unable to find location fields', + warning: t('geo.get-geometry-field.warning-unable-to-find', 'Unable to find location fields'), }; case FrameGeometrySourceMode.Coords: @@ -190,7 +191,7 @@ export function getGeometryField(frame: DataFrame, location: LocationFieldMatche }; } return { - warning: 'Select latitude/longitude fields', + warning: t('geo.get-geometry-field.warning-select-lat-long', 'Select latitude/longitude fields'), }; case FrameGeometrySourceMode.Geohash: @@ -202,7 +203,7 @@ export function getGeometryField(frame: DataFrame, location: LocationFieldMatche }; } return { - warning: 'Select geohash field', + warning: t('geo.get-geometry-field.warning-select-geohash', 'Select geohash field'), }; case FrameGeometrySourceMode.Lookup: @@ -215,13 +216,13 @@ export function getGeometryField(frame: DataFrame, location: LocationFieldMatche }; } return { - warning: 'Gazetteer not found', + warning: t('geo.get-geometry-field.warning-gazetteer-not-found', 'Gazetteer not found'), }; } return { - warning: 'Select lookup field', + warning: t('geo.get-geometry-field.warning-select-lookup', 'Select lookup field'), }; } - return { warning: 'unable to find geometry' }; + return { warning: t('geo.get-geometry-field.warning-no-geometry', 'unable to find geometry') }; } diff --git a/public/app/features/invites/InviteeRow.tsx b/public/app/features/invites/InviteeRow.tsx index dd819e7ddbb..d5fd484e083 100644 --- a/public/app/features/invites/InviteeRow.tsx +++ b/public/app/features/invites/InviteeRow.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { t } from '@grafana/i18n'; import { Button, ClipboardButton } from '@grafana/ui'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import { revokeInvite } from './state/actions'; diff --git a/public/app/features/invites/InviteesTable.tsx b/public/app/features/invites/InviteesTable.tsx index 2bab93ecfdb..04fc9544562 100644 --- a/public/app/features/invites/InviteesTable.tsx +++ b/public/app/features/invites/InviteesTable.tsx @@ -1,7 +1,7 @@ import { PureComponent } from 'react'; import { Trans } from '@grafana/i18n'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import InviteeRow from './InviteeRow'; diff --git a/public/app/features/invites/state/actions.ts b/public/app/features/invites/state/actions.ts index d4af5d085b1..72a40840bd6 100644 --- a/public/app/features/invites/state/actions.ts +++ b/public/app/features/invites/state/actions.ts @@ -1,7 +1,9 @@ import { getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { FormModel } from 'app/features/org/UserInviteForm'; -import { AccessControlAction, createAsyncThunk, Invitee } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { createAsyncThunk } from 'app/types/store'; +import { Invitee } from 'app/types/user'; export const fetchInvitees = createAsyncThunk('users/fetchInvitees', async () => { if (!contextSrv.hasPermission(AccessControlAction.OrgUsersAdd)) { diff --git a/public/app/features/invites/state/reducers.ts b/public/app/features/invites/state/reducers.ts index 35ac54eeca0..00a5701cba8 100644 --- a/public/app/features/invites/state/reducers.ts +++ b/public/app/features/invites/state/reducers.ts @@ -1,6 +1,6 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import { fetchInvitees, revokeInvite } from './actions'; diff --git a/public/app/features/invites/state/selectors.ts b/public/app/features/invites/state/selectors.ts index 5722ecb263f..4f5af894ee9 100644 --- a/public/app/features/invites/state/selectors.ts +++ b/public/app/features/invites/state/selectors.ts @@ -1,6 +1,6 @@ import { EntityState, createSelector } from '@reduxjs/toolkit'; -import { Invitee } from 'app/types'; +import { Invitee } from 'app/types/user'; import { selectors } from './reducers'; diff --git a/public/app/features/library-panels/components/ChangeLibraryPanelModal/ChangeLibraryPanelModal.tsx b/public/app/features/library-panels/components/ChangeLibraryPanelModal/ChangeLibraryPanelModal.tsx index 3e48d672a3f..c90a9fb2128 100644 --- a/public/app/features/library-panels/components/ChangeLibraryPanelModal/ChangeLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/ChangeLibraryPanelModal/ChangeLibraryPanelModal.tsx @@ -1,3 +1,4 @@ +import { t } from '@grafana/i18n'; import { ConfirmModal } from '@grafana/ui'; import { PanelModel } from '../../../dashboard/state/PanelModel'; @@ -19,7 +20,11 @@ export const ChangeLibraryPanelModal = ({ onConfirm, onDismiss, panel }: ChangeL <ConfirmModal onConfirm={onConfirm} onDismiss={onDismiss} - confirmText={isLibraryPanel ? 'Change' : 'Replace'} + confirmText={ + isLibraryPanel + ? t('library-panels.change-library-panel-modal.confirmText-change', 'Change') + : t('library-panels.change-library-panel-modal.confirmText-replace', 'Replace') + } title={title} body={body} dismissText="Cancel" diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx index d4588f0ccff..d86f7a55517 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx @@ -5,7 +5,7 @@ import { useDebounce } from 'react-use'; import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, VerticalGroup, FilterInput } from '@grafana/ui'; -import { FolderInfo } from 'app/types'; +import { FolderInfo } from 'app/types/folders'; import { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; @@ -69,7 +69,7 @@ export const LibraryPanelsSearch = ({ onChange={setSearchQuery} placeholder={t( 'library-panels.library-panels-search.placeholder-search-by-name-or-description', - 'Search by name or description' + 'Search by name, description or folder name' )} width={0} escapeRegex={false} diff --git a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx index 4d3820e6609..d9dafffe82d 100644 --- a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx +++ b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx @@ -7,7 +7,7 @@ import { Button, VerticalGroup } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { changeToLibraryPanel } from 'app/features/panel/state/actions'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; import { LibraryElementDTO } from '../../types'; diff --git a/public/app/features/logs/components/LogDetails.tsx b/public/app/features/logs/components/LogDetails.tsx index 69a5552950f..99313cf1ae9 100644 --- a/public/app/features/logs/components/LogDetails.tsx +++ b/public/app/features/logs/components/LogDetails.tsx @@ -43,7 +43,6 @@ export interface Props extends Themeable2 { onPinLine?: (row: LogRowModel) => void; pinLineButtonTooltipTitle?: PopoverContent; - mode?: 'inline' | 'sidebar'; links?: Record<string, LinkModel[]>; } @@ -51,7 +50,7 @@ interface LinkModelWithIcon extends LinkModel { icon?: IconName; } -const useAttributesExtensionLinks = (row: LogRowModel) => { +export const useAttributesExtensionLinks = (row: LogRowModel) => { // Stable context for useMemo inside usePluginLinks const context: PluginExtensionResourceAttributesContext = useMemo(() => { return { @@ -121,7 +120,6 @@ class UnThemedLogDetails extends PureComponent<Props> { onPinLine, styles, pinLineButtonTooltipTitle, - mode = 'inline', links, } = this.props; const levelStyles = getLogLevelStyles(theme, row.logLevel); @@ -152,14 +150,9 @@ class UnThemedLogDetails extends PureComponent<Props> { return ( <tr className={cx(className, styles.logDetails)}> {showDuplicates && <td />} - {mode === 'inline' && ( - <td - className={levelClassName} - aria-label={t('logs.un-themed-log-details.aria-label-log-level', 'Log level')} - /> - )} + <td className={levelClassName} aria-label={t('logs.un-themed-log-details.aria-label-log-level', 'Log level')} /> <td colSpan={4}> - <div className={mode === 'inline' ? styles.logDetailsContainer : styles.logDetailsSidebarContainer}> + <div className={styles.logDetailsContainer}> <table className={styles.logDetailsTable}> <tbody> {displayedFields && displayedFields.length > 0 && ( @@ -168,7 +161,7 @@ class UnThemedLogDetails extends PureComponent<Props> { <td colSpan={100} className={styles.logDetailsHeading} - aria-label={t('logs.un-themed-log-details.aria-label-fields', 'Fields')} + aria-label={t('logs.un-themed-log-details.aria-label-line', 'Log line')} > <Trans i18nKey="logs.log-details.log-line">Log line</Trans> </td> diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index d672ec49be9..b0138e2e867 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -83,6 +83,7 @@ const getStyles = memoizeOne((theme: GrafanaTheme2) => { }), copyButton: css({ '& > button': { + gap: 0, color: theme.colors.text.secondary, padding: 0, justifyContent: 'center', diff --git a/public/app/features/logs/components/LogLabelStats.tsx b/public/app/features/logs/components/LogLabelStats.tsx index b8d9a5d62a1..bee555170f2 100644 --- a/public/app/features/logs/components/LogLabelStats.tsx +++ b/public/app/features/logs/components/LogLabelStats.tsx @@ -1,15 +1,15 @@ import { css } from '@emotion/css'; -import { PureComponent } from 'react'; +import { useMemo } from 'react'; import { LogLabelStatsModel, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { stylesFactory, withTheme2, Themeable2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { LogLabelStatsRow } from './LogLabelStatsRow'; const STATS_ROW_LIMIT = 5; -const getStyles = stylesFactory((theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2) => { return { logsStats: css({ label: 'logs-stats', @@ -42,9 +42,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { padding: '5px 0px', }), }; -}); +}; -interface Props extends Themeable2 { +interface Props { + className?: string; stats: LogLabelStatsModel[]; label: string; value: string; @@ -52,10 +53,9 @@ interface Props extends Themeable2 { isLabel?: boolean; } -class UnThemedLogLabelStats extends PureComponent<Props> { - render() { - const { label, rowCount, stats, value, theme, isLabel } = this.props; - const style = getStyles(theme); +export const LogLabelStats = ({ className, label, rowCount, stats, value, isLabel }: Props) => { + const style = useStyles2(getStyles); + const rows = useMemo(() => { const topRows = stats.slice(0, STATS_ROW_LIMIT); let activeRow = topRows.find((row) => row.value === value); let otherRows = stats.slice(STATS_ROW_LIMIT); @@ -66,45 +66,45 @@ class UnThemedLogLabelStats extends PureComponent<Props> { activeRow = otherRows.find((row) => row.value === value); otherRows = otherRows.filter((row) => row.value !== value); } + return { topRows, otherRows, insertActiveRow, activeRow }; + }, [stats, value]); - const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0); - const topCount = topRows.reduce((sum, row) => sum + row.count, 0); - const total = topCount + otherCount; - const otherProportion = otherCount / total; + const otherCount = useMemo(() => rows.otherRows.reduce((sum, row) => sum + row.count, 0), [rows.otherRows]); + const topCount = useMemo(() => rows.topRows.reduce((sum, row) => sum + row.count, 0), [rows.topRows]); + const total = topCount + otherCount; + const otherProportion = otherCount / total; - return ( - <div className={style.logsStats} data-testid="logLabelStats"> - <div className={style.logsStatsHeader}> - <div className={style.logsStatsTitle}> - {isLabel - ? t( - 'logs.un-themed-log-label-stats.label-log-stats', - '{{label}}: {{total}} of {{rowCount}} rows have that label', - { - label, - total, - rowCount, - } - ) - : t( - 'logs.un-themed-log-label-stats.field-log-stats', - '{{label}}: {{total}} of {{rowCount}} rows have that field' - )} - </div> - </div> - <div className={style.logsStatsBody}> - {topRows.map((stat) => ( - <LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} /> - ))} - {insertActiveRow && activeRow && <LogLabelStatsRow key={activeRow.value} {...activeRow} active />} - {otherCount > 0 && ( - <LogLabelStatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} /> - )} + return ( + <div className={className ?? style.logsStats} data-testid="logLabelStats"> + <div className={style.logsStatsHeader}> + <div className={style.logsStatsTitle}> + {isLabel + ? t( + 'logs.un-themed-log-label-stats.label-log-stats', + '{{label}}: {{total}} of {{rowCount}} rows have that label', + { + label, + total, + rowCount, + } + ) + : t( + 'logs.un-themed-log-label-stats.field-log-stats', + '{{label}}: {{total}} of {{rowCount}} rows have that field' + )} </div> </div> - ); - } -} - -export const LogLabelStats = withTheme2(UnThemedLogLabelStats); -LogLabelStats.displayName = 'LogLabelStats'; + <div className={style.logsStatsBody}> + {rows.topRows.map((stat) => ( + <LogLabelStatsRow key={stat.value} {...stat} active={stat.value === value} /> + ))} + {rows.insertActiveRow && rows.activeRow && ( + <LogLabelStatsRow key={rows.activeRow.value} {...rows.activeRow} active /> + )} + {otherCount > 0 && ( + <LogLabelStatsRow key="__OTHERS__" count={otherCount} value="Other" proportion={otherProportion} /> + )} + </div> + </div> + ); +}; diff --git a/public/app/features/logs/components/getLogRowStyles.ts b/public/app/features/logs/components/getLogRowStyles.ts index 79e88c1a485..1bd1c5c7a7a 100644 --- a/public/app/features/logs/components/getLogRowStyles.ts +++ b/public/app/features/logs/components/getLogRowStyles.ts @@ -187,14 +187,6 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { margin: theme.spacing(2.5, 1, 2.5, 2), cursor: 'default', }), - logDetailsSidebarContainer: css({ - label: 'logs-row-details-table', - border: `1px solid ${theme.colors.border.medium}`, - padding: theme.spacing(0, 1, 1), - borderRadius: theme.shape.radius.default, - margin: theme.spacing(0, 1, 0, 1), - cursor: 'default', - }), logDetailsTable: css({ label: 'logs-row-details-table', lineHeight: '18px', diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx index 29cf6c6c5e3..f6446e804e1 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.test.tsx @@ -57,8 +57,8 @@ const dfAfter = createDataFrame({ let getRowContext = jest.fn(); const dispatchMock = jest.fn(); -jest.mock('app/types', () => ({ - ...jest.requireActual('app/types'), +jest.mock('app/types/store', () => ({ + ...jest.requireActual('app/types/store'), useDispatch: () => dispatchMock, })); diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.tsx index 5956c892c40..c7aaa58a3af 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.tsx @@ -24,7 +24,7 @@ import { Button, Modal, useTheme2 } from '@grafana/ui'; import store from 'app/core/store'; import { SETTINGS_KEYS } from 'app/features/explore/Logs/utils/logs'; import { splitOpen } from 'app/features/explore/state/main'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { dataFrameToLogsModel } from '../../logsModel'; import { sortLogRows } from '../../utils'; diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index cf5b0903fa8..c6c310ec88c 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -154,6 +154,7 @@ export const InfiniteScroll = ({ displayedFields={displayedFields} index={index} log={logs[index]} + logs={logs} onClick={onClick} showTime={showTime} style={style} diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx index 1a099f950e9..e31ba38cabf 100644 --- a/public/app/features/logs/components/panel/LogLine.test.tsx +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -8,13 +8,14 @@ import { createLogLine } from '../mocks/logRow'; import { getGridTemplateColumns, getStyles, LogLine, Props } from './LogLine'; import { LogListFontSize } from './LogList'; -import { LogListContextProvider } from './LogListContext'; +import { LogListContextProvider, LogListContext } from './LogListContext'; import { LogListSearchContext } from './LogListSearchContext'; -import { defaultProps } from './__mocks__/LogListContext'; +import { defaultProps, defaultValue } from './__mocks__/LogListContext'; import { LogListModel } from './processing'; import { LogLineVirtualization } from './virtualization'; jest.mock('./LogListContext'); +jest.mock('../LogDetails'); const theme = createTheme(); const virtualization = new LogLineVirtualization(theme, 'default'); @@ -44,6 +45,7 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { displayedFields: [], index: 0, log, + logs: [log], onClick: jest.fn(), showTime: true, style: {}, @@ -424,6 +426,40 @@ describe.each(fontSizes)('LogLine', (fontSize: LogListFontSize) => { expect(screen.getByText('un')).toBeInTheDocument(); }); }); + + describe('Inline details', () => { + test('Details are not rendered if details mode is not inline', () => { + render( + <LogListContext.Provider + value={{ + ...defaultValue, + showDetails: [log], + detailsMode: 'sidebar', + detailsDisplayed: jest.fn().mockReturnValue(true), + }} + > + <LogLine {...defaultProps} /> + </LogListContext.Provider> + ); + expect(screen.queryByPlaceholderText('Search field names and values')).not.toBeInTheDocument(); + }); + + test('Details are rendered if details mode is inline', () => { + render( + <LogListContext.Provider + value={{ + ...defaultValue, + showDetails: [log], + detailsMode: 'inline', + detailsDisplayed: jest.fn().mockReturnValue(true), + }} + > + <LogLine {...defaultProps} /> + </LogListContext.Provider> + ); + expect(screen.getByPlaceholderText('Search field names and values')).toBeInTheDocument(); + }); + }); }); describe('getGridTemplateColumns', () => { diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index 3e7ac11bc96..7bc7edd9cd6 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -10,6 +10,7 @@ import { Button, Icon, Tooltip } from '@grafana/ui'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; import { LogMessageAnsi } from '../LogMessageAnsi'; +import { InlineLogLineDetails } from './LogLineDetails'; import { LogLineMenu } from './LogLineMenu'; import { useLogIsPermalinked, useLogIsPinned, useLogListContext } from './LogListContext'; import { useLogListSearchContext } from './LogListSearchContext'; @@ -26,6 +27,7 @@ export interface Props { displayedFields: string[]; index: number; log: LogListModel; + logs: LogListModel[]; showTime: boolean; style: CSSProperties; styles: LogLineStyles; @@ -40,6 +42,7 @@ export const LogLine = ({ displayedFields, index, log, + logs, style, styles, onClick, @@ -50,12 +53,13 @@ export const LogLine = ({ wrapLogMessage, }: Props) => { return ( - <div style={style}> + <div style={wrapLogMessage ? style : { ...style, width: 'max-content', minWidth: '100%' }}> <LogLineComponent displayedFields={displayedFields} height={style.height} index={index} log={log} + logs={logs} styles={styles} onClick={onClick} onOverflow={onOverflow} @@ -78,6 +82,7 @@ const LogLineComponent = memo( height, index, log, + logs, styles, onClick, onOverflow, @@ -88,6 +93,7 @@ const LogLineComponent = memo( }: LogLineComponentProps) => { const { detailsDisplayed, + detailsMode, dedupStrategy, enableLogDetails, fontSize, @@ -235,6 +241,7 @@ const LogLineComponent = memo( </Button> </div> )} + {detailsMode === 'inline' && detailsShown && <InlineLogLineDetails logs={logs} />} </> ); } @@ -528,9 +535,6 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER), whiteSpace: 'pre', paddingBottom: theme.spacing(0.75), - '& .field': { - overflow: 'hidden', - }, }), wrappedLogLine: css({ alignSelf: 'flex-start', diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx new file mode 100644 index 00000000000..fd4e5be7af3 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx @@ -0,0 +1,518 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + Field, + LogLevel, + LogRowModel, + FieldType, + createDataFrame, + DataFrameType, + PluginExtensionPoints, + toDataFrame, + LogsSortOrder, + DataFrame, + ScopedVars, +} from '@grafana/data'; +import { setPluginLinksHook } from '@grafana/runtime'; + +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { createLogLine } from '../mocks/logRow'; + +import { LogLineDetails, Props } from './LogLineDetails'; +import { LogListContext, LogListContextData } from './LogListContext'; +import { defaultValue } from './__mocks__/LogListContext'; + +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + usePluginLinks: jest.fn().mockReturnValue({ links: [] }), + }; +}); +jest.mock('./LogListContext'); + +const setup = ( + propOverrides?: Partial<Props>, + rowOverrides?: Partial<LogRowModel>, + contextOverrides?: Partial<LogListContextData> +) => { + const logs = [createLogLine({ logLevel: LogLevel.error, timeEpochMs: 1546297200000, ...rowOverrides })]; + + const props: Props = { + containerElement: document.createElement('div'), + focusLogLine: jest.fn(), + logs, + onResize: jest.fn(), + ...(propOverrides || {}), + }; + + const contextData: LogListContextData = { + ...defaultValue, + showDetails: logs, + ...contextOverrides, + }; + + return render( + <LogListContext.Provider value={contextData}> + <LogLineDetails {...props} /> + </LogListContext.Provider> + ); +}; + +describe('LogLineDetails', () => { + describe('when fields are present', () => { + test('should render the fields and the log line', () => { + setup(undefined, { labels: { key1: 'label1', key2: 'label2' } }); + expect(screen.getByText('Log line')).toBeInTheDocument(); + expect(screen.getByText('Fields')).toBeInTheDocument(); + }); + test('fields should be visible by default', () => { + setup(undefined, { labels: { key1: 'label1', key2: 'label2' } }); + expect(screen.getByText('key1')).toBeInTheDocument(); + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('key2')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + }); + test('should show an option to display the log line when displayed fields are used', async () => { + const onClickShowField = jest.fn(); + + setup( + undefined, + { labels: { key1: 'label1' } }, + { displayedFields: ['key1'], onClickShowField, onClickHideField: jest.fn() } + ); + expect(screen.getByText('key1')).toBeInTheDocument(); + expect(screen.getByLabelText('Show log line')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Show log line')); + + expect(onClickShowField).toHaveBeenCalledTimes(1); + }); + test('should show an active option to display the log line when displayed fields are used', async () => { + const onClickHideField = jest.fn(); + + setup( + undefined, + { labels: { key1: 'label1' } }, + { displayedFields: ['key1', LOG_LINE_BODY_FIELD_NAME], onClickHideField, onClickShowField: jest.fn() } + ); + expect(screen.getByText('key1')).toBeInTheDocument(); + expect(screen.getByLabelText('Hide log line')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Hide log line')); + + expect(onClickHideField).toHaveBeenCalledTimes(1); + }); + test('should not show an option to display the log line when displayed fields are not used', () => { + setup(undefined, { labels: { key1: 'label1' } }, { displayedFields: [] }); + expect(screen.getByText('key1')).toBeInTheDocument(); + expect(screen.queryByLabelText('Show log line')).not.toBeInTheDocument(); + }); + test('should render the filter controls when the callbacks are provided', () => { + setup( + undefined, + { labels: { key1: 'label1' } }, + { + onClickFilterLabel: () => {}, + onClickFilterOutLabel: () => {}, + } + ); + expect(screen.getByLabelText('Filter for value in query A')).toBeInTheDocument(); + expect(screen.getByLabelText('Filter out value in query A')).toBeInTheDocument(); + }); + describe('Toggleable filters', () => { + test('should pass the log row to Explore filter functions', async () => { + const onClickFilterLabelMock = jest.fn(); + const onClickFilterOutLabelMock = jest.fn(); + const isLabelFilterActiveMock = jest.fn().mockResolvedValue(true); + const log = createLogLine({ + logLevel: LogLevel.error, + timeEpochMs: 1546297200000, + labels: { key1: 'label1' }, + }); + + setup( + { + logs: [log], + }, + undefined, + { + onClickFilterLabel: onClickFilterLabelMock, + onClickFilterOutLabel: onClickFilterOutLabelMock, + isLabelFilterActive: isLabelFilterActiveMock, + showDetails: [log], + } + ); + + expect(isLabelFilterActiveMock).toHaveBeenCalledWith('key1', 'label1', log.dataFrame.refId); + + await userEvent.click(screen.getByLabelText('Filter for value in query A')); + expect(onClickFilterLabelMock).toHaveBeenCalledTimes(1); + expect(onClickFilterLabelMock).toHaveBeenCalledWith( + 'key1', + 'label1', + expect.objectContaining({ + fields: [ + expect.objectContaining({ values: [0] }), + expect.objectContaining({ values: ['line1'] }), + expect.objectContaining({ values: [{ app: 'app01' }] }), + ], + length: 1, + }) + ); + + await userEvent.click(screen.getByLabelText('Filter out value in query A')); + expect(onClickFilterOutLabelMock).toHaveBeenCalledTimes(1); + expect(onClickFilterOutLabelMock).toHaveBeenCalledWith( + 'key1', + 'label1', + expect.objectContaining({ + fields: [ + expect.objectContaining({ values: [0] }), + expect.objectContaining({ values: ['line1'] }), + expect.objectContaining({ values: [{ app: 'app01' }] }), + ], + length: 1, + }) + ); + }); + }); + test('should not render filter controls when the callbacks are not provided', () => { + setup( + undefined, + { labels: { key1: 'label1' } }, + { + onClickFilterLabel: undefined, + onClickFilterOutLabel: undefined, + } + ); + expect(screen.queryByLabelText('Filter for value')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Filter out value')).not.toBeInTheDocument(); + }); + }); + describe('when the log has no fields to display', () => { + test('should render no details available message', () => { + setup(undefined, { entry: '' }); + expect(screen.getByText('No fields to display.')).toBeInTheDocument(); + }); + test('should not render headings', () => { + setup(undefined, { entry: '' }); + expect(screen.queryByText('Fields')).not.toBeInTheDocument(); + expect(screen.queryByText('Links')).not.toBeInTheDocument(); + expect(screen.queryByText(/Indexed label/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Parsed field/)).not.toBeInTheDocument(); + expect(screen.queryByText('Structured metadata')).not.toBeInTheDocument(); + }); + }); + test('should render fields from the dataframe with links', () => { + const entry = 'traceId=1234 msg="some message"'; + const dataFrame = toDataFrame({ + fields: [ + { name: 'timestamp', config: {}, type: FieldType.time, values: [1] }, + { name: 'entry', values: [entry] }, + // As we have traceId in message already this will shadow it. + { + name: 'traceId', + values: ['1234'], + config: { links: [{ title: 'link title', url: 'localhost:3210/${__value.text}' }] }, + }, + { name: 'userId', values: ['5678'] }, + ], + }); + const log = createLogLine( + { entry, dataFrame, entryFieldIndex: 0, rowIndex: 0 }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + virtualization: undefined, + wrapLogMessage: true, + getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame, vars: ScopedVars) => { + if (field.config && field.config.links) { + return field.config.links.map((link) => { + return { + href: link.url.replace('${__value.text}', field.values[rowIndex]), + title: link.title, + target: '_blank', + origin: field, + }; + }); + } + return []; + }, + } + ); + + setup({ logs: [log] }, undefined, { showDetails: [log] }); + + expect(screen.getByText('Fields')).toBeInTheDocument(); + expect(screen.getByText('Links')).toBeInTheDocument(); + expect(screen.getByText('traceId')).toBeInTheDocument(); + expect(screen.getByText('link title')).toBeInTheDocument(); + expect(screen.getByText('1234')).toBeInTheDocument(); + }); + + test('should show the correct log details fields, links and labels for DataFrameType.LogLines frames', () => { + const entry = 'test'; + const dataFrame = createDataFrame({ + fields: [ + { name: 'timestamp', config: {}, type: FieldType.time, values: [1] }, + { name: 'body', type: FieldType.string, values: [entry] }, + { + name: 'labels', + type: FieldType.other, + values: [ + { + label1: 'value1', + }, + ], + }, + { + name: 'shouldNotShowFieldName', + type: FieldType.string, + values: ['shouldNotShowFieldValue'], + }, + { + name: 'shouldShowLinkName', + type: FieldType.string, + values: ['shouldShowLinkValue'], + config: { links: [{ title: 'link', url: 'localhost:3210/${__value.text}' }] }, + }, + ], + meta: { + type: DataFrameType.LogLines, + }, + }); + + const log = createLogLine( + { entry, dataFrame, entryFieldIndex: 0, rowIndex: 0, labels: { label1: 'value1' } }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + virtualization: undefined, + wrapLogMessage: true, + getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame, vars: ScopedVars) => { + if (field.config && field.config.links) { + return field.config.links.map((link) => { + return { + href: link.url.replace('${__value.text}', field.values[rowIndex]), + title: link.title, + target: '_blank', + origin: field, + }; + }); + } + return []; + }, + } + ); + + setup({ logs: [log] }, undefined, { showDetails: [log] }); + + expect(screen.getByText('Log line')).toBeInTheDocument(); + expect(screen.getByText('Fields')).toBeInTheDocument(); + expect(screen.getByText('Links')).toBeInTheDocument(); + + // Don't show additional fields for DataFrameType.LogLines + expect(screen.queryByText('shouldNotShowFieldName')).not.toBeInTheDocument(); + expect(screen.queryByText('shouldNotShowFieldValue')).not.toBeInTheDocument(); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('shouldShowLinkName')).toBeInTheDocument(); + expect(screen.getByText('shouldShowLinkValue')).toBeInTheDocument(); + }); + + test('should load plugin links for logs view resource attributes extension point', () => { + const usePluginLinksMock = jest.fn().mockReturnValue({ links: [] }); + setPluginLinksHook(usePluginLinksMock); + jest.requireMock('@grafana/runtime').usePluginLinks = usePluginLinksMock; + + const rowOverrides = { + datasourceType: 'loki', + datasourceUid: 'grafanacloud-logs', + labels: { key1: 'label1', key2: 'label2' }, + }; + setup(undefined, rowOverrides); + + expect(usePluginLinksMock).toHaveBeenCalledWith({ + extensionPointId: PluginExtensionPoints.LogsViewResourceAttributes, + limitPerPlugin: 10, + context: { + datasource: { + type: 'loki', + uid: 'grafanacloud-logs', + }, + attributes: { key1: ['label1'], key2: ['label2'] }, + }, + }); + }); + + describe('Label types', () => { + const entry = 'test'; + const labels = { + label1: 'value1', + label2: 'value2', + label3: 'value3', + }; + const dataFrame = createDataFrame({ + fields: [ + { name: 'timestamp', config: {}, type: FieldType.time, values: [1] }, + { name: 'body', type: FieldType.string, values: [entry] }, + { name: 'id', type: FieldType.string, values: ['1'] }, + { + name: 'labels', + type: FieldType.other, + values: [labels], + }, + { + name: 'labelTypes', + type: FieldType.other, + values: [ + { + label1: 'I', + label2: 'S', + label3: 'P', + }, + ], + }, + ], + meta: { + type: DataFrameType.LogLines, + }, + }); + test('should show label types if they are available and supported', () => { + setup(undefined, { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'loki', + rowId: '1', + }); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + expect(screen.getByText(/Indexed label/)).toBeInTheDocument(); + expect(screen.getByText(/Parsed field/)).toBeInTheDocument(); + expect(screen.getByText('Structured metadata')).toBeInTheDocument(); + }); + test('should not show label types if they are unavailable or not supported', () => { + setup( + {}, + { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'other datasource', + rowId: '1', + } + ); + + // Show labels and links + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + + expect(screen.getByText('Fields')).toBeInTheDocument(); + expect(screen.queryByText(/Indexed label/)).not.toBeInTheDocument(); + expect(screen.queryByText(/Parsed field/)).not.toBeInTheDocument(); + expect(screen.queryByText('Structured metadata')).not.toBeInTheDocument(); + }); + + test('Should allow to search within fields', async () => { + setup(undefined, { + entry, + dataFrame, + entryFieldIndex: 0, + rowIndex: 0, + labels, + datasourceType: 'loki', + rowId: '1', + }); + + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('label2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + expect(screen.getByText('label3')).toBeInTheDocument(); + expect(screen.getByText('value3')).toBeInTheDocument(); + + const input = screen.getByPlaceholderText('Search field names and values'); + + await userEvent.type(input, 'something else'); + + expect(screen.getAllByText('No results to display.')).toHaveLength(3); + }); + }); + + describe('Label types', () => { + test('Does not show displayed fields controls if not present', () => { + setup(undefined, { labels: { key1: 'label1', key2: 'label2' } }); + expect(screen.queryByText('Displayed fields')).not.toBeInTheDocument(); + }); + + test('Does not show displayed fields controls if required props are not present', () => { + setup(undefined, { labels: { key1: 'label1', key2: 'label2' } }, { displayedFields: ['key1', 'key2'] }); + expect(screen.queryByText('Displayed fields')).not.toBeInTheDocument(); + }); + + test('Shows displayed fields controls if required props are present', async () => { + const setDisplayedFields = jest.fn(); + const onClickHideField = jest.fn(); + setup( + undefined, + { labels: { key1: 'label1', key2: 'label2' } }, + { displayedFields: ['key1', 'key2'], setDisplayedFields, onClickHideField } + ); + + expect(screen.getByText('Organize displayed fields')).toBeInTheDocument(); + expect(screen.queryAllByLabelText('Remove field')).toHaveLength(0); + + await userEvent.click(screen.getByText('Organize displayed fields')); + + expect(screen.getAllByLabelText('Remove field')).toHaveLength(2); + + await userEvent.click(screen.getAllByLabelText('Remove field')[0]); + + expect(onClickHideField).toHaveBeenCalledWith('key1'); + }); + + test('Exposes buttons to reorder displayed fields', async () => { + const setDisplayedFields = jest.fn(); + const onClickHideField = jest.fn(); + setup( + undefined, + { labels: { key1: 'label1', key2: 'label2' } }, + { displayedFields: ['key1', 'key2', 'key3'], setDisplayedFields, onClickHideField } + ); + + await userEvent.click(screen.getByText('Organize displayed fields')); + + expect(screen.getAllByLabelText('Remove field')).toHaveLength(3); + expect(screen.getAllByLabelText('Move down')).toHaveLength(3); + expect(screen.getAllByLabelText('Move up')).toHaveLength(3); + + await userEvent.click(screen.getAllByLabelText('Move down')[0]); + + expect(setDisplayedFields).toHaveBeenCalledWith(['key2', 'key1', 'key3']); + + await userEvent.click(screen.getAllByLabelText('Move up')[2]); + + expect(setDisplayedFields).toHaveBeenCalledWith(['key1', 'key3', 'key2']); + }); + }); +}); diff --git a/public/app/features/logs/components/panel/LogLineDetails.tsx b/public/app/features/logs/components/panel/LogLineDetails.tsx index 08725535ef2..fee8143396f 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.tsx @@ -1,49 +1,36 @@ import { css } from '@emotion/css'; import { Resizable } from 're-resizable'; -import { useCallback, useRef } from 'react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { t } from '@grafana/i18n'; -import { getDragStyles, IconButton, useStyles2, useTheme2 } from '@grafana/ui'; -import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; +import { getDragStyles, useStyles2 } from '@grafana/ui'; -import { LogDetails } from '../LogDetails'; -import { getLogRowStyles } from '../getLogRowStyles'; - -import { useLogListContext } from './LogListContext'; +import { LogLineDetailsComponent } from './LogLineDetailsComponent'; +import { getDetailsScrollPosition, saveDetailsScrollPosition, useLogListContext } from './LogListContext'; import { LogListModel } from './processing'; import { LOG_LIST_MIN_WIDTH } from './virtualization'; -interface Props { +export interface Props { containerElement: HTMLDivElement; - getFieldLinks?: GetFieldLinksFn; + focusLogLine: (log: LogListModel) => void; logs: LogListModel[]; onResize(): void; } -export const LogLineDetails = ({ containerElement, getFieldLinks, logs, onResize }: Props) => { - const { - app, - closeDetails, - detailsWidth, - displayedFields, - isLabelFilterActive, - onClickFilterLabel, - onClickFilterOutLabel, - onClickShowField, - onClickHideField, - onPinLine, - pinLineButtonTooltipTitle, - setDetailsWidth, - showDetails, - wrapLogMessage, - } = useLogListContext(); - const getRows = useCallback(() => logs, [logs]); - const logRowsStyles = getLogRowStyles(useTheme2()); - const styles = useStyles2(getStyles); +export type LogLineDetailsMode = 'inline' | 'sidebar'; + +export const LogLineDetails = ({ containerElement, focusLogLine, logs, onResize }: Props) => { + const { detailsWidth, setDetailsWidth, showDetails } = useLogListContext(); + const styles = useStyles2(getStyles, 'sidebar'); const dragStyles = useStyles2(getDragStyles); const containerRef = useRef<HTMLDivElement | null>(null); + useEffect(() => { + focusLogLine(showDetails[0]); + // Just once + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleResize = useCallback(() => { if (containerRef.current) { setDetailsWidth(containerRef.current.clientWidth); @@ -53,6 +40,10 @@ export const LogLineDetails = ({ containerElement, getFieldLinks, logs, onResize const maxWidth = containerElement.clientWidth - LOG_LIST_MIN_WIDTH; + if (!showDetails.length) { + return null; + } + return ( <Resizable onResize={handleResize} @@ -65,54 +56,70 @@ export const LogLineDetails = ({ containerElement, getFieldLinks, logs, onResize > <div className={styles.container} ref={containerRef}> <div className={styles.scrollContainer}> - <IconButton - name="times" - className={styles.closeIcon} - aria-label={t('logs.log-details.close', 'Close log details')} - onClick={closeDetails} - /> - <table width="100%"> - <tbody> - <LogDetails - getRows={getRows} - mode="sidebar" - row={showDetails[0]} - showDuplicates={false} - styles={logRowsStyles} - wrapLogMessage={wrapLogMessage} - onPinLine={onPinLine} - getFieldLinks={getFieldLinks} - onClickFilterLabel={onClickFilterLabel} - onClickFilterOutLabel={onClickFilterOutLabel} - onClickShowField={onClickShowField} - onClickHideField={onClickHideField} - hasError={showDetails[0].hasError} - displayedFields={displayedFields} - app={app} - isFilterLabelActive={isLabelFilterActive} - pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} - /> - </tbody> - </table> + <LogLineDetailsComponent log={showDetails[0]} logs={logs} /> </div> </div> </Resizable> ); }; -const getStyles = (theme: GrafanaTheme2) => ({ +export interface InlineLogLineDetailsProps { + logs: LogListModel[]; +} + +export const InlineLogLineDetails = memo(({ logs }: InlineLogLineDetailsProps) => { + const { showDetails } = useLogListContext(); + const styles = useStyles2(getStyles, 'inline'); + const scrollRef = useRef<HTMLDivElement | null>(null); + + const saveScroll = useCallback(() => { + saveDetailsScrollPosition(showDetails[0], scrollRef.current?.scrollTop ?? 0); + }, [showDetails]); + + useEffect(() => { + if (!scrollRef.current) { + return; + } + scrollRef.current.scrollTop = getDetailsScrollPosition(showDetails[0]); + }, [showDetails]); + + if (!showDetails.length) { + return null; + } + + return ( + <div className={`${styles.inlineWrapper} log-line-inline-details`}> + <div className={styles.container}> + <div className={styles.scrollContainer} ref={scrollRef} onScroll={saveScroll}> + <LogLineDetailsComponent log={showDetails[0]} logs={logs} /> + </div> + </div> + </div> + ); +}); +InlineLogLineDetails.displayName = 'InlineLogLineDetails'; + +export const LOG_LINE_DETAILS_HEIGHT = 35; + +const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode) => ({ + inlineWrapper: css({ + gridColumn: '1 / -1', + height: `${LOG_LINE_DETAILS_HEIGHT}vh`, + paddingBottom: theme.spacing(0.5), + marginRight: 1, + }), container: css({ overflow: 'auto', height: '100%', + boxShadow: theme.shadows.z1, + border: `1px solid ${theme.colors.border.medium}`, + borderRight: mode === 'sidebar' ? 'none' : undefined, }), scrollContainer: css({ overflow: 'auto', - position: 'relative', height: '100%', }), - closeIcon: css({ - position: 'absolute', - top: theme.spacing(1), - right: theme.spacing(1.5), + componentWrapper: css({ + padding: theme.spacing(0, 1, 1, 1), }), }); diff --git a/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx new file mode 100644 index 00000000000..d38eff282f9 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsComponent.tsx @@ -0,0 +1,194 @@ +import { css } from '@emotion/css'; +import { camelCase, groupBy } from 'lodash'; +import { memo, startTransition, useCallback, useMemo, useRef, useState } from 'react'; + +import { DataFrameType, GrafanaTheme2, store } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { ControlledCollapse, useStyles2 } from '@grafana/ui'; + +import { getLabelTypeFromRow } from '../../utils'; +import { useAttributesExtensionLinks } from '../LogDetails'; +import { createLogLineLinks } from '../logParser'; + +import { LogLineDetailsDisplayedFields } from './LogLineDetailsDisplayedFields'; +import { LabelWithLinks, LogLineDetailsFields, LogLineDetailsLabelFields } from './LogLineDetailsFields'; +import { LogLineDetailsHeader } from './LogLineDetailsHeader'; +import { LogLineDetailsLog } from './LogLineDetailsLog'; +import { useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +interface LogLineDetailsComponentProps { + log: LogListModel; + logs: LogListModel[]; +} + +export const LogLineDetailsComponent = memo(({ log, logs }: LogLineDetailsComponentProps) => { + const { displayedFields, logOptionsStorageKey, setDisplayedFields } = useLogListContext(); + const [search, setSearch] = useState(''); + const inputRef = useRef(''); + const styles = useStyles2(getStyles); + const extensionLinks = useAttributesExtensionLinks(log); + const fieldsWithLinks = useMemo(() => { + const fieldsWithLinks = log.fields.filter((f) => f.links?.length); + const displayedFieldsWithLinks = fieldsWithLinks.filter((f) => f.fieldIndex !== log.entryFieldIndex).sort(); + const hiddenFieldsWithLinks = fieldsWithLinks.filter((f) => f.fieldIndex === log.entryFieldIndex).sort(); + const fieldsWithLinksFromVariableMap = createLogLineLinks(hiddenFieldsWithLinks); + return { + links: displayedFieldsWithLinks, + linksFromVariableMap: fieldsWithLinksFromVariableMap, + }; + }, [log.entryFieldIndex, log.fields]); + const fieldsWithoutLinks = + log.dataFrame.meta?.type === DataFrameType.LogLines + ? // for LogLines frames (dataplane) we don't want to show any additional fields besides already extracted labels and links + [] + : // for other frames, do not show the log message unless there is a link attached + log.fields.filter((f) => f.links?.length === 0 && f.fieldIndex !== log.entryFieldIndex).sort(); + const labelsWithLinks: LabelWithLinks[] = useMemo( + () => + Object.keys(log.labels) + .sort() + .map((label) => ({ + key: label, + value: log.labels[label], + link: extensionLinks?.[label], + })), + [extensionLinks, log.labels] + ); + const groupedLabels = useMemo( + () => groupBy(labelsWithLinks, (label) => getLabelTypeFromRow(label.key, log, true) ?? ''), + [labelsWithLinks, log] + ); + const labelGroups = useMemo(() => Object.keys(groupedLabels), [groupedLabels]); + + const logLineOpen = logOptionsStorageKey + ? store.getBool(`${logOptionsStorageKey}.log-details.logLineOpen`, false) + : false; + const linksOpen = logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.log-details.linksOpen`, true) : true; + const fieldsOpen = logOptionsStorageKey + ? store.getBool(`${logOptionsStorageKey}.log-details.fieldsOpen`, true) + : true; + const displayedFieldsOpen = logOptionsStorageKey + ? store.getBool(`${logOptionsStorageKey}.log-details.displayedFieldsOpen`, false) + : false; + + const handleToggle = useCallback( + (option: string, isOpen: boolean) => { + store.set(`${logOptionsStorageKey}.log-details.${option}`, isOpen); + }, + [logOptionsStorageKey] + ); + + const handleSearch = useCallback((newSearch: string) => { + inputRef.current = newSearch; + startTransition(() => { + setSearch(inputRef.current); + }); + }, []); + + const noDetails = + !fieldsWithLinks.links.length && + !fieldsWithLinks.linksFromVariableMap.length && + !labelGroups.length && + !fieldsWithoutLinks.length; + + return ( + <> + <LogLineDetailsHeader log={log} search={search} onSearch={handleSearch} /> + <div className={styles.componentWrapper}> + <ControlledCollapse + className={styles.collapsable} + label={t('logs.log-line-details.log-line-section', 'Log line')} + collapsible + isOpen={logLineOpen} + onToggle={(isOpen: boolean) => handleToggle('logLineOpen', isOpen)} + > + <LogLineDetailsLog log={log} /> + </ControlledCollapse> + {displayedFields.length > 0 && setDisplayedFields && ( + <ControlledCollapse + label={t('logs.log-line-details.displayed-fields-section', 'Organize displayed fields')} + collapsible + isOpen={displayedFieldsOpen} + onToggle={(isOpen: boolean) => handleToggle('displayedFieldsOpen', isOpen)} + > + <LogLineDetailsDisplayedFields /> + </ControlledCollapse> + )} + {fieldsWithLinks.links.length > 0 && ( + <ControlledCollapse + className={styles.collapsable} + label={t('logs.log-line-details.links-section', 'Links')} + collapsible + isOpen={linksOpen} + onToggle={(isOpen: boolean) => handleToggle('linksOpen', isOpen)} + > + <LogLineDetailsFields disableActions log={log} logs={logs} fields={fieldsWithLinks.links} search={search} /> + <LogLineDetailsFields + disableActions + log={log} + logs={logs} + fields={fieldsWithLinks.linksFromVariableMap} + search={search} + /> + </ControlledCollapse> + )} + {labelGroups.map((group) => + group === '' ? ( + <ControlledCollapse + className={styles.collapsable} + key={'fields'} + label={t('logs.log-line-details.fields-section', 'Fields')} + collapsible + isOpen={fieldsOpen} + onToggle={(isOpen: boolean) => handleToggle('fieldsOpen', isOpen)} + > + <LogLineDetailsLabelFields log={log} logs={logs} fields={groupedLabels[group]} search={search} /> + <LogLineDetailsFields log={log} logs={logs} fields={fieldsWithoutLinks} search={search} /> + </ControlledCollapse> + ) : ( + <ControlledCollapse + className={styles.collapsable} + key={group} + label={group} + collapsible + isOpen={store.getBool(`${logOptionsStorageKey}.log-details.${groupOptionName(group)}`, true)} + onToggle={(isOpen: boolean) => handleToggle(groupOptionName(group), isOpen)} + > + <LogLineDetailsLabelFields log={log} logs={logs} fields={groupedLabels[group]} search={search} /> + </ControlledCollapse> + ) + )} + {!labelGroups.length && fieldsWithoutLinks.length > 0 && ( + <ControlledCollapse + className={styles.collapsable} + key={'fields'} + label={t('logs.log-line-details.fields-section', 'Fields')} + collapsible + isOpen={fieldsOpen} + onToggle={(isOpen: boolean) => handleToggle('fieldsOpen', isOpen)} + > + <LogLineDetailsFields log={log} logs={logs} fields={fieldsWithoutLinks} search={search} /> + </ControlledCollapse> + )} + {noDetails && <Trans i18nKey="logs.log-line-details.no-details">No fields to display.</Trans>} + </div> + </> + ); +}); +LogLineDetailsComponent.displayName = 'LogLineDetailsComponent'; + +function groupOptionName(group: string) { + return `${camelCase(group)}Open`; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + collapsable: css({ + '&:last-of-type': { + marginBottom: 0, + }, + }), + componentWrapper: css({ + padding: theme.spacing(0, 1, 1, 1), + }), +}); diff --git a/public/app/features/logs/components/panel/LogLineDetailsDisplayedFields.tsx b/public/app/features/logs/components/panel/LogLineDetailsDisplayedFields.tsx new file mode 100644 index 00000000000..50e997ad80e --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsDisplayedFields.tsx @@ -0,0 +1,144 @@ +import { css } from '@emotion/css'; +import { DragDropContext, Draggable, DraggableProvided, Droppable, DropResult } from '@hello-pangea/dnd'; +import { useCallback } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { Card, IconButton, useStyles2 } from '@grafana/ui'; + +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; + +import { LogLineDetailsMode } from './LogLineDetails'; +import { useLogListContext } from './LogListContext'; + +export const LogLineDetailsDisplayedFields = () => { + const { displayedFields, setDisplayedFields } = useLogListContext(); + + const reorganizeDisplayedFields = useCallback( + (srcIndex: number, destIndex: number) => { + const newDisplayedFields = [...displayedFields]; + const element = displayedFields[srcIndex]; + + newDisplayedFields.splice(srcIndex, 1); + newDisplayedFields.splice(destIndex, 0, element); + + setDisplayedFields?.(newDisplayedFields); + }, + [displayedFields, setDisplayedFields] + ); + + const onDragEnd = useCallback( + (result: DropResult) => { + if (result.destination == null) { + return; + } + reorganizeDisplayedFields(result.source.index, result.destination.index); + }, + [reorganizeDisplayedFields] + ); + + return ( + <div> + <DragDropContext onDragEnd={onDragEnd}> + <Droppable droppableId="displayed-fields" direction="vertical"> + {(provided) => { + return ( + <div ref={provided.innerRef} {...provided.droppableProps}> + {displayedFields.map((field, index) => ( + <DraggableDisplayedField + key={field} + field={field} + index={index} + moveField={reorganizeDisplayedFields} + /> + ))} + {provided.placeholder} + </div> + ); + }} + </Droppable> + </DragDropContext> + </div> + ); +}; + +interface DraggableDisplayedFieldProps { + field: string; + index: number; + moveField: (srcIndex: number, destIndex: number) => void; +} + +const DraggableDisplayedField = ({ field, index, moveField }: DraggableDisplayedFieldProps) => { + return ( + <Draggable draggableId={field} index={index}> + {(provided) => ( + <DisplayedField key={field} field={field} index={index} moveField={moveField} provided={provided} /> + )} + </Draggable> + ); +}; + +const DisplayedField = ({ + field, + index, + moveField, + provided, +}: DraggableDisplayedFieldProps & { provided: DraggableProvided }) => { + const { detailsMode, displayedFields, onClickHideField } = useLogListContext(); + const styles = useStyles2(getStyles, detailsMode); + const nextIndex = index === displayedFields.length - 1 ? 0 : index + 1; + const prevIndex = index === 0 ? displayedFields.length - 1 : index - 1; + return ( + <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> + <Card noMargin className={styles.fieldCard}> + <div className={styles.fieldWrapper}> + <div className={styles.field}> + {field === LOG_LINE_BODY_FIELD_NAME ? t('logs.log-line-details.log-line-field', 'Log line') : field} + </div> + {displayedFields.length > 1 && ( + <> + <IconButton + name="arrow-down" + onClick={() => moveField(index, nextIndex)} + tooltip={t('logs.log-line-details.move-displayed-field-down', 'Move down')} + /> + <IconButton + name="arrow-up" + onClick={() => moveField(index, prevIndex)} + tooltip={t('logs.log-line-details.move-displayed-field-up', 'Move up')} + /> + </> + )} + {onClickHideField && ( + <IconButton + name="times" + onClick={() => onClickHideField(field)} + tooltip={t('logs.log-line-details.remove-displayed-field', 'Remove field')} + /> + )} + </div> + </Card> + </div> + ); +}; + +const getStyles = (theme: GrafanaTheme2, detailsMode: LogLineDetailsMode) => ({ + fieldCard: css({ + display: 'block', + padding: theme.spacing(1), + marginBottom: theme.spacing(1), + width: detailsMode === 'inline' ? '30vw' : undefined, + overflow: 'hidden', + textOverflow: 'ellipsis', + wordBreak: 'break-word', + }), + fieldWrapper: css({ + cursor: 'move', + display: 'flex', + gap: theme.spacing(0.5), + justifyContent: 'space-evenly', + }), + field: css({ + flex: 1, + }), +}); diff --git a/public/app/features/logs/components/panel/LogLineDetailsFields.tsx b/public/app/features/logs/components/panel/LogLineDetailsFields.tsx new file mode 100644 index 00000000000..3f68e7e829a --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsFields.tsx @@ -0,0 +1,551 @@ +import { css } from '@emotion/css'; +import { isEqual } from 'lodash'; +import { parse, stringify } from 'lossless-json'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; + +import { CoreApp, Field, fuzzySearch, GrafanaTheme2, IconName, LinkModel, LogLabelStatsModel } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; +import { ClipboardButton, DataLinkButton, IconButton, useStyles2 } from '@grafana/ui'; + +import { logRowToSingleRowDataFrame } from '../../logsModel'; +import { calculateLogsLabelStats, calculateStats } from '../../utils'; +import { LogLabelStats } from '../LogLabelStats'; +import { FieldDef } from '../logParser'; + +import { useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +interface LogLineDetailsFieldsProps { + disableActions?: boolean; + fields: FieldDef[]; + log: LogListModel; + logs: LogListModel[]; + search?: string; +} + +export const LogLineDetailsFields = memo(({ disableActions, fields, log, logs, search }: LogLineDetailsFieldsProps) => { + if (!fields.length) { + return null; + } + const styles = useStyles2(getFieldsStyles); + const getLogs = useCallback(() => logs, [logs]); + const filteredFields = useMemo(() => (search ? filterFields(fields, search) : fields), [fields, search]); + + if (filteredFields.length === 0) { + return t('logs.log-line-details.search.no-results', 'No results to display.'); + } + + return ( + <div className={disableActions ? styles.fieldsTableNoActions : styles.fieldsTable}> + {filteredFields.map((field, i) => ( + <LogLineDetailsField + key={`${field.keys[0]}=${field.values[0]}-${i}`} + disableActions={disableActions} + getLogs={getLogs} + fieldIndex={field.fieldIndex} + keys={field.keys} + links={field.links} + log={log} + values={field.values} + /> + ))} + </div> + ); +}); +LogLineDetailsFields.displayName = 'LogLineDetailsFields'; + +interface LinkModelWithIcon extends LinkModel<Field> { + icon?: IconName; +} + +export interface LabelWithLinks { + key: string; + value: string; + links?: LinkModelWithIcon[]; +} + +interface LogLineDetailsLabelFieldsProps { + fields: LabelWithLinks[]; + log: LogListModel; + logs: LogListModel[]; + search?: string; +} + +export const LogLineDetailsLabelFields = ({ fields, log, logs, search }: LogLineDetailsLabelFieldsProps) => { + if (!fields.length) { + return null; + } + const styles = useStyles2(getFieldsStyles); + const getLogs = useCallback(() => logs, [logs]); + const filteredFields = useMemo(() => (search ? filterLabels(fields, search) : fields), [fields, search]); + + if (filteredFields.length === 0) { + return t('logs.log-line-details.search.no-results', 'No results to display.'); + } + + return ( + <div className={styles.fieldsTable}> + {filteredFields.map((field, i) => ( + <LogLineDetailsField + key={`${field.key}=${field.value}-${i}`} + getLogs={getLogs} + isLabel + keys={[field.key]} + links={field.links} + log={log} + values={[field.value]} + /> + ))} + </div> + ); +}; + +const getFieldsStyles = (theme: GrafanaTheme2) => ({ + fieldsTable: css({ + display: 'grid', + gap: theme.spacing(1), + gridTemplateColumns: `${theme.spacing(11.5)} auto 1fr`, + }), + fieldsTableNoActions: css({ + display: 'grid', + gap: theme.spacing(1), + gridTemplateColumns: `auto 1fr`, + }), +}); + +interface LogLineDetailsFieldProps { + keys: string[]; + values: string[]; + disableActions?: boolean; + fieldIndex?: number; + getLogs(): LogListModel[]; + isLabel?: boolean; + links?: LinkModelWithIcon[]; + log: LogListModel; +} + +export const LogLineDetailsField = ({ + disableActions = false, + fieldIndex, + getLogs, + isLabel, + links, + log, + keys, + values, +}: LogLineDetailsFieldProps) => { + const [showFieldsStats, setShowFieldStats] = useState(false); + const [fieldCount, setFieldCount] = useState(0); + const [fieldStats, setFieldStats] = useState<LogLabelStatsModel[] | null>(null); + const { + app, + closeDetails, + displayedFields, + isLabelFilterActive, + onClickFilterLabel, + onClickFilterOutLabel, + onClickShowField, + onClickHideField, + onPinLine, + pinLineButtonTooltipTitle, + syntaxHighlighting, + } = useLogListContext(); + + const styles = useStyles2(getFieldStyles); + + const getStats = useCallback(() => { + if (isLabel) { + return calculateLogsLabelStats(getLogs(), keys[0]); + } + if (fieldIndex !== undefined) { + return calculateStats(log.dataFrame.fields[fieldIndex].values); + } + return []; + }, [fieldIndex, getLogs, isLabel, keys, log.dataFrame.fields]); + + const updateStats = useCallback(() => { + const newStats = getStats(); + const newCount = newStats.reduce((sum, stat) => sum + stat.count, 0); + if (!isEqual(fieldStats, newStats) || fieldCount !== newCount) { + setFieldStats(newStats); + setFieldCount(newCount); + } + }, [fieldCount, fieldStats, getStats]); + + useEffect(() => { + if (showFieldsStats) { + updateStats(); + } + }, [showFieldsStats, updateStats]); + + const showField = useCallback(() => { + if (onClickShowField) { + onClickShowField(keys[0]); + } + + reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', { + datasourceType: log.datasourceType, + logRowUid: log.uid, + type: 'enable', + }); + }, [onClickShowField, keys, log.datasourceType, log.uid]); + + const hideField = useCallback(() => { + if (onClickHideField) { + onClickHideField(keys[0]); + } + + reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', { + datasourceType: log.datasourceType, + logRowUid: log.uid, + type: 'disable', + }); + }, [onClickHideField, keys, log.datasourceType, log.uid]); + + const filterLabel = useCallback(() => { + if (onClickFilterLabel) { + onClickFilterLabel(keys[0], values[0], logRowToSingleRowDataFrame(log) || undefined); + } + + reportInteraction('grafana_explore_logs_log_details_filter_clicked', { + datasourceType: log.datasourceType, + filterType: 'include', + logRowUid: log.uid, + }); + }, [onClickFilterLabel, keys, values, log]); + + const filterOutLabel = useCallback(() => { + if (onClickFilterOutLabel) { + onClickFilterOutLabel(keys[0], values[0], logRowToSingleRowDataFrame(log) || undefined); + } + + reportInteraction('grafana_explore_logs_log_details_filter_clicked', { + datasourceType: log.datasourceType, + filterType: 'exclude', + logRowUid: log.uid, + }); + }, [onClickFilterOutLabel, keys, values, log]); + + const labelFilterActive = useCallback(async () => { + if (isLabelFilterActive) { + return await isLabelFilterActive(keys[0], values[0], log.dataFrame?.refId); + } + return false; + }, [isLabelFilterActive, keys, values, log.dataFrame?.refId]); + + const showStats = useCallback(() => { + setShowFieldStats((showFieldStats: boolean) => !showFieldStats); + + reportInteraction('grafana_explore_logs_log_details_stats_clicked', { + dataSourceType: log.datasourceType, + fieldType: isLabel ? 'label' : 'detectedField', + type: showFieldsStats ? 'close' : 'open', + logRowUid: log.uid, + app, + }); + }, [app, isLabel, log.datasourceType, log.uid, showFieldsStats]); + + const refIdTooltip = useMemo( + () => (app === CoreApp.Explore && log.dataFrame?.refId ? ` in query ${log.dataFrame?.refId}` : ''), + [app, log.dataFrame?.refId] + ); + const singleKey = keys.length === 1; + const singleValue = values.length === 1; + + return ( + <> + <div className={styles.row}> + {!disableActions && ( + <div className={styles.actions}> + {onClickFilterLabel && ( + <AsyncIconButton + name="search-plus" + onClick={filterLabel} + // We purposely want to pass a new function on every render to allow the active state to be updated when log details remains open between updates. + isActive={labelFilterActive} + tooltipSuffix={refIdTooltip} + /> + )} + {onClickFilterOutLabel && ( + <IconButton + name="search-minus" + tooltip={ + app === CoreApp.Explore && log.dataFrame?.refId + ? t('logs.log-line-details.fields.filter-out-query', 'Filter out value in query {{query}}', { + query: log.dataFrame?.refId, + }) + : t('logs.log-line-details.fields.filter-out', 'Filter out value') + } + onClick={filterOutLabel} + /> + )} + {singleKey && displayedFields.includes(keys[0]) && ( + <IconButton + variant="primary" + tooltip={t('logs.log-line-details.fields.toggle-field-button.hide-this-field', 'Hide this field')} + name="eye" + onClick={hideField} + /> + )} + {singleKey && !displayedFields.includes(keys[0]) && ( + <IconButton + tooltip={t( + 'logs.log-line-details.fields.toggle-field-button.field-instead-message', + 'Show this field instead of the message' + )} + name="eye" + onClick={showField} + /> + )} + <IconButton + variant={showFieldsStats ? 'primary' : 'secondary'} + name="signal" + tooltip={t('logs.log-line-details.fields.adhoc-statistics', 'Ad-hoc statistics')} + className="stats-button" + disabled={!singleKey} + onClick={showStats} + /> + </div> + )} + <div className={styles.label}>{singleKey ? keys[0] : <MultipleValue values={keys} />}</div> + <div className={styles.value}> + <div className={styles.valueContainer}> + {singleValue ? ( + <SingleValue value={values[0]} syntaxHighlighting={syntaxHighlighting} /> + ) : ( + <MultipleValue showCopy={true} values={values} /> + )} + </div> + </div> + </div> + {links?.map((link, i) => { + if (link.onClick && onPinLine) { + const originalOnClick = link.onClick; + link.onClick = (e, origin) => { + // Pin the line + onPinLine(log); + + // Execute the link onClick function + originalOnClick(e, origin); + + closeDetails(); + }; + } + return ( + <div className={styles.row} key={`${link.title}-${i}`}> + <div className={disableActions ? styles.linkNoActions : styles.link}> + <DataLinkButton + buttonProps={{ + // Show tooltip message if max number of pinned lines has been reached + tooltip: + typeof pinLineButtonTooltipTitle === 'object' && link.onClick + ? pinLineButtonTooltipTitle + : undefined, + variant: 'secondary', + fill: 'outline', + ...(link.icon && { icon: link.icon }), + }} + link={link} + /> + </div> + </div> + ); + })} + {showFieldsStats && fieldStats && ( + <div className={styles.row}> + <div /> + <div className={disableActions ? undefined : styles.statsColumn}> + <LogLabelStats + className={styles.stats} + stats={fieldStats} + label={keys[0]} + value={values[0]} + rowCount={fieldCount} + isLabel={isLabel} + /> + </div> + </div> + )} + </> + ); +}; + +const getFieldStyles = (theme: GrafanaTheme2) => ({ + row: css({ + display: 'contents', + }), + actions: css({ + whiteSpace: 'nowrap', + }), + label: css({ + overflowWrap: 'break-word', + wordBreak: 'break-word', + }), + value: css({ + overflowWrap: 'break-word', + wordBreak: 'break-word', + button: { + visibility: 'hidden', + }, + '&:hover': { + button: { + visibility: 'visible', + }, + }, + }), + link: css({ + gridColumn: 'span 3', + }), + linkNoActions: css({ + gridColumn: 'span 2', + }), + stats: css({ + paddingRight: theme.spacing(1), + wordBreak: 'break-all', + width: '100%', + maxWidth: '50vh', + }), + statsColumn: css({ + gridColumn: 'span 2', + }), + valueContainer: css({ + display: 'flex', + lineHeight: theme.typography.body.lineHeight, + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + maxHeight: '50vh', + overflow: 'auto', + }), +}); + +const ClipboardButtonWrapper = ({ value }: { value: string }) => { + const styles = useStyles2(getClipboardButtonStyles); + return ( + <div className={styles.button}> + <ClipboardButton + getText={() => value} + title={t('logs.log-line-details.fields.copy-value-to-clipboard', 'Copy value to clipboard')} + fill="text" + variant="secondary" + icon="copy" + size="md" + /> + </div> + ); +}; + +const getClipboardButtonStyles = (theme: GrafanaTheme2) => ({ + button: css({ + '& > button': { + color: theme.colors.text.secondary, + gap: 0, + padding: 0, + justifyContent: 'center', + borderRadius: theme.shape.radius.circle, + height: theme.spacing(theme.components.height.sm), + width: theme.spacing(theme.components.height.sm), + svg: { + margin: 0, + }, + + 'span > div': { + top: '-5px', + '& button': { + color: theme.colors.success.main, + }, + }, + }, + }), +}); + +const MultipleValue = ({ showCopy, values = [] }: { showCopy?: boolean; values: string[] }) => { + if (values.every((val) => val === '')) { + return null; + } + return ( + <table> + <tbody> + {values.map((val, i) => { + return ( + <tr key={`${val}-${i}`}> + <td>{val}</td> + <td>{showCopy && val !== '' && <ClipboardButtonWrapper value={val} />}</td> + </tr> + ); + })} + </tbody> + </table> + ); +}; + +const SingleValue = ({ value: originalValue, syntaxHighlighting }: { value: string; syntaxHighlighting?: boolean }) => { + const value = useMemo(() => { + if (!syntaxHighlighting) { + return originalValue; + } + try { + const parsed = stringify(parse(originalValue), undefined, 2); + if (parsed) { + return parsed; + } + } catch (error) {} + return originalValue; + }, [originalValue, syntaxHighlighting]); + + return ( + <> + {value} + <ClipboardButtonWrapper value={value} /> + </> + ); +}; + +interface AsyncIconButtonProps extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> { + name: IconName; + isActive(): Promise<boolean>; + tooltipSuffix: string; +} + +const AsyncIconButton = ({ isActive, tooltipSuffix, ...rest }: AsyncIconButtonProps) => { + const [active, setActive] = useState(false); + const tooltip = active ? 'Remove filter' : 'Filter for value'; + + useEffect(() => { + isActive().then(setActive); + }, [isActive]); + + return <IconButton {...rest} variant={active ? 'primary' : undefined} tooltip={tooltip + tooltipSuffix} />; +}; + +function filterFields(fields: FieldDef[], search: string) { + const keys = fields.map((field) => field.keys.join(' ')); + const keysIdx = fuzzySearch(keys, search); + const values = fields.map((field) => field.values.join(' ')); + const valuesIdx = fuzzySearch(values, search); + + const results = keysIdx.map((index) => fields[index]); + valuesIdx.forEach((index) => { + if (!results.includes(fields[index])) { + results.push(fields[index]); + } + }); + + return results; +} + +function filterLabels(labels: LabelWithLinks[], search: string) { + const keys = labels.map((field) => field.key); + const keysIdx = fuzzySearch(keys, search); + const values = labels.map((field) => field.value); + const valuesIdx = fuzzySearch(values, search); + + const results = keysIdx.map((index) => labels[index]); + valuesIdx.forEach((index) => { + if (!results.includes(labels[index])) { + results.push(labels[index]); + } + }); + + return results; +} diff --git a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx new file mode 100644 index 00000000000..140a7f86fca --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx @@ -0,0 +1,245 @@ +import { css } from '@emotion/css'; +import { useCallback, useMemo, MouseEvent, useRef, ChangeEvent } from 'react'; + +import { colorManipulator, GrafanaTheme2, LogRowModel, store } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { IconButton, Input, useStyles2 } from '@grafana/ui'; + +import { copyText, handleOpenLogsContextClick } from '../../utils'; +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; + +import { LogLineDetailsMode } from './LogLineDetails'; +import { useLogIsPinned, useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +interface Props { + log: LogListModel; + search: string; + onSearch(newSearch: string): void; +} + +export const LogLineDetailsHeader = ({ log, search, onSearch }: Props) => { + const { + closeDetails, + detailsMode, + displayedFields, + getRowContextQuery, + logOptionsStorageKey, + logSupportsContext, + setDetailsMode, + onClickHideField, + onClickShowField, + onOpenContext, + onPermalinkClick, + onPinLine, + onUnpinLine, + wrapLogMessage, + } = useLogListContext(); + const pinned = useLogIsPinned(log); + const styles = useStyles2(getStyles, detailsMode, wrapLogMessage); + const containerRef = useRef<HTMLDivElement | null>(null); + const inputRef = useRef<HTMLInputElement | null>(null); + + const copyLogLine = useCallback(() => { + copyText(log.entry, containerRef); + }, [log.entry]); + + const copyLinkToLogLine = useCallback(() => { + onPermalinkClick?.(log); + }, [log, onPermalinkClick]); + + const togglePinning = useCallback(() => { + if (pinned) { + onUnpinLine?.(log); + } else { + onPinLine?.(log); + } + }, [log, onPinLine, onUnpinLine, pinned]); + + const shouldlogSupportsContext = useMemo( + () => (logSupportsContext ? logSupportsContext(log) : false), + [log, logSupportsContext] + ); + + const showContext = useCallback( + async (event: MouseEvent<HTMLElement>) => { + handleOpenLogsContextClick(event, log, getRowContextQuery, (log: LogRowModel) => onOpenContext?.(log, () => {})); + }, + [onOpenContext, getRowContextQuery, log] + ); + + const showLogLineToggle = onClickHideField && onClickShowField && displayedFields.length > 0; + const logLineDisplayed = displayedFields.includes(LOG_LINE_BODY_FIELD_NAME); + + const toggleDetailsMode = useCallback(() => { + const newMode = detailsMode === 'inline' ? 'sidebar' : 'inline'; + if (logOptionsStorageKey) { + store.set(`${logOptionsStorageKey}.detailsMode`, newMode); + } + + setDetailsMode(newMode); + }, [detailsMode, logOptionsStorageKey, setDetailsMode]); + + const toggleLogLine = useCallback(() => { + if (logLineDisplayed) { + onClickHideField?.(LOG_LINE_BODY_FIELD_NAME); + } else { + onClickShowField?.(LOG_LINE_BODY_FIELD_NAME); + } + }, [logLineDisplayed, onClickHideField, onClickShowField]); + + const clearSearch = useMemo( + () => ( + <IconButton + name="times" + size="sm" + onClick={() => { + onSearch(''); + if (inputRef.current) { + inputRef.current.value = ''; + } + }} + tooltip={t('logs.log-line-details.clear-search', 'Clear')} + /> + ), + [onSearch] + ); + + const handleSearch = useCallback( + (e: ChangeEvent<HTMLInputElement>) => { + onSearch(e.target.value); + }, + [onSearch] + ); + + return ( + <div className={styles.header} ref={containerRef}> + <Input + onChange={handleSearch} + placeholder={t('logs.log-line-details.search-placeholder', 'Search field names and values')} + ref={inputRef} + suffix={search !== '' ? clearSearch : undefined} + /> + {showLogLineToggle && ( + <IconButton + tooltip={ + logLineDisplayed + ? t('logs.log-line-details.hide-log-line', 'Hide log line') + : t('logs.log-line-details.show-log-line', 'Show log line') + } + tooltipPlacement="top" + size="md" + name="eye" + onClick={toggleLogLine} + tabIndex={0} + variant={logLineDisplayed ? 'primary' : undefined} + /> + )} + <div className={styles.icons}> + <IconButton + tooltip={t('logs.log-line-details.copy-to-clipboard', 'Copy to clipboard')} + tooltipPlacement="top" + size="md" + name="copy" + onClick={copyLogLine} + tabIndex={0} + /> + {onPermalinkClick && log.rowId !== undefined && log.uid && ( + <IconButton + tooltip={t('logs.log-line-details.copy-shortlink', 'Copy shortlink')} + tooltipPlacement="top" + size="md" + name="share-alt" + onClick={copyLinkToLogLine} + tabIndex={0} + /> + )} + {pinned && onUnpinLine && ( + <IconButton + size="md" + name="gf-pin" + onClick={togglePinning} + tooltip={t('logs.log-line-details.unpin-line', 'Unpin log')} + tooltipPlacement="top" + tabIndex={0} + variant="primary" + /> + )} + {!pinned && onPinLine && ( + <IconButton + size="md" + name="gf-pin" + onClick={togglePinning} + tooltip={t('logs.log-line-details.pin-line', 'Pin log')} + tooltipPlacement="top" + tabIndex={0} + /> + )} + {shouldlogSupportsContext && ( + <IconButton + size="md" + name="gf-show-context" + onClick={showContext} + tooltip={t('logs.log-line-details.show-context', 'Show context')} + tooltipPlacement="top" + tabIndex={0} + /> + )} + <IconButton + name={detailsMode === 'inline' ? 'columns' : 'gf-layout-simple'} + tooltip={ + detailsMode === 'inline' + ? t('logs.log-line-details.sidebar-mode', 'Anchor to the right') + : t('logs.log-line-details.inline-mode', 'Display inline') + } + onClick={toggleDetailsMode} + /> + <IconButton + name="times" + aria-label={t('logs.log-line-details.close', 'Close log details')} + onClick={closeDetails} + /> + </div> + </div> + ); +}; + +const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode, wrapLogMessage: boolean) => ({ + container: css({ + overflow: 'auto', + height: '100%', + }), + scrollContainer: css({ + overflow: 'auto', + height: '100%', + }), + header: css({ + alignItems: 'center', + background: theme.colors.background.canvas, + display: 'flex', + flexDirection: !wrapLogMessage && mode === 'inline' ? 'row-reverse' : 'row', + gap: theme.spacing(0.75), + zIndex: theme.zIndex.navbarFixed, + height: theme.spacing(5.5), + marginBottom: theme.spacing(1), + padding: theme.spacing(0.5, 1), + position: 'sticky', + top: 0, + }), + icons: css({ + display: 'flex', + gap: theme.spacing(0.75), + }), + copyLogButton: css({ + padding: 0, + height: theme.spacing(4), + width: theme.spacing(2.5), + overflow: 'hidden', + '&:hover': { + backgroundColor: colorManipulator.alpha(theme.colors.text.primary, 0.12), + }, + }), + componentWrapper: css({ + padding: theme.spacing(0, 1, 1, 1), + }), +}); diff --git a/public/app/features/logs/components/panel/LogLineDetailsLog.tsx b/public/app/features/logs/components/panel/LogLineDetailsLog.tsx new file mode 100644 index 00000000000..259d7ca27c5 --- /dev/null +++ b/public/app/features/logs/components/panel/LogLineDetailsLog.tsx @@ -0,0 +1,44 @@ +import { css } from '@emotion/css'; +import { memo, useMemo } from 'react'; + +import { useStyles2 } from '@grafana/ui'; + +import { getStyles } from './LogLine'; +import { useLogListContext } from './LogListContext'; +import { LogListModel } from './processing'; + +interface Props { + log: LogListModel; +} + +export const LogLineDetailsLog = memo(({ log: originalLog }: Props) => { + const { syntaxHighlighting } = useLogListContext(); + const logStyles = useStyles2(getStyles); + const log = useMemo(() => { + const log = originalLog.clone(); + return log; + }, [originalLog]); + + return ( + <div className={styles.logLineWrapper}> + {!syntaxHighlighting ? ( + <div className="field no-highlighting">{log.body}</div> + ) : ( + <div className={logStyles.logLine}> + <div className={logStyles.wrappedLogLine}> + <div className="field log-syntax-highlight" dangerouslySetInnerHTML={{ __html: log.highlightedBody }} /> + </div> + </div> + )} + </div> + ); +}); + +LogLineDetailsLog.displayName = 'LogLineDetailsLog'; + +const styles = { + logLineWrapper: css({ + maxHeight: '50vh', + overflow: 'auto', + }), +}; diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 17eda97f4f6..fde0ea89dec 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; +import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data'; import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils'; import { createLogRow } from '../mocks/logRow'; @@ -75,6 +75,12 @@ describe('LogList', () => { }); test('Supports showing log details', async () => { + jest.spyOn(store, 'get').mockImplementation((option: string) => { + if (option === 'storage-key.detailsMode') { + return 'sidebar'; + } + return undefined; + }); const onClickFilterLabel = jest.fn(); const onClickFilterOutLabel = jest.fn(); const onClickShowField = jest.fn(); @@ -86,6 +92,50 @@ describe('LogList', () => { onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} onClickShowField={onClickShowField} + logOptionsStorageKey="storage-key" + /> + ); + + await userEvent.click(screen.getByText('log message 1')); + await screen.findByText('Fields'); + + expect(screen.getByText('name_of_the_label')).toBeInTheDocument(); + expect(screen.getByText('value of the label')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Filter for value in query A')); + expect(onClickFilterLabel).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByLabelText('Filter out value in query A')); + expect(onClickFilterOutLabel).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByLabelText('Show this field instead of the message')); + expect(onClickShowField).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByLabelText('Close log details')); + + expect(screen.queryByText('Fields')).not.toBeInTheDocument(); + expect(screen.queryByText('Close log details')).not.toBeInTheDocument(); + }); + + test('Supports showing inline log details', async () => { + jest.spyOn(store, 'get').mockImplementation((option: string) => { + if (option === 'storage-key.detailsMode') { + return 'inline'; + } + return undefined; + }); + const onClickFilterLabel = jest.fn(); + const onClickFilterOutLabel = jest.fn(); + const onClickShowField = jest.fn(); + + render( + <LogList + {...defaultProps} + enableLogDetails={true} + onClickFilterLabel={onClickFilterLabel} + onClickFilterOutLabel={onClickFilterOutLabel} + onClickShowField={onClickShowField} + logOptionsStorageKey="storage-key" /> ); diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index acdbc0dc593..a5115f5e447 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { debounce } from 'lodash'; import { Grammar } from 'prismjs'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, MouseEvent } from 'react'; -import { VariableSizeList } from 'react-window'; +import { Align, VariableSizeList } from 'react-window'; import { AbsoluteTimeRange, @@ -26,7 +26,7 @@ import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { InfiniteScroll } from './InfiniteScroll'; import { getGridTemplateColumns } from './LogLine'; -import { LogLineDetails } from './LogLineDetails'; +import { LogLineDetails, LogLineDetailsMode } from './LogLineDetails'; import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu'; import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext'; import { LogListControls } from './LogListControls'; @@ -41,6 +41,7 @@ export interface Props { app: CoreApp; containerElement: HTMLDivElement; dedupStrategy: LogsDedupStrategy; + detailsMode?: LogLineDetailsMode; displayedFields: string[]; enableLogDetails: boolean; eventBus?: EventBus; @@ -73,6 +74,7 @@ export interface Props { permalinkedLogId?: string; pinLineButtonTooltipTitle?: PopoverContent; pinnedLogs?: string[]; + setDisplayedFields?: (displayedFields: string[]) => void; showControls: boolean; showTime: boolean; sortOrder: LogsSortOrder; @@ -92,6 +94,7 @@ type LogListComponentProps = Omit< | 'dedupStrategy' | 'displayedFields' | 'enableLogDetails' + | 'logOptionsStorageKey' | 'permalinkedLogId' | 'showTime' | 'sortOrder' @@ -103,11 +106,12 @@ export const LogList = ({ app, displayedFields, containerElement, + logOptionsStorageKey, + detailsMode = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.detailsMode`) ?? 'sidebar') : 'sidebar', dedupStrategy, enableLogDetails, eventBus, filterLevels, - logOptionsStorageKey, fontSize = logOptionsStorageKey ? (store.get(`${logOptionsStorageKey}.fontSize`) ?? 'default') : 'default', getFieldLinks, getRowContextQuery, @@ -135,6 +139,7 @@ export const LogList = ({ permalinkedLogId, pinLineButtonTooltipTitle, pinnedLogs, + setDisplayedFields, showControls, showTime, sortOrder, @@ -149,6 +154,7 @@ export const LogList = ({ app={app} containerElement={containerElement} dedupStrategy={dedupStrategy} + detailsMode={detailsMode} displayedFields={displayedFields} enableLogDetails={enableLogDetails} filterLevels={filterLevels} @@ -176,6 +182,7 @@ export const LogList = ({ permalinkedLogId={permalinkedLogId} pinLineButtonTooltipTitle={pinLineButtonTooltipTitle} pinnedLogs={pinnedLogs} + setDisplayedFields={setDisplayedFields} showControls={showControls} showTime={showTime} sortOrder={sortOrder} @@ -218,6 +225,7 @@ const LogListComponent = ({ app, displayedFields, dedupStrategy, + detailsMode, filterLevels, fontSize, forceEscape, @@ -269,6 +277,12 @@ const LogListComponent = ({ }, 25); }, []); + const debouncedScrollToItem = useMemo(() => { + return debounce((index: number, align?: Align) => { + listRef.current?.scrollToItem(index, align); + }, 250); + }, []); + useEffect(() => { const subscription = eventBus.subscribe(ScrollToLogsEvent, (e: ScrollToLogsEvent) => handleScrollToEvent(e, logs.length, listRef.current) @@ -378,6 +392,16 @@ const LogListComponent = ({ [filterLogs, levelFilteredLogs, matchingUids] ); + const focusLogLine = useCallback( + (log: LogListModel) => { + const index = filteredLogs.indexOf(log); + if (index >= 0) { + debouncedScrollToItem(index, 'start'); + } + }, + [debouncedScrollToItem, filteredLogs] + ); + return ( <div className={styles.logListContainer}> <div className={styles.logListWrapper} ref={wrapperRef}> @@ -436,9 +460,11 @@ const LogListComponent = ({ height={listHeight} itemCount={itemCount} itemSize={getLogLineSize.bind(null, virtualization, filteredLogs, widthContainer, displayedFields, { + detailsMode, hasLogsWithErrors, hasSampledLogs, showDuplicates: dedupStrategy !== LogsDedupStrategy.none, + showDetails, showTime, wrap: wrapLogMessage, })} @@ -456,10 +482,10 @@ const LogListComponent = ({ )} </InfiniteScroll> </div> - {showDetails.length > 0 && ( + {detailsMode === 'sidebar' && showDetails.length > 0 && ( <LogLineDetails containerElement={containerElement} - getFieldLinks={getFieldLinks} + focusLogLine={focusLogLine} logs={filteredLogs} onResize={handleLogDetailsResize} /> @@ -481,6 +507,9 @@ function getStyles( '& .unwrapped-log-line': { display: 'grid', gridTemplateColumns: getGridTemplateColumns(columns, displayedFields), + '& .field': { + overflow: 'hidden', + }, }, }), logListContainer: css({ diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index c638437aa2a..d4e8dfea793 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -26,6 +26,7 @@ import { PopoverContent } from '@grafana/ui'; import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils'; +import { LogLineDetailsMode } from './LogLineDetails'; import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu'; import { LogListFontSize } from './LogList'; import { LogListModel } from './processing'; @@ -34,6 +35,7 @@ import { LOG_LIST_MIN_WIDTH } from './virtualization'; export interface LogListContextData extends Omit<Props, 'containerElement' | 'logs' | 'logsMeta' | 'showControls'> { closeDetails: () => void; detailsDisplayed: (log: LogListModel) => boolean; + detailsMode: LogLineDetailsMode; detailsWidth: number; downloadLogs: (format: DownloadFormat) => void; enableLogDetails: boolean; @@ -43,6 +45,7 @@ export interface LogListContextData extends Omit<Props, 'containerElement' | 'lo hasUnescapedContent?: boolean; logLineMenuCustomItems?: LogLineMenuCustomItem[]; setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void; + setDetailsMode: (mode: LogLineDetailsMode) => void; setDetailsWidth: (width: number) => void; setFilterLevels: (filterLevels: LogLevel[]) => void; setFontSize: (size: LogListFontSize) => void; @@ -64,6 +67,7 @@ export const LogListContext = createContext<LogListContextData>({ closeDetails: () => {}, dedupStrategy: LogsDedupStrategy.none, detailsDisplayed: () => false, + detailsMode: 'sidebar', detailsWidth: 0, displayedFields: [], downloadLogs: () => {}, @@ -72,6 +76,7 @@ export const LogListContext = createContext<LogListContextData>({ fontSize: 'default', hasUnescapedContent: false, setDedupStrategy: () => {}, + setDetailsMode: () => {}, setDetailsWidth: () => {}, setFilterLevels: () => {}, setFontSize: () => {}, @@ -132,6 +137,7 @@ export interface Props { children?: ReactNode; // Only ControlledLogRows can send an undefined containerElement. See LogList.tsx containerElement?: HTMLDivElement; + detailsMode?: LogLineDetailsMode; dedupStrategy: LogsDedupStrategy; displayedFields: string[]; enableLogDetails: boolean; @@ -162,6 +168,7 @@ export interface Props { pinLineButtonTooltipTitle?: PopoverContent; pinnedLogs?: string[]; prettifyJSON?: boolean; + setDisplayedFields?: (displayedFields: string[]) => void; showControls: boolean; showUniqueLabels?: boolean; showTime: boolean; @@ -175,6 +182,7 @@ export const LogListContextProvider = ({ children, containerElement, enableLogDetails, + detailsMode: detailsModeProp, dedupStrategy, displayedFields, filterLevels, @@ -204,6 +212,7 @@ export const LogListContextProvider = ({ pinLineButtonTooltipTitle, pinnedLogs, prettifyJSON, + setDisplayedFields, showControls, showTime, showUniqueLabels, @@ -228,6 +237,7 @@ export const LogListContextProvider = ({ }); const [showDetails, setShowDetails] = useState<LogListModel[]>([]); const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey)); + const [detailsMode, setDetailsMode] = useState<LogLineDetailsMode>(detailsModeProp ?? 'sidebar'); useEffect(() => { // Props are updated in the context only of the panel is being externally controlled. @@ -432,8 +442,11 @@ export const LogListContextProvider = ({ ); const closeDetails = useCallback(() => { + if (showDetails.length) { + removeDetailsScrollPosition(showDetails[0]); + } setShowDetails([]); - }, []); + }, [showDetails]); const toggleDetails = useCallback( (log: LogListModel) => { @@ -478,6 +491,7 @@ export const LogListContextProvider = ({ closeDetails, detailsDisplayed, dedupStrategy: logListState.dedupStrategy, + detailsMode, detailsWidth, displayedFields, downloadLogs, @@ -492,6 +506,7 @@ export const LogListContextProvider = ({ getRowContextQuery, logSupportsContext, logLineMenuCustomItems, + logOptionsStorageKey, onClickFilterLabel, onClickFilterOutLabel, onClickFilterString, @@ -508,7 +523,9 @@ export const LogListContextProvider = ({ pinnedLogs: logListState.pinnedLogs, prettifyJSON: logListState.prettifyJSON, setDedupStrategy, + setDetailsMode, setDetailsWidth, + setDisplayedFields, setFilterLevels, setFontSize, setForceEscape, @@ -559,7 +576,9 @@ function getDetailsWidth( const defaultWidth = containerElement.clientWidth * 0.4; const detailsWidth = currentWidth || - (logOptionsStorageKey ? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`), 10) : defaultWidth); + (logOptionsStorageKey + ? parseInt(store.get(`${logOptionsStorageKey}.detailsWidth`) ?? defaultWidth, 10) + : defaultWidth); const maxWidth = containerElement.clientWidth - LOG_LIST_MIN_WIDTH; @@ -569,3 +588,17 @@ function getDetailsWidth( } return detailsWidth; } + +const detailsScrollMap = new Map<string, number>(); + +export function saveDetailsScrollPosition(log: LogListModel, position: number) { + detailsScrollMap.set(log.uid, position); +} + +export function getDetailsScrollPosition(log: LogListModel) { + return detailsScrollMap.get(log.uid) ?? 0; +} + +export function removeDetailsScrollPosition(log: LogListModel) { + detailsScrollMap.delete(log.uid); +} diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index df35eb00704..949f61fc861 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -3,6 +3,7 @@ import { createContext, useContext } from 'react'; import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; import { checkLogsError, checkLogsSampled } from 'app/features/logs/utils'; +import { LogLineDetailsMode } from '../LogLineDetails'; import { LogListContextData, Props } from '../LogListContext'; import { LogListModel } from '../processing'; @@ -37,6 +38,10 @@ export const LogListContext = createContext<LogListContextData>({ syntaxHighlighting: true, toggleDetails: () => {}, wrapLogMessage: false, + detailsMode: 'sidebar', + setDetailsMode: function (mode: LogLineDetailsMode): void { + throw new Error('Function not implemented.'); + }, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -59,6 +64,8 @@ export const useLogIsPermalinked = (log: LogListModel) => { }; export const defaultValue: LogListContextData = { + detailsMode: 'sidebar', + setDetailsMode: jest.fn(), setDedupStrategy: jest.fn(), setFilterLevels: jest.fn(), setFontSize: jest.fn(), @@ -73,7 +80,7 @@ export const defaultValue: LogListContextData = { setWrapLogMessage: jest.fn(), closeDetails: jest.fn(), detailsDisplayed: jest.fn(), - detailsWidth: 0, + detailsWidth: 300, downloadLogs: jest.fn(), enableLogDetails: false, filterLevels: [], @@ -130,11 +137,12 @@ export const LogListContextProvider = ({ onUnpinLine = jest.fn(), permalinkedLogId, pinnedLogs = [], + showDetails = [], showTime = true, sortOrder = LogsSortOrder.Descending, syntaxHighlighting = true, wrapLogMessage = true, -}: Partial<Props>) => { +}: Partial<Props> & { showDetails?: LogListModel[] }) => { const hasLogsWithErrors = logs.some((log) => !!checkLogsError(log)); const hasSampledLogs = logs.some((log) => !!checkLogsSampled(log)); @@ -172,6 +180,7 @@ export const LogListContextProvider = ({ setSortOrder: jest.fn(), setSyntaxHighlighting: jest.fn(), setWrapLogMessage: jest.fn(), + showDetails, showTime, sortOrder, syntaxHighlighting, @@ -182,3 +191,9 @@ export const LogListContextProvider = ({ </LogListContext.Provider> ); }; + +export const saveDetailsScrollPosition = jest.fn(); + +export const getDetailsScrollPosition = jest.fn(); + +export const removeDetailsScrollPosition = jest.fn(); diff --git a/public/app/features/logs/components/panel/processing.test.ts b/public/app/features/logs/components/panel/processing.test.ts index 906d2f4a7c5..7ef61741624 100644 --- a/public/app/features/logs/components/panel/processing.test.ts +++ b/public/app/features/logs/components/panel/processing.test.ts @@ -140,6 +140,36 @@ describe('preProcessLogs', () => { ); expect(logListModel.getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME, true)).toBe('log message 1'); }); + + test('Prettifies JSON', () => { + const entry = '{"key": "value", "otherKey": "otherValue"}'; + const logListModel = createLogLine( + { entry }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: false, // unwrapped + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).not.toBe(entry); + }); + + test('Uses lossless parsing', () => { + const entry = '{"number": 90071992547409911}'; + const logListModel = createLogLine( + { entry }, + { + escape: false, + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: false, // unwrapped + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).toContain('90071992547409911'); + }); }); test('Orders logs', () => { @@ -243,6 +273,35 @@ describe('preProcessLogs', () => { expect(entry).toContain(longLog.body); }); + test('Sets the collapsed state based on the new lines count', () => { + const entry = new Array(virtualization.getTruncationLineCount()).fill('test\n').join(''); + const multilineLog = createLogLine( + { entry, labels: { field: 'value' } }, + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } + ); + + expect(multilineLog.collapsed).toBeUndefined(); + + multilineLog.updateCollapsedState([], container); + + expect(multilineLog.collapsed).toBe(true); + expect(entry).toContain(multilineLog.body); + }); + + test('Correctly counts new lines', () => { + const entry = new Array(virtualization.getTruncationLineCount() - 1).fill('test\n').join(''); + const multilineLog = createLogLine( + { entry, labels: { field: 'value' } }, + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } + ); + + expect(multilineLog.collapsed).toBeUndefined(); + + multilineLog.updateCollapsedState([], container); + + expect(multilineLog.collapsed).toBeUndefined(); + }); + test('Considers the displayed fields to set the collapsed state', () => { // Make container half of the size jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(100); @@ -255,6 +314,22 @@ describe('preProcessLogs', () => { expect(longLog.collapsed).toBeUndefined(); }); + test('Considers new lines in displayed fields to set the collapsed state', () => { + const entry = new Array(virtualization.getTruncationLineCount() - 1).fill('test\n').join(''); + const field = 'test\n'; + const multilineLog = createLogLine( + { entry, labels: { field } }, + { escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true } + ); + + expect(multilineLog.collapsed).toBeUndefined(); + + multilineLog.updateCollapsedState(['field', LOG_LINE_BODY_FIELD_NAME], container); + + expect(multilineLog.collapsed).toBe(true); + expect(entry).toContain(multilineLog.body); + }); + test('Updates the body based on the collapsed state', () => { expect(longLog.collapsed).toBeUndefined(); expect(longLog.body).toBe(entry); diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 60e51ca91d4..0872e241a30 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -1,7 +1,8 @@ import ansicolor from 'ansicolor'; +import { parse, stringify } from 'lossless-json'; import Prism, { Grammar } from 'prismjs'; -import { DataFrame, dateTimeFormat, Labels, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { DataFrame, dateTimeFormat, Labels, LogLevel, LogRowModel, LogsSortOrder, textUtil } from '@grafana/data'; import { GetFieldLinksFn } from 'app/plugins/panel/logs/types'; import { checkLogsError, checkLogsSampled, escapeUnescapedString, sortLogRows } from '../../utils'; @@ -97,8 +98,23 @@ export class LogListModel implements LogRowModel { this.raw = raw; } + clone() { + const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this); + // Unless this function is required outside of <LogLineDetailsLog />, we create a wrapped clone, so new lines are not stripped. + clone._wrapLogMessage = true; + clone._body = undefined; + clone._highlightedBody = undefined; + return clone; + } + get body(): string { if (this._body === undefined) { + try { + const parsed = stringify(parse(this.raw), undefined, this._wrapLogMessage ? 2 : 1); + if (parsed) { + this.raw = parsed; + } + } catch (error) {} this._body = this.collapsed ? this.raw.substring(0, this._virtualization?.getTruncationLength(null) ?? TRUNCATION_DEFAULT_LENGTH) : this.raw; @@ -124,7 +140,11 @@ export class LogListModel implements LogRowModel { if (this._highlightedBody === undefined) { this._grammar = this._grammar ?? generateLogGrammar(this); const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch); - this._highlightedBody = Prism.highlight(this.body, { ...extraGrammar, ...this._grammar }, 'lokiql'); + this._highlightedBody = Prism.highlight( + textUtil.sanitize(this.body), + { ...extraGrammar, ...this._grammar }, + 'lokiql' + ); } return this._highlightedBody; } @@ -154,16 +174,27 @@ export class LogListModel implements LogRowModel { } updateCollapsedState(displayedFields: string[], container: HTMLDivElement | null) { - const lineLength = + const line = displayedFields.length > 0 - ? displayedFields.map((field) => this.getDisplayedFieldValue(field, true)).join('').length - : this.entry.length; - const collapsed = - lineLength >= (this._virtualization?.getTruncationLength(container) ?? TRUNCATION_DEFAULT_LENGTH) + ? displayedFields.map((field) => this.getDisplayedFieldValue(field, true)).join('') + : this.body; + + // Length truncation + let collapsed = + line.length >= (this._virtualization?.getTruncationLength(container) ?? TRUNCATION_DEFAULT_LENGTH) ? true : undefined; + + // Newlines truncation + if (!collapsed && this._virtualization) { + const truncationLimit = this._virtualization.getTruncationLineCount(); + collapsed = countNewLines(line, truncationLimit) >= truncationLimit ? true : collapsed; + } + if (this.collapsed === undefined || collapsed === undefined) { this.collapsed = collapsed; + this._body = undefined; + this._highlightedBody = undefined; } return this.collapsed; } @@ -226,3 +257,23 @@ function logLevelToDisplayLevel(level = '') { return level; } } + +function countNewLines(log: string, limit = Infinity) { + let count = 0; + for (let i = 0; i < log.length; ++i) { + // No need to iterate further + if (count > Infinity) { + return count; + } + if (log[i] === '\n') { + count += 1; + } else if (log[i] === '\r') { + count += 1; + // skip LF in CRLF + if (log[i] === '\n') { + i += 1; + } + } + } + return count; +} diff --git a/public/app/features/logs/components/panel/virtualization.test.ts b/public/app/features/logs/components/panel/virtualization.test.ts index 75534d4e170..342262aafd1 100644 --- a/public/app/features/logs/components/panel/virtualization.test.ts +++ b/public/app/features/logs/components/panel/virtualization.test.ts @@ -3,14 +3,17 @@ import { createTheme, LogsSortOrder } from '@grafana/data'; import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; import { createLogLine } from '../mocks/logRow'; +import { LOG_LINE_DETAILS_HEIGHT } from './LogLineDetails'; import { LogListModel, PreProcessOptions } from './processing'; -import { LogLineVirtualization, getLogLineSize, DisplayOptions } from './virtualization'; +import { LogLineVirtualization, getLogLineSize, DisplayOptions, FIELD_GAP_MULTIPLIER } from './virtualization'; describe('Virtualization', () => { let log: LogListModel, container: HTMLDivElement; let virtualization = new LogLineVirtualization(createTheme(), 'default'); + const GAP = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER; + const DETAILS_HEIGHT = window.innerHeight * (LOG_LINE_DETAILS_HEIGHT / 100) + GAP / 2; const PADDING_BOTTOM = 6; const LINE_HEIGHT = virtualization.getLineHeight(); const SINGLE_LINE_HEIGHT = LINE_HEIGHT + PADDING_BOTTOM; @@ -21,8 +24,10 @@ describe('Virtualization', () => { let TWO_LINES_OF_CHARACTERS: number; const defaultOptions: DisplayOptions = { + detailsMode: 'sidebar', wrap: false, showTime: false, + showDetails: [], showDuplicates: false, hasLogsWithErrors: false, hasSampledLogs: false, @@ -50,6 +55,30 @@ describe('Virtualization', () => { expect(size).toBe(SINGLE_LINE_HEIGHT); }); + test('Returns the a single line plus inline details if the display mode is unwrapped', () => { + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 0 + ); + expect(size).toBe(SINGLE_LINE_HEIGHT + DETAILS_HEIGHT); + }); + + test('Should not throw when an undefined index is passed', () => { + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 1 // Index out of bounds + ); + expect(size).toBe(SINGLE_LINE_HEIGHT); + }); + test('Returns the a single line if the line is not loaded yet', () => { const logs = [log]; const size = getLogLineSize( @@ -78,6 +107,21 @@ describe('Virtualization', () => { expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT); }); + test('Returns the size of a truncated long line with inline details', () => { + // Very small container + log.collapsed = true; + jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(10); + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, wrap: true, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 0 + ); + expect(size).toBe((virtualization.getTruncationLineCount() + 1) * LINE_HEIGHT + DETAILS_HEIGHT); + }); + test.each([true, false])('Measures a log line with controls %s and displayed time %s', (showTime: boolean) => { const size = getLogLineSize(virtualization, [log], container, [], { ...defaultOptions, wrap: true, showTime }, 0); expect(size).toBe(SINGLE_LINE_HEIGHT); @@ -115,6 +159,24 @@ describe('Virtualization', () => { expect(size).toBe(THREE_LINES_HEIGHT); }); + test('Measures a multi-line log line with level, controls, displayed time, and inline details', () => { + log = createLogLine( + { labels: { place: 'luna' }, entry: new Array(TWO_LINES_OF_CHARACTERS).fill('e').join('') }, + preProcessOptions + ); + + const size = getLogLineSize( + virtualization, + [log], + container, + [], + { ...defaultOptions, wrap: true, showTime: true, showDetails: [log], detailsMode: 'inline' }, + 0 + ); + // Two lines for the log and one extra for level and time + expect(size).toBe(THREE_LINES_HEIGHT + DETAILS_HEIGHT); + }); + test('Measures a multi-line log line with displayed fields', () => { log = createLogLine( { diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index 2f9b6b6773a..bf84f5fe3ff 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -2,6 +2,7 @@ import ansicolor from 'ansicolor'; import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; +import { LOG_LINE_DETAILS_HEIGHT, LogLineDetailsMode } from './LogLineDetails'; import { LogListFontSize } from './LogList'; import { LogListModel } from './processing'; @@ -54,7 +55,6 @@ export class LogLineVirtualization { getGridSize = () => this.gridSize; getPaddingBottom = () => this.paddingBottom; - // 2/3 of the viewport height getTruncationLineCount = () => Math.round(window.innerHeight / this.getLineHeight() / 1.5); getTruncationLength = (container: HTMLDivElement | null) => { @@ -232,8 +232,10 @@ export class LogLineVirtualization { } export interface DisplayOptions { + detailsMode: LogLineDetailsMode; hasLogsWithErrors?: boolean; hasSampledLogs?: boolean; + showDetails: LogListModel[]; showDuplicates: boolean; showTime: boolean; wrap: boolean; @@ -244,20 +246,25 @@ export function getLogLineSize( logs: LogListModel[], container: HTMLDivElement | null, displayedFields: string[], - { hasLogsWithErrors, hasSampledLogs, showDuplicates, showTime, wrap }: DisplayOptions, + { detailsMode, hasLogsWithErrors, hasSampledLogs, showDuplicates, showDetails, showTime, wrap }: DisplayOptions, index: number ) { if (!container) { return 0; } + const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER; + const detailsHeight = + detailsMode === 'inline' && logs[index] && showDetails.findIndex((log) => log.uid === logs[index].uid) >= 0 + ? window.innerHeight * (LOG_LINE_DETAILS_HEIGHT / 100) + gap / 2 + : 0; // !logs[index] means the line is not yet loaded by infinite scrolling if (!wrap || !logs[index]) { - return virtualization.getLineHeight() + virtualization.getPaddingBottom(); + return virtualization.getLineHeight() + virtualization.getPaddingBottom() + detailsHeight; } // If a long line is collapsed, we show the line count + an extra line for the expand/collapse control logs[index].updateCollapsedState(displayedFields, container); if (logs[index].collapsed) { - return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight(); + return (virtualization.getTruncationLineCount() + 1) * virtualization.getLineHeight() + detailsHeight; } const storedSize = virtualization.retrieveLogLineSize(logs[index].uid, container); @@ -266,7 +273,6 @@ export function getLogLineSize( } let textToMeasure = ''; - const gap = virtualization.getGridSize() * FIELD_GAP_MULTIPLIER; const iconsGap = virtualization.getGridSize() * 0.5; let optionsWidth = 0; if (showDuplicates) { @@ -296,7 +302,9 @@ export function getLogLineSize( const { height } = virtualization.measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth); // When the log is collapsed, add an extra line for the expand/collapse control - return logs[index].collapsed === false ? height + virtualization.getLineHeight() : height; + return logs[index].collapsed === false + ? height + virtualization.getLineHeight() + detailsHeight + : height + detailsHeight; } export interface LogFieldDimension { @@ -313,14 +321,28 @@ export function hasUnderOrOverflow( if (collapsed !== undefined && calculatedHeight) { calculatedHeight -= virtualization.getLineHeight(); } + const inlineDetails = element.parentElement + ? Array.from(element.parentElement.children).filter((element) => + element.classList.contains('log-line-inline-details') + ) + : undefined; + const detailsHeight = inlineDetails?.length ? inlineDetails[0].clientHeight : 0; + + // Line overflows container + let measuredHeight = element.scrollHeight + detailsHeight; const height = calculatedHeight ?? element.clientHeight; - if (element.scrollHeight > height) { - return collapsed !== undefined ? element.scrollHeight + virtualization.getLineHeight() : element.scrollHeight; + if (measuredHeight > height) { + return collapsed !== undefined ? measuredHeight + virtualization.getLineHeight() : measuredHeight; } + + // Line is smaller than container const child = element.children[1]; - if (child instanceof HTMLDivElement && child.clientHeight < height) { - return collapsed !== undefined ? child.clientHeight + virtualization.getLineHeight() : child.clientHeight; + measuredHeight = child.clientHeight + detailsHeight; + if (child instanceof HTMLDivElement && measuredHeight < height) { + return collapsed !== undefined ? measuredHeight + virtualization.getLineHeight() : measuredHeight; } + + // No overflow or undermeasurement return null; } diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 16460c350ec..0d16732ba7d 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -30,6 +30,7 @@ import { Field, LogsMetaItem, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { getConfig } from 'app/core/config'; import { getLogsExtractFields } from '../explore/Logs/LogsTable'; @@ -391,35 +392,27 @@ function getLabelTypeFromFrame(labelKey: string, frame: DataFrame, index: number return typeField[labelKey] ?? null; } -export function getLabelTypeFromRow(label: string, row: LogRowModel) { +export function getLabelTypeFromRow(label: string, row: LogRowModel, plural = false) { if (!row.datasourceType) { return null; } - const idField = row.dataFrame.fields.find((field) => field.name === 'id'); - if (!idField) { - return null; - } - const rowIndex = idField.values.findIndex((id) => id === row.rowId); - if (rowIndex < 0) { - return null; - } - const labelType = getLabelTypeFromFrame(label, row.dataFrame, rowIndex); + const labelType = getLabelTypeFromFrame(label, row.dataFrame, row.rowIndex); if (!labelType) { return null; } - return getDataSourceLabelType(labelType, row.datasourceType); + return getDataSourceLabelType(labelType, row.datasourceType, plural); } -function getDataSourceLabelType(labelType: string, datasourceType: string) { +function getDataSourceLabelType(labelType: string, datasourceType: string, plural: boolean) { switch (datasourceType) { case 'loki': switch (labelType) { case 'I': - return 'Indexed label'; + return t('logs.fields.type.loki.indexed-label', 'Indexed label', { count: plural ? 2 : 1 }); case 'S': - return 'Structured metadata'; + return t('logs.fields.type.loki.structured-metadata', 'Structured metadata', { count: plural ? 2 : 1 }); case 'P': - return 'Parsed label'; + return t('logs.fields.type.loki.parsedl-label', 'Parsed field', { count: plural ? 2 : 1 }); default: return null; } diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index cf23c94f73e..4652a4811bd 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -28,7 +28,7 @@ import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { dispatch } from 'app/store/store'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { cleanUpAction } from '../../core/actions/cleanUp'; import { ImportDashboardOverviewV2 } from '../dashboard-scene/v2schema/ImportDashboardOverviewV2'; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx index 314954ca16c..66508d4b07f 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { locationService, reportInteraction } from '@grafana/runtime'; import { Box, Legend, TextLink } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { clearLoadedDashboard, importDashboard } from '../state/actions'; import { DashboardSource, ImportDashboardDTO } from '../state/reducers'; diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx index 79676bc4a19..282227e4f31 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/PublicDashboardListTable.tsx @@ -27,7 +27,7 @@ import { generatePublicDashboardConfigUrl, generatePublicDashboardUrl, } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PublicDashboardListResponse } from '../../types'; diff --git a/public/app/features/manage-dashboards/components/SnapshotListTable.tsx b/public/app/features/manage-dashboards/components/SnapshotListTable.tsx index 41148e8376b..0891d24b7a5 100644 --- a/public/app/features/manage-dashboards/components/SnapshotListTable.tsx +++ b/public/app/features/manage-dashboards/components/SnapshotListTable.tsx @@ -104,8 +104,12 @@ export const SnapshotListTable = () => { isOpen={!!removeSnapshot} icon="trash-alt" title={t('manage-dashboards.snapshot-list-table.title-delete', 'Delete')} - body={`Are you sure you want to delete '${removeSnapshot?.name}'?`} - confirmText="Delete" + body={t( + 'manage-dashboards.snapshot-list-table.body-delete', + "Are you sure you want to delete '{{snapshotToRemove}}'?", + { snapshotToRemove: removeSnapshot?.name } + )} + confirmText={t('manage-dashboards.snapshot-list-table.confirmText-delete', 'Delete')} onDismiss={() => setRemoveSnapshot(undefined)} onConfirm={() => { doRemoveSnapshot(removeSnapshot!); diff --git a/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx b/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx index f6268790dcf..29d062da35d 100644 --- a/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx +++ b/public/app/features/manage-dashboards/components/SnapshotListTableRow.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { contextSrv } from 'app/core/services/context_srv'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { SnapshotListTableRow } from './SnapshotListTableRow'; diff --git a/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx b/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx index 5e0c08b7395..6a217ca63ce 100644 --- a/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx +++ b/public/app/features/manage-dashboards/components/SnapshotListTableRow.tsx @@ -6,7 +6,7 @@ import { Button, LinkButton, useStyles2 } from '@grafana/ui'; import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { contextSrv } from 'app/core/services/context_srv'; import { Snapshot } from 'app/features/dashboard/services/SnapshotSrv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export interface Props { snapshot: Snapshot; diff --git a/public/app/features/manage-dashboards/services/ValidationSrv.ts b/public/app/features/manage-dashboards/services/ValidationSrv.ts index 24d41c1c048..48ca75c417b 100644 --- a/public/app/features/manage-dashboards/services/ValidationSrv.ts +++ b/public/app/features/manage-dashboards/services/ValidationSrv.ts @@ -1,3 +1,4 @@ +import { t } from '@grafana/i18n'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; class ValidationError extends Error { @@ -13,14 +14,24 @@ export class ValidationSrv { rootName = 'general'; validateNewDashboardName(folderUID: string, name: string) { - return this.validate(folderUID, name, 'A dashboard or a folder with the same name already exists'); + return this.validate( + folderUID, + name, + t( + 'manage-dashboards.validation-srv.message-same-name', + 'A dashboard or a folder with the same name already exists' + ) + ); } validateNewFolderName(name?: string) { return this.validate( this.rootName, name, - 'A folder or dashboard in the general folder with the same name already exists' + t( + 'manage-dashboards.validation-srv.message-same-name-general', + 'A folder or dashboard in the general folder with the same name already exists' + ) ); } @@ -29,11 +40,20 @@ export class ValidationSrv { const nameLowerCased = name.toLowerCase(); if (name.length === 0) { - throw new ValidationError('REQUIRED', 'Name is required'); + throw new ValidationError( + 'REQUIRED', + t('manage-dashboards.validation-srv.message-name-required', 'Name is required') + ); } if (nameLowerCased === this.rootName) { - throw new ValidationError('EXISTING', 'This is a reserved name and cannot be used for a folder.'); + throw new ValidationError( + 'EXISTING', + t( + 'manage-dashboards.validation-srv.message-reserved-name', + 'This is a reserved name and cannot be used for a folder.' + ) + ); } const searcher = getGrafanaSearcher(); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 1b5b02e24e4..5d07c1d66e6 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -10,7 +10,8 @@ import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; +import { PermissionLevelString, SearchQueryType } from 'app/types/acl'; +import { ThunkResult } from 'app/types/store'; import { Input, diff --git a/public/app/features/manage-dashboards/utils/validation.test.ts b/public/app/features/manage-dashboards/utils/validation.test.ts index 40d1a8ee2b4..d6fb178bf80 100644 --- a/public/app/features/manage-dashboards/utils/validation.test.ts +++ b/public/app/features/manage-dashboards/utils/validation.test.ts @@ -5,7 +5,7 @@ import { import { AnnoKeyFolder, AnnoKeyFolderTitle } from 'app/features/apiserver/types'; import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; -import { DashboardDTO } from 'app/types'; +import { DashboardDTO } from 'app/types/dashboard'; import { validateUid } from './validation'; diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index c6a3bae02da..339184f1635 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -206,6 +206,8 @@ export type CreateSnapshotRequestDto = { }; export type MigrateDataResponseItemDto = { errorCode?: + | 'ALERT_RULES_QUOTA_REACHED' + | 'ALERT_RULES_GROUP_QUOTA_REACHED' | 'DATASOURCE_NAME_CONFLICT' | 'DATASOURCE_INVALID_URL' | 'DATASOURCE_ALREADY_MANAGED' diff --git a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx index a47efe48d43..63c194fbf73 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx @@ -13,6 +13,16 @@ interface ResourceDetailsModalProps { function getTMessage(errorCode: MigrateDataResponseItemDto['errorCode']): string { switch (errorCode) { + case 'ALERT_RULES_QUOTA_REACHED': + return t( + 'migrate-to-cloud.resource-details.error-messages.alert-rules-quota-reached', + 'Maximum number of alert rules reached: Delete some alert rules or upgrade your plan and try again.' + ); + case 'ALERT_RULES_GROUP_QUOTA_REACHED': + return t( + 'migrate-to-cloud.resource-details.error-messages.alert-rules-group-quota-reached', + 'Maximum number of alert rule groups reached: Delete some alert rule groups or upgrade your plan and try again.' + ); case 'DATASOURCE_NAME_CONFLICT': return t( 'migrate-to-cloud.resource-details.error-messages.datasource-name-conflict', @@ -106,9 +116,11 @@ export function ResourceDetailsModal(props: ResourceDetailsModalProps) { <> <Text element="p">{msgTitle}</Text> <Text element="p"> - {getTMessage(resource?.errorCode) || - resource?.message || - 'There has been an error while migrating. Please check the cloud migration logs for more information.'} + {getTMessage(resource?.errorCode) || resource?.message || ( + <Trans i18nKey="migrate-to-cloud.resource-details.error-messages.generic-error"> + There has been an error while migrating. Please check the cloud migration logs for more information. + </Trans> + )} </Text> </> ) : ( diff --git a/public/app/features/notifications/StoredNotifications.tsx b/public/app/features/notifications/StoredNotifications.tsx index 5443458036e..b5db2d1327a 100644 --- a/public/app/features/notifications/StoredNotifications.tsx +++ b/public/app/features/notifications/StoredNotifications.tsx @@ -14,7 +14,7 @@ import { selectWarningsAndErrors, selectLastReadTimestamp, } from 'app/core/reducers/appNotification'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; export function StoredNotifications() { const dispatch = useDispatch(); diff --git a/public/app/features/org/OrgDetailsPage.test.tsx b/public/app/features/org/OrgDetailsPage.test.tsx index 0fc7ab9e728..c71bfd1eff8 100644 --- a/public/app/features/org/OrgDetailsPage.test.tsx +++ b/public/app/features/org/OrgDetailsPage.test.tsx @@ -4,9 +4,9 @@ import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { TestProvider } from 'test/helpers/TestProvider'; import { NavModel } from '@grafana/data'; +import { Organization } from 'app/types/organization'; import { backendSrv } from '../../core/services/backend_srv'; -import { Organization } from '../../types'; import { OrgDetailsPage, Props } from './OrgDetailsPage'; import { setOrganizationName } from './state/reducers'; diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index d97faf6636b..bffc18ef3c7 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -7,8 +7,9 @@ import { Page } from 'app/core/components/Page/Page'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; import { appEvents, contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { ShowConfirmModalEvent } from 'app/types/events'; +import { StoreState } from 'app/types/store'; import OrgProfile from './OrgProfile'; import { loadOrganization, updateOrganization } from './state/actions'; diff --git a/public/app/features/org/OrgProfile.tsx b/public/app/features/org/OrgProfile.tsx index 23d56e2d92a..46b5cfa0c9b 100644 --- a/public/app/features/org/OrgProfile.tsx +++ b/public/app/features/org/OrgProfile.tsx @@ -2,7 +2,7 @@ import { Trans, t } from '@grafana/i18n'; import { Input, Field, FieldSet, Button } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export interface Props { orgName: string; diff --git a/public/app/features/org/SelectOrgPage.tsx b/public/app/features/org/SelectOrgPage.tsx index f9d93c63e93..3990613cbf8 100644 --- a/public/app/features/org/SelectOrgPage.tsx +++ b/public/app/features/org/SelectOrgPage.tsx @@ -5,7 +5,8 @@ import { Trans } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState, UserOrg } from 'app/types'; +import { StoreState } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; import { getUserOrganizations, setUserOrganization } from './state/actions'; diff --git a/public/app/features/org/UserInviteForm.tsx b/public/app/features/org/UserInviteForm.tsx index b4bc4bf67c2..1a0ad769b76 100644 --- a/public/app/features/org/UserInviteForm.tsx +++ b/public/app/features/org/UserInviteForm.tsx @@ -1,6 +1,6 @@ import { Controller } from 'react-hook-form'; -import { locationUtil, SelectableValue } from '@grafana/data'; +import { locationUtil, OrgRole, SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { @@ -18,7 +18,7 @@ import { Stack, } from '@grafana/ui'; import { getConfig } from 'app/core/config'; -import { OrgRole, useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { Form } from '../../core/components/Form/Form'; import { addInvitee } from '../invites/state/actions'; diff --git a/public/app/features/org/state/actions.test.ts b/public/app/features/org/state/actions.test.ts index dedda1a8fb5..b624430697a 100644 --- a/public/app/features/org/state/actions.test.ts +++ b/public/app/features/org/state/actions.test.ts @@ -1,8 +1,8 @@ import { thunkTester } from 'test/core/thunk/thunkTester'; +import { OrgRole } from '@grafana/data'; import { BackendSrv } from '@grafana/runtime'; import { updateConfigurationSubtitle } from 'app/core/actions'; -import { OrgRole } from 'app/types'; import { updateOrganization, setUserOrganization, getUserOrganizations } from './actions'; diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts index af1b46f49b0..1d580c5b3ae 100644 --- a/public/app/features/org/state/actions.ts +++ b/public/app/features/org/state/actions.ts @@ -1,6 +1,7 @@ import { getBackendSrv } from '@grafana/runtime'; import { updateConfigurationSubtitle } from 'app/core/actions'; -import { ThunkResult, UserOrg } from 'app/types'; +import { ThunkResult } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; import { organizationLoaded, userOrganizationsLoaded } from './reducers'; diff --git a/public/app/features/org/state/reducers.test.ts b/public/app/features/org/state/reducers.test.ts index b99e2ce762f..6bcc44a6aac 100644 --- a/public/app/features/org/state/reducers.test.ts +++ b/public/app/features/org/state/reducers.test.ts @@ -1,5 +1,7 @@ +import { OrgRole } from '@grafana/data'; +import { OrganizationState } from 'app/types/organization'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { OrganizationState, OrgRole } from '../../../types'; import { initialState, diff --git a/public/app/features/org/state/reducers.ts b/public/app/features/org/state/reducers.ts index d43850ffb2b..151c8cb40f0 100644 --- a/public/app/features/org/state/reducers.ts +++ b/public/app/features/org/state/reducers.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Organization, OrganizationState, UserOrg } from 'app/types'; +import { OrganizationState, Organization } from 'app/types/organization'; +import { UserOrg } from 'app/types/user'; export const initialState: OrganizationState = { organization: {} as Organization, diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 339a33d84a2..ba2ddcf7f8d 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -20,7 +20,7 @@ import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/Pa import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; import { findVizPanelByKey, getVizPanelKeyForPanelId } from 'app/features/dashboard-scene/utils/utils'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { changePanelPlugin } from '../state/actions'; diff --git a/public/app/features/panel/components/PanelPluginError.tsx b/public/app/features/panel/components/PanelPluginError.tsx index d4edf2d1027..39ec1eec5d0 100644 --- a/public/app/features/panel/components/PanelPluginError.tsx +++ b/public/app/features/panel/components/PanelPluginError.tsx @@ -5,7 +5,7 @@ import { PureComponent, ReactNode } from 'react'; import { PanelProps, PanelPlugin, PluginType, PanelPluginMeta } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert } from '@grafana/ui'; -import { AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; import grafanaIconSvg from 'img/grafana_icon.svg'; interface Props { diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index 6bde8fc0480..24d7c2b1a92 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -4,8 +4,8 @@ import { getLibraryPanel } from 'app/features/library-panels/state/api'; import { LibraryElementDTO } from 'app/features/library-panels/types'; import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError'; import { loadPanelPlugin } from 'app/features/plugins/admin/state/actions'; -import { ThunkResult } from 'app/types'; import { DashboardPanelsChangedEvent, PanelOptionsChangedEvent, PanelQueriesChangedEvent } from 'app/types/events'; +import { ThunkResult } from 'app/types/store'; import { changePanelKey, panelModelAndPluginReady, removePanel } from './reducers'; diff --git a/public/app/features/panel/state/selectors.ts b/public/app/features/panel/state/selectors.ts index 6851c061940..492e73af8e0 100644 --- a/public/app/features/panel/state/selectors.ts +++ b/public/app/features/panel/state/selectors.ts @@ -1,5 +1,5 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { PanelState } from './reducers'; diff --git a/public/app/features/playlist/PlaylistSrv.test.ts b/public/app/features/playlist/PlaylistSrv.test.ts index 77c45ca388b..9b7bed662a7 100644 --- a/public/app/features/playlist/PlaylistSrv.test.ts +++ b/public/app/features/playlist/PlaylistSrv.test.ts @@ -50,15 +50,16 @@ function createPlaylistSrv(): PlaylistSrv { } const mockWindowLocation = (): [jest.Mock, () => void] => { - const oldLocation = window.location; + const win: typeof globalThis = window; + const oldLocation = win.location; const hrefMock = jest.fn(); // JSDom defines window in a way that you cannot tamper with location so this seems to be the only way to change it. // https://github.com/facebook/jest/issues/5124#issuecomment-446659510 //@ts-ignore - delete window.location; + delete win.location; - window.location = {} as Location; + win.location = {} as Location; // Only mocking href as that is all this test needs, but otherwise there is lots of things missing, so keep that // in mind if this is reused. @@ -67,7 +68,7 @@ const mockWindowLocation = (): [jest.Mock, () => void] => { get: hrefMock, }); const unmock = () => { - window.location = oldLocation; + win.location = oldLocation; }; return [hrefMock, unmock]; }; diff --git a/public/app/features/plugins/admin/components/Badges/index.ts b/public/app/features/plugins/admin/components/Badges/index.ts deleted file mode 100644 index c62d5cecc71..00000000000 --- a/public/app/features/plugins/admin/components/Badges/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { PluginDisabledBadge } from './PluginDisabledBadge'; -export { PluginInstalledBadge } from './PluginInstallBadge'; -export { PluginEnterpriseBadge } from './PluginEnterpriseBadge'; -export { PluginUpdateAvailableBadge } from './PluginUpdateAvailableBadge'; -export { PluginDeprecatedBadge } from './PluginDeprecatedBadge'; diff --git a/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx b/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx index 710f90dc13b..c7e4bb6cb02 100644 --- a/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx +++ b/public/app/features/plugins/admin/components/ConnectionsTab.test.tsx @@ -5,15 +5,19 @@ import { DataSourceSettings } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; import { getMockDataSources } from 'app/features/datasources/mocks/dataSourcesMocks'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { datasourcePlugin } from '../mocks/catalogPlugin.mock'; import ConnectionsTab, { ConnectionsList } from './ConnectionsTab'; -jest.mock('app/features/datasources/state', () => ({ - ...jest.requireActual('app/features/datasources/state'), +jest.mock('app/features/datasources/state/hooks.ts', () => ({ + ...jest.requireActual('app/features/datasources/state/hooks.ts'), useLoadDataSource: jest.fn().mockReturnValue({ isLoading: false }), +})); + +jest.mock('app/features/datasources/state/selectors.ts', () => ({ + ...jest.requireActual('app/features/datasources/state/selectors.ts'), getDataSources: jest.fn(() => 'getDataSources mock implementation'), })); @@ -46,7 +50,7 @@ describe('<ConnectionsTab>', () => { setupContextSrv(); const mockedConnections = getMockDataSources(3, { type: datasourcePlugin.id }); mockedConnections[2].type = 'other-plugin-id'; - jest.requireMock('app/features/datasources/state').getDataSources.mockReturnValue(mockedConnections); + jest.requireMock('app/features/datasources/state/selectors.ts').getDataSources.mockReturnValue(mockedConnections); render(<ConnectionsTab plugin={datasourcePlugin} />); @@ -59,7 +63,9 @@ describe('<ConnectionsTab>', () => { it('should render add new datasource button when no datasources are defined', async () => { setupContextSrv(); - jest.requireMock('app/features/datasources/state').getDataSources.mockReturnValue(getMockDataSources(1)); + jest + .requireMock('app/features/datasources/state/selectors.ts') + .getDataSources.mockReturnValue(getMockDataSources(1)); render(<ConnectionsTab plugin={datasourcePlugin} />); expect(screen.getByText('Add new data source')).toBeVisible(); diff --git a/public/app/features/plugins/admin/components/ConnectionsTab.tsx b/public/app/features/plugins/admin/components/ConnectionsTab.tsx index e12bfec46d0..70a2a26f900 100644 --- a/public/app/features/plugins/admin/components/ConnectionsTab.tsx +++ b/public/app/features/plugins/admin/components/ConnectionsTab.tsx @@ -6,8 +6,10 @@ import { EmptyState, Stack, TextLink, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { ViewProps } from 'app/features/datasources/components/DataSourcesList'; import { DataSourcesListCard } from 'app/features/datasources/components/DataSourcesListCard'; -import { getDataSources, useLoadDataSources } from 'app/features/datasources/state'; -import { AccessControlAction, useSelector } from 'app/types'; +import { useLoadDataSources } from 'app/features/datasources/state/hooks'; +import { getDataSources } from 'app/features/datasources/state/selectors'; +import { AccessControlAction } from 'app/types/accessControl'; +import { useSelector } from 'app/types/store'; import { CatalogPlugin } from '../types'; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx index 6e0ab172f3a..f15eefad873 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithApp.tsx @@ -5,7 +5,7 @@ import { Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { Button } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { updatePluginSettings } from '../../api'; import { usePluginConfig } from '../../hooks/usePluginConfig'; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx index a1b80773d8a..b36b4a936b9 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx @@ -4,7 +4,7 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { PluginSignatureStatus } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CatalogPlugin } from '../../types'; diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx index cd58057ae3c..eba64519719 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.tsx @@ -6,8 +6,8 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button } from '@grafana/ui'; import { ROUTES } from 'app/features/connections/constants'; -import { addDataSource } from 'app/features/datasources/state'; -import { useDispatch } from 'app/types'; +import { addDataSource } from 'app/features/datasources/state/actions'; +import { useDispatch } from 'app/types/store'; import { isDataSourceEditor } from '../../permissions'; import { CatalogPlugin } from '../../types'; diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 288eba13443..5e5a61b0365 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -9,7 +9,7 @@ import { Button, ConfirmModal, LinkButton, Stack } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { removePluginFromNavTree } from 'app/core/reducers/navBarTree'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { getExternalManageLink, isDisabledAngularPlugin } from '../../helpers'; import { @@ -139,8 +139,11 @@ export function InstallControlsButton({ title={t('plugins.install-controls-button.title-uninstall-modal', 'Uninstall {{plugin}}', { plugin: plugin.name, })} - body="Are you sure you want to uninstall this plugin?" - confirmText="Confirm" + body={t( + 'plugins.install-controls-button.uninstall-controls.body-uninstall-plugin', + 'Are you sure you want to uninstall this plugin?' + )} + confirmText={t('plugins.install-controls-button.uninstall-controls.confirmText-confirm', 'Confirm')} icon="exclamation-triangle" onConfirm={onUninstall} onDismiss={hideConfirmModal} diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx index 1ccdc6e5311..99a198c1455 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { config, featureEnabled } from '@grafana/runtime'; import { LinkButton, useStyles2, Alert, TextLink, Stack } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { getExternalManageLink } from '../../helpers'; import { useIsRemotePluginsAvailable } from '../../state/hooks'; diff --git a/public/app/features/plugins/admin/components/InstallControls/index.tsx b/public/app/features/plugins/admin/components/InstallControls/index.tsx deleted file mode 100644 index b471668b365..00000000000 --- a/public/app/features/plugins/admin/components/InstallControls/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { InstallControlsWarning } from './InstallControlsWarning'; -export { InstallControlsButton } from './InstallControlsButton'; diff --git a/public/app/features/plugins/admin/components/PluginActions.tsx b/public/app/features/plugins/admin/components/PluginActions.tsx index edcd72cfb4c..d3e7f4a675c 100644 --- a/public/app/features/plugins/admin/components/PluginActions.tsx +++ b/public/app/features/plugins/admin/components/PluginActions.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { Icon, Stack, useStyles2 } from '@grafana/ui'; import { GetStartedWithPlugin } from '../components/GetStartedWithPlugin'; -import { InstallControlsButton } from '../components/InstallControls'; +import { InstallControlsButton } from '../components/InstallControls/InstallControlsButton'; import { getLatestCompatibleVersion, hasInstallControlWarning, diff --git a/public/app/features/plugins/admin/components/PluginDashboards.tsx b/public/app/features/plugins/admin/components/PluginDashboards.tsx index fafd1b19874..8d5caba5861 100644 --- a/public/app/features/plugins/admin/components/PluginDashboards.tsx +++ b/public/app/features/plugins/admin/components/PluginDashboards.tsx @@ -6,7 +6,7 @@ import { Trans } from '@grafana/i18n'; import { getBackendSrv } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import DashboardsTable from 'app/features/datasources/components/DashboardsTable'; -import { PluginDashboard } from 'app/types'; +import { PluginDashboard } from 'app/types/plugins'; interface Props { plugin: PluginMeta; diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index ef6f56c0559..d5a69de88ec 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -8,7 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Alert, Box, Stack, TabContent, TextLink, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; -import { AppNotificationSeverity } from 'app/types'; +import { AppNotificationSeverity } from 'app/types/appNotifications'; import { Loader } from '../components/Loader'; import { PluginDetailsBody } from '../components/PluginDetailsBody'; diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx index 206b5993162..b6b6cd9e084 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx @@ -3,13 +3,11 @@ import { PluginSignatureBadge, Stack } from '@grafana/ui'; import { isPluginUpdatable } from '../helpers'; import { CatalogPlugin } from '../types'; -import { - PluginEnterpriseBadge, - PluginDisabledBadge, - PluginInstalledBadge, - PluginUpdateAvailableBadge, - PluginDeprecatedBadge, -} from './Badges'; +import { PluginDeprecatedBadge } from './Badges/PluginDeprecatedBadge'; +import { PluginDisabledBadge } from './Badges/PluginDisabledBadge'; +import { PluginEnterpriseBadge } from './Badges/PluginEnterpriseBadge'; +import { PluginInstalledBadge } from './Badges/PluginInstallBadge'; +import { PluginUpdateAvailableBadge } from './Badges/PluginUpdateAvailableBadge'; type PluginBadgeType = { plugin: CatalogPlugin; diff --git a/public/app/features/plugins/admin/components/PluginSubtitle.tsx b/public/app/features/plugins/admin/components/PluginSubtitle.tsx index 6f8175908db..0a66d863938 100644 --- a/public/app/features/plugins/admin/components/PluginSubtitle.tsx +++ b/public/app/features/plugins/admin/components/PluginSubtitle.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Alert, Stack, useStyles2 } from '@grafana/ui'; -import { InstallControlsWarning } from '../components/InstallControls'; +import { InstallControlsWarning } from '../components/InstallControls/InstallControlsWarning'; import { getLatestCompatibleVersion, hasInstallControlWarning } from '../helpers'; import { useInstallStatus, useIsRemotePluginsAvailable } from '../state/hooks'; import { CatalogPlugin, PluginStatus } from '../types'; diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 056ce1fa22f..188ecccf65b 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -121,7 +121,11 @@ export const VersionInstallButton = ({ <ConfirmModal isOpen={isModalOpen} title={t('plugins.catalog.versions.downgrade-title', 'Downgrade plugin version')} - body={`${t('plugins.catalog.versions.confirmation-text-1', 'Are you really sure you want to downgrade to version')} ${version.version}? ${t('plugins.catalog.versions.confirmation-text-2', 'You should normally not be doing this')}`} + body={t( + 'plugins.catalog.versions.confirmation-text', + 'Are you really sure you want to downgrade to version {{version}}? You should normally not be doing this', + { version: version.version } + )} confirmText={t('plugins.catalog.versions.downgrade-confirm', 'Downgrade')} onConfirm={onConfirm} onDismiss={onDismiss} diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 38660fba145..171644b2546 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -2,10 +2,8 @@ import uFuzzy from '@leeoniya/ufuzzy'; import { PluginSignatureStatus, dateTimeParse, PluginError, PluginType, PluginErrorCode } from '@grafana/data'; import { config, featureEnabled } from '@grafana/runtime'; -import { Settings } from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { getBackendSrv } from 'app/core/services/backend_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { CatalogPlugin, @@ -341,14 +339,6 @@ function getPluginSignature(options: { return PluginSignatureStatus.missing; } -// Updates the core Grafana config to have the correct list available panels -export const updatePanels = () => - getBackendSrv() - .get('/api/frontend/settings') - .then((settings: Settings) => { - config.panels = settings.panels; - }); - export function getLatestCompatibleVersion(versions: Version[] | undefined): Version | undefined { if (!versions) { return; diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 26c700658ff..f0eebd3bd0a 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -5,7 +5,7 @@ import { GrafanaPlugin, NavModelItem, PluginIncludeType, PluginType } from '@gra import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { usePluginConfig } from '../hooks/usePluginConfig'; import { CatalogPlugin, PluginTabIds, PluginTabLabels } from '../types'; diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx index d8c37ecbd5d..92e75eb3ab9 100644 --- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -4,7 +4,7 @@ import { GrafanaTheme2, PluginSignatureType } from '@grafana/data'; import { t } from '@grafana/i18n'; import { PageInfoItem } from '../../../../core/components/Page/types'; -import { PluginDisabledBadge } from '../components/Badges'; +import { PluginDisabledBadge } from '../components/Badges/PluginDisabledBadge'; import { PluginDetailsHeaderDependencies } from '../components/PluginDetailsHeaderDependencies'; import { PluginDetailsHeaderSignature } from '../components/PluginDetailsHeaderSignature'; import { getLatestCompatibleVersion } from '../helpers'; diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 4cc44785f5b..a8b0ee559e7 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -10,7 +10,7 @@ import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { AdvisorRedirectNotice } from 'app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; -import { useSelector } from 'app/types'; +import { useSelector } from 'app/types/store'; import { HorizontalGroup } from '../components/HorizontalGroup'; import { PluginList } from '../components/PluginList'; diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index 42b83c85835..b725d3a9d58 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { GrafanaEdition } from '@grafana/data/internal'; import { selectors } from '@grafana/e2e-selectors'; -import { config } from '@grafana/runtime'; +import { config, getBackendSrv, setBackendSrv } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; import * as api from '../api'; @@ -30,10 +30,10 @@ import { import PluginDetailsPage from './PluginDetails'; jest.mock('@grafana/runtime', () => { - const original = jest.requireActual('@grafana/runtime'); - const mockedRuntime = { ...original }; - mockedRuntime.config.buildInfo.version = 'v8.1.0'; - return mockedRuntime; + const runtime = jest.requireActual('@grafana/runtime'); + runtime.config.buildInfo.version = 'v8.1.0'; + + return runtime; }); jest.mock('../hooks/usePluginConfig.tsx', () => ({ @@ -44,11 +44,6 @@ jest.mock('../hooks/usePluginConfig.tsx', () => ({ })), })); -jest.mock('../helpers.ts', () => ({ - ...jest.requireActual('../helpers.ts'), - updatePanels: jest.fn(), -})); - jest.mock('app/core/core', () => ({ contextSrv: { hasPermission: (action: string) => true, @@ -85,6 +80,7 @@ describe('Plugin details page', () => { const id = 'my-plugin'; const originalWindowLocation = window.location; let dateNow: jest.SpyInstance<number, []>; + const originalBackendSrv = getBackendSrv(); beforeAll(() => { dateNow = jest.spyOn(Date, 'now').mockImplementation(() => 1609470000000); // 2021-01-01 04:00:00 @@ -100,6 +96,7 @@ describe('Plugin details page', () => { jest.clearAllMocks(); config.pluginAdminExternalManageEnabled = false; config.licenseInfo.enabledFeatures = {}; + setBackendSrv(originalBackendSrv); }); afterAll(() => { @@ -422,6 +419,11 @@ describe('Plugin details page', () => { // @ts-ignore api.uninstallPlugin = jest.fn(); + setBackendSrv({ + ...originalBackendSrv, + get: jest.fn().mockResolvedValue({ panels: [] }), + }); + const { queryByText, getByRole, findByRole, user } = renderPluginDetails({ id, name: 'Akumuli', diff --git a/public/app/features/plugins/admin/permissions.ts b/public/app/features/plugins/admin/permissions.ts index e0b7d752881..4ea5eccdd34 100644 --- a/public/app/features/plugins/admin/permissions.ts +++ b/public/app/features/plugins/admin/permissions.ts @@ -1,6 +1,6 @@ import { config } from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; export function isGrafanaAdmin(): boolean { return config.bootData.user.isGrafanaAdmin; diff --git a/public/app/features/plugins/admin/routes.tsx b/public/app/features/plugins/admin/routes.tsx index ab7fb2d13ce..6b85ddc14e7 100644 --- a/public/app/features/plugins/admin/routes.tsx +++ b/public/app/features/plugins/admin/routes.tsx @@ -1,7 +1,7 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { contextSrv } from 'app/core/core'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PluginAdminRoutes } from './types'; diff --git a/public/app/features/plugins/admin/state/actions.ts b/public/app/features/plugins/admin/state/actions.ts index e08e7241c42..599a588d160 100644 --- a/public/app/features/plugins/admin/state/actions.ts +++ b/public/app/features/plugins/admin/state/actions.ts @@ -3,8 +3,9 @@ import { from, forkJoin, timeout, lastValueFrom, catchError, of } from 'rxjs'; import { PanelPlugin, PluginError } from '@grafana/data'; import { config, getBackendSrv, isFetchError } from '@grafana/runtime'; +import { Settings } from 'app/core/config'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; -import { StoreState, ThunkResult } from 'app/types'; +import { StoreState, ThunkResult } from 'app/types/store'; import { invalidatePluginInCache } from '../../loader/cache'; import { @@ -18,7 +19,7 @@ import { getProvisionedPlugins, } from '../api'; import { STATE_PREFIX } from '../constants'; -import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers'; +import { mapLocalToCatalog, mergeLocalsAndRemotes } from '../helpers'; import { CatalogPlugin, RemotePlugin, LocalPlugin, InstancePlugin, ProvisionedPlugin, PluginStatus } from '../types'; // Fetches @@ -278,3 +279,11 @@ export const loadPanelPlugin = (id: string): ThunkResult<Promise<PanelPlugin>> = return plugin; }; }; + +function updatePanels() { + return getBackendSrv() + .get('/api/frontend/settings') + .then((settings: Settings) => { + config.panels = settings.panels; + }); +} diff --git a/public/app/features/plugins/admin/state/hooks.ts b/public/app/features/plugins/admin/state/hooks.ts index 457f2f1d73e..2185ec99465 100644 --- a/public/app/features/plugins/admin/state/hooks.ts +++ b/public/app/features/plugins/admin/state/hooks.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; import { PluginError, PluginType } from '@grafana/data'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { sortPlugins, Sorters, isPluginUpdatable } from '../helpers'; import { CatalogPlugin, PluginStatus } from '../types'; diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 791a6d4eb28..eaedf1766ee 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -9,7 +9,8 @@ import { WithAccessControlMetadata, } from '@grafana/data'; import { IconName } from '@grafana/ui'; -import { StoreState, PluginsState } from 'app/types'; +import { PluginsState } from 'app/types/plugins'; +import { StoreState } from 'app/types/store'; export type PluginTypeCode = 'app' | 'panel' | 'datasource'; diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index bef9ed62306..1b054508724 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -15,15 +15,15 @@ import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsR import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry'; import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry'; import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry'; +import { importAppPlugin } from '../pluginLoader'; import { getPluginSettings } from '../pluginSettings'; -import { importAppPlugin } from '../plugin_loader'; import AppRootPage from './AppRootPage'; jest.mock('../pluginSettings', () => ({ getPluginSettings: jest.fn(), })); -jest.mock('../plugin_loader', () => ({ +jest.mock('../pluginLoader', () => ({ importAppPlugin: jest.fn(), })); diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index 3bb8d1c3169..7210f0ff471 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -16,7 +16,7 @@ import { } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config, locationSearchToObject } from '@grafana/runtime'; -import { Alert } from '@grafana/ui'; +import { Alert, ErrorWithStack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; @@ -32,10 +32,11 @@ import { useExposedComponentsRegistry, useAddedFunctionsRegistry, } from '../extensions/ExtensionRegistriesContext'; +import { importAppPlugin } from '../pluginLoader'; import { getPluginSettings } from '../pluginSettings'; -import { importAppPlugin } from '../plugin_loader'; import { buildPluginSectionNav, pluginsLogger } from '../utils'; +import { PluginErrorBoundary } from './PluginErrorBoundary'; import { buildPluginPageContext, PluginPageContext } from './PluginPageContext'; interface Props { @@ -106,22 +107,32 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { const pluginRoot = plugin.root && ( <PluginContextProvider meta={plugin.meta}> - <ExtensionRegistriesProvider - registries={{ - addedLinksRegistry: addedLinksRegistry.readOnly(), - addedComponentsRegistry: addedComponentsRegistry.readOnly(), - exposedComponentsRegistry: exposedComponentsRegistry.readOnly(), - addedFunctionsRegistry: addedFunctionsRegistry.readOnly(), - }} + <PluginErrorBoundary + fallback={({ error, errorInfo }) => ( + <ErrorWithStack + title={t('plugins.app-root-page.error-loading-plugin', 'Plugin failed to load')} + error={error} + errorInfo={errorInfo} + /> + )} > - <plugin.root - meta={plugin.meta} - basename={location.pathname} - onNavChanged={onNavChanged} - query={queryParams} - path={location.pathname} - /> - </ExtensionRegistriesProvider> + <ExtensionRegistriesProvider + registries={{ + addedLinksRegistry: addedLinksRegistry.readOnly(), + addedComponentsRegistry: addedComponentsRegistry.readOnly(), + exposedComponentsRegistry: exposedComponentsRegistry.readOnly(), + addedFunctionsRegistry: addedFunctionsRegistry.readOnly(), + }} + > + <plugin.root + meta={plugin.meta} + basename={location.pathname} + onNavChanged={onNavChanged} + query={queryParams} + path={location.pathname} + /> + </ExtensionRegistriesProvider> + </PluginErrorBoundary> </PluginContextProvider> ); diff --git a/public/app/features/plugins/components/PluginErrorBoundary.test.tsx b/public/app/features/plugins/components/PluginErrorBoundary.test.tsx new file mode 100644 index 00000000000..377e508a33d --- /dev/null +++ b/public/app/features/plugins/components/PluginErrorBoundary.test.tsx @@ -0,0 +1,184 @@ +import { render, screen } from '@testing-library/react'; +import * as React from 'react'; + +import { PluginMeta, PluginType, PluginContext } from '@grafana/data'; +import { getMockPlugin } from '@grafana/data/test'; + +import { PluginErrorBoundary } from './PluginErrorBoundary'; + +const ThrowingComponent = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error('Test error message'); + } + return <div>Working component</div>; +}; + +const TestFallback = ({ error, errorInfo }: { error: Error | null; errorInfo: React.ErrorInfo | null }) => ( + <div> + <div>Fallback rendered</div> + <div>Error: {error?.message}</div> + {errorInfo && <div>Error info available</div>} + </div> +); + +const renderWithPluginContext = ({ + children, + pluginMeta, + fallback, + onError, +}: { + children: React.ReactNode; + pluginMeta?: PluginMeta; + fallback?: React.ComponentType<{ error: Error | null; errorInfo: React.ErrorInfo | null }>; + onError?: (error: Error, info: React.ErrorInfo) => void; +}) => { + const mockPluginMeta = pluginMeta || getMockPlugin({ id: 'test-plugin', type: PluginType.panel }); + + return render( + <PluginContext.Provider value={{ meta: mockPluginMeta }}> + <PluginErrorBoundary fallback={fallback} onError={onError}> + {children} + </PluginErrorBoundary> + </PluginContext.Provider> + ); +}; + +describe('PluginErrorBoundary', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('should render children normally when no error occurs', () => { + renderWithPluginContext({ children: <ThrowingComponent shouldThrow={false} /> }); + + expect(screen.getByText('Working component')).toBeInTheDocument(); + }); + + it('should render null when an error occurs and no fallback is provided', () => { + const { container } = renderWithPluginContext({ children: <ThrowingComponent shouldThrow={true} /> }); + + expect(container.firstChild).toBeNull(); + }); + + it('should render custom fallback component when an error occurs', () => { + renderWithPluginContext({ children: <ThrowingComponent shouldThrow={true} />, fallback: TestFallback }); + + expect(screen.getByText('Fallback rendered')).toBeInTheDocument(); + expect(screen.getByText('Error: Test error message')).toBeInTheDocument(); + expect(screen.getByText('Error info available')).toBeInTheDocument(); + }); + + it('should call onError callback when an error occurs', () => { + const onErrorMock = jest.fn(); + + renderWithPluginContext({ children: <ThrowingComponent shouldThrow={true} />, onError: onErrorMock }); + + expect(onErrorMock).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Test error message' }), + expect.objectContaining({ + componentStack: expect.any(String), + }) + ); + }); + + it('should log error to console with plugin ID when no onError callback is provided', () => { + const mockPluginMeta = getMockPlugin({ id: 'my-test-plugin', type: PluginType.datasource }); + + renderWithPluginContext({ children: <ThrowingComponent shouldThrow={true} />, pluginMeta: mockPluginMeta }); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Plugin "my-test-plugin" failed to load:', + expect.objectContaining({ message: 'Test error message' }), + expect.objectContaining({ + componentStack: expect.any(String), + }) + ); + }); + + it('should handle error when plugin context is not available', () => { + render( + <PluginErrorBoundary> + <ThrowingComponent shouldThrow={true} /> + </PluginErrorBoundary> + ); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Plugin "undefined" failed to load:', + expect.objectContaining({ message: 'Test error message' }), + expect.objectContaining({ + componentStack: expect.any(String), + }) + ); + }); + + it('should update state correctly when error occurs', () => { + renderWithPluginContext({ children: <ThrowingComponent shouldThrow={true} />, fallback: TestFallback }); + + // Verify that both error and errorInfo are available in the fallback + expect(screen.getByText('Error: Test error message')).toBeInTheDocument(); + expect(screen.getByText('Error info available')).toBeInTheDocument(); + }); + + it('should reset error state when children change to non-throwing component', () => { + const { rerender } = renderWithPluginContext({ + children: <ThrowingComponent shouldThrow={true} />, + fallback: TestFallback, + }); + + // Initially should show fallback + expect(screen.getByText('Fallback rendered')).toBeInTheDocument(); + + // Re-render with non-throwing component + rerender( + <PluginContext.Provider value={{ meta: getMockPlugin({ id: 'test-plugin', type: PluginType.panel }) }}> + <PluginErrorBoundary fallback={TestFallback}> + <ThrowingComponent shouldThrow={false} /> + </PluginErrorBoundary> + </PluginContext.Provider> + ); + + // Should still show fallback since error boundary doesn't reset automatically + expect(screen.getByText('Fallback rendered')).toBeInTheDocument(); + }); + + it('should handle multiple children correctly', () => { + renderWithPluginContext({ + children: ( + <> + <div>First child</div> + <ThrowingComponent shouldThrow={false} /> + <div>Third child</div> + </> + ), + }); + + expect(screen.getByText('First child')).toBeInTheDocument(); + expect(screen.getByText('Working component')).toBeInTheDocument(); + expect(screen.getByText('Third child')).toBeInTheDocument(); + }); + + it('should handle error in one of multiple children', () => { + renderWithPluginContext({ + children: ( + <> + <div>First child</div> + <ThrowingComponent shouldThrow={true} /> + <div>Third child</div> + </> + ), + fallback: TestFallback, + }); + + // Should show fallback and not render any of the children + expect(screen.getByText('Fallback rendered')).toBeInTheDocument(); + expect(screen.queryByText('First child')).not.toBeInTheDocument(); + expect(screen.queryByText('Third child')).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/plugins/components/PluginErrorBoundary.tsx b/public/app/features/plugins/components/PluginErrorBoundary.tsx new file mode 100644 index 00000000000..6495acfeaf3 --- /dev/null +++ b/public/app/features/plugins/components/PluginErrorBoundary.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import { PluginContext } from '@grafana/data'; + +interface PluginErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ComponentType<{ error: Error | null; errorInfo: React.ErrorInfo | null }>; + onError?: (error: Error, info: React.ErrorInfo) => void; +} + +interface PluginErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +export class PluginErrorBoundary extends React.Component<PluginErrorBoundaryProps, PluginErrorBoundaryState> { + static contextType = PluginContext; + + declare context: React.ContextType<typeof PluginContext>; + + constructor(props: PluginErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error: Error): PluginErrorBoundaryState { + return { hasError: true, error: error, errorInfo: null }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + if (this.props.onError) { + this.props.onError(error, info); + } else { + console.error(`Plugin "${this.context?.meta.id}" failed to load:`, error, info); + } + + this.setState({ error, errorInfo: info }); + } + + render() { + const Fallback = this.props.fallback; + if (this.state.hasError) { + return Fallback ? <Fallback error={this.state.error} errorInfo={this.state.errorInfo} /> : null; + } + + return this.props.children; + } +} diff --git a/public/app/features/plugins/datasource_srv.test.ts b/public/app/features/plugins/datasource_srv.test.ts index 94e714ce35c..9734aaa9b02 100644 --- a/public/app/features/plugins/datasource_srv.test.ts +++ b/public/app/features/plugins/datasource_srv.test.ts @@ -61,7 +61,7 @@ class TestRuntimeDataSource extends RuntimeDataSource { } } -jest.mock('./plugin_loader', () => ({ +jest.mock('./pluginLoader', () => ({ importDataSourcePlugin: (meta: DataSourcePluginMeta) => { return Promise.resolve(new DataSourcePlugin(TestDataSource as any)); }, diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index f434681943f..a7d081244c6 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -29,7 +29,7 @@ import { } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionDatasourceUID } from 'app/features/expressions/types'; -import { importDataSourcePlugin } from './plugin_loader'; +import { importDataSourcePlugin } from './pluginLoader'; export class DatasourceSrv implements DataSourceService { private datasources: Record<string, DataSourceApi> = {}; // UID diff --git a/public/app/features/plugins/extensions/ExtensionErrorAlert.tsx b/public/app/features/plugins/extensions/ExtensionErrorAlert.tsx new file mode 100644 index 00000000000..41b1b021d34 --- /dev/null +++ b/public/app/features/plugins/extensions/ExtensionErrorAlert.tsx @@ -0,0 +1,20 @@ +import { t } from '@grafana/i18n'; +import { Alert } from '@grafana/ui'; + +export const ExtensionErrorAlert = ({ pluginId, extensionTitle }: { pluginId: string; extensionTitle: string }) => { + return ( + <Alert + title={t( + 'plugins.extensions.extension-error-alert-title', + 'Extension failed to load: "{{pluginId}}/{{extensionTitle}}"', + { + pluginId, + extensionTitle, + } + )} + severity="error" + > + {t('plugins.extensions.extension-error-alert-description', 'Check the console for more details on the error.')} + </Alert> + ); +}; diff --git a/public/app/features/plugins/extensions/ExtensionErrorBoundary.tsx b/public/app/features/plugins/extensions/ExtensionErrorBoundary.tsx new file mode 100644 index 00000000000..5d85e16eee1 --- /dev/null +++ b/public/app/features/plugins/extensions/ExtensionErrorBoundary.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; + +import { PluginErrorBoundary } from '../components/PluginErrorBoundary'; + +import { ExtensionErrorAlert } from './ExtensionErrorAlert'; +import { ExtensionsLog, log as baseLog } from './logs/log'; +import { isGrafanaDevMode } from './utils'; + +export const ExtensionErrorBoundary = ({ + children, + pluginId, + extensionTitle, + log = baseLog, + fallbackAlwaysVisible = false, +}: { + children: React.ReactNode; + pluginId: string; + extensionTitle: string; + log?: ExtensionsLog; + fallbackAlwaysVisible?: boolean; +}) => { + return ( + <PluginErrorBoundary + onError={(error, errorInfo) => { + log.error(`Extension "${pluginId}/${extensionTitle}" failed to load.`, { + message: error.message, + componentStack: errorInfo.componentStack ?? '', + digest: errorInfo.digest ?? '', + }); + }} + fallback={() => { + if (isGrafanaDevMode() || fallbackAlwaysVisible) { + return <ExtensionErrorAlert pluginId={pluginId} extensionTitle={extensionTitle} />; + } + + return null; + }} + > + {children} + </PluginErrorBoundary> + ); +}; diff --git a/public/app/features/plugins/extensions/errors.ts b/public/app/features/plugins/extensions/errors.ts index cb9d22e7b46..3ef7a93a638 100644 --- a/public/app/features/plugins/extensions/errors.ts +++ b/public/app/features/plugins/extensions/errors.ts @@ -1,5 +1,13 @@ export const INVALID_EXTENSION_POINT_ID = - 'Invalid usage of extension point. Reason: Extension point id should be prefixed with your plugin id, e.g "myorg-foo-app/toolbar/v1".'; + 'Invalid usage of extension point. Reason: Extension point id should be prefixed with your plugin id, e.g "myorg-foo-app/toolbar/v1". Returning an empty array of extensions.'; + +export const INVALID_EXTENSION_POINT_ID_PLUGIN = (pluginId: string, extensionPointId: string) => + `Invalid usage of extension point. Reason: Extension point id should be prefixed with your plugin id, e.g "${pluginId}/${extensionPointId}".`; + +export const INVALID_EXTENSION_POINT_ID_GRAFANA_PREFIX = (extensionPointId: string) => + `Invalid usage of extension point. Reason: Core Grafana extension point id should be prefixed with "grafana/", e.g "grafana/${extensionPointId}".`; + +export const INVALID_EXTENSION_POINT_ID_GRAFANA_EXPOSED = `Invalid usage of extension point. Reason: Core Grafana extension point id should be exposed to plugins via the "PluginExtensionPoints" enum in the "grafana-data" package (/packages/grafana-data/src/types/pluginExtensions.ts).`; export const EXTENSION_POINT_META_INFO_MISSING = 'Invalid usage of extension point. Reason: The extension point is not recorded in the "plugin.json" file. Extension points must be listed in the section "extensions.extensionPoints[]". Returning an empty array of extensions.'; diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index aed7866e9b2..5968bb6fc42 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -172,7 +172,12 @@ export const getPluginExtensions: GetExtensions = ({ pluginId: addedComponent.pluginId, title: addedComponent.title, description: addedComponent.description ?? '', - component: wrapWithPluginContext(addedComponent.pluginId, addedComponent.component, componentLog), + component: wrapWithPluginContext({ + pluginId: addedComponent.pluginId, + extensionTitle: addedComponent.title, + Component: addedComponent.component, + log: componentLog, + }), }; extensions.push(extension); diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts index d4b8b0b04f5..de732ce8c6d 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.ts @@ -62,7 +62,12 @@ export class AddedComponentsRegistry extends Registry< const result = { pluginId, - component: wrapWithPluginContext(pluginId, config.component, pointIdLog), + component: wrapWithPluginContext({ + pluginId, + extensionTitle: config.title, + Component: config.component, + log: pointIdLog, + }), description: config.description, title: config.title, }; diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 9ce151c2200..51341e2051f 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -382,7 +382,7 @@ describe('usePluginComponent()', () => { // Should log an error in dev mode expect(log.error).toHaveBeenCalledWith( - 'Attempted to mutate object property "c" from extension with id myorg-extensions-app', + 'Attempted to mutate object property "c" from extension with id myorg-extensions-app and version unknown', { stack: expect.any(String), } @@ -438,7 +438,7 @@ describe('usePluginComponent()', () => { // Should log a warning expect(log.warning).toHaveBeenCalledWith( - 'Attempted to mutate object property "c" from extension with id myorg-extensions-app', + 'Attempted to mutate object property "c" from extension with id myorg-extensions-app and version unknown', { stack: expect.any(String), } diff --git a/public/app/features/plugins/extensions/usePluginComponent.tsx b/public/app/features/plugins/extensions/usePluginComponent.tsx index 5e079beea93..7a2d3669b24 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.tsx @@ -54,7 +54,12 @@ export function usePluginComponent<Props extends object = {}>(id: string): UsePl return { isLoading: false, - component: wrapWithPluginContext(registryItem.pluginId, registryItem.component, componentLog), + component: wrapWithPluginContext({ + pluginId: registryItem.pluginId, + extensionTitle: registryItem.title, + Component: registryItem.component, + log: componentLog, + }), }; }, [id, pluginContext, registryState, isLoadingAppPlugins]); } diff --git a/public/app/features/plugins/extensions/usePluginComponents.test.tsx b/public/app/features/plugins/extensions/usePluginComponents.test.tsx index dc8b84a7287..f40955c3f54 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.test.tsx @@ -1,7 +1,7 @@ import { act, render, renderHook, screen } from '@testing-library/react'; import React from 'react'; -import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; +import { PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; @@ -267,7 +267,7 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render(<Component foo={originalFoo} override />)).not.toThrow(); expect(log.error).toHaveBeenCalledWith( - `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app`, + `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app and version unknown`, { stack: expect.any(String), } @@ -331,7 +331,7 @@ describe('usePluginComponents()', () => { // Should also render the component if it wants to change the props expect(() => render(<Component foo={originalFoo} override />)).not.toThrow(); expect(log.warning).toHaveBeenCalledWith( - `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app`, + `Attempted to mutate object property "foo4" from extension with id myorg-extensions-app and version unknown`, { stack: expect.any(String), } @@ -498,7 +498,7 @@ describe('usePluginComponents()', () => { pluginId: 'grafana', // Only core Grafana can register extensions without a plugin context configs: [ { - targets: 'grafana/extension-point/v1', + targets: PluginExtensionPoints.DashboardPanelMenu, title: '1', description: '1', component: () => <div>Component</div>, @@ -506,14 +506,17 @@ describe('usePluginComponents()', () => { ], }); - let { result } = renderHook(() => usePluginComponents({ extensionPointId: 'grafana/extension-point/v1' }), { - wrapper, - }); + let { result } = renderHook( + () => usePluginComponents({ extensionPointId: PluginExtensionPoints.DashboardPanelMenu }), + { + wrapper, + } + ); expect(result.current.components.length).toBe(1); expect(log.error).not.toHaveBeenCalled(); }); - it('should not validate the extension point id if used in Grafana core (no plugin context)', () => { + it('should not allow to create an extension point in core Grafana that is not exposed to plugins', () => { // Imitate running in dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); @@ -522,11 +525,26 @@ describe('usePluginComponents()', () => { <ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider> ); - let { result } = renderHook(() => usePluginComponents({ extensionPointId: 'invalid-extension-point-id' }), { + const extensionPointId = 'grafana/not-exposed-extension-point/v1'; + + // Adding an extension to the extension point + registries.addedComponentsRegistry.register({ + pluginId: 'grafana', // Only core Grafana can register extensions without a plugin context + configs: [ + { + targets: extensionPointId, + title: '1', + description: '1', + component: () => <div>Component</div>, + }, + ], + }); + + let { result } = renderHook(() => usePluginComponents({ extensionPointId }), { wrapper, }); expect(result.current.components.length).toBe(0); - expect(log.error).not.toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); it('should validate if the extension point meta-info is correct if in dev-mode and used by a plugin', () => { diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index 58d95289c15..6f37f3bdbba 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -28,8 +28,7 @@ export function usePluginComponents<Props extends object = {}>({ const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId)); return useMemo(() => { - // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. - const enableRestrictions = isGrafanaDevMode() && pluginContext; + const isInsidePlugin = Boolean(pluginContext); const components: Array<ComponentTypeWithExtensionMeta<Props>> = []; const extensionsByPlugin: Record<string, number> = {}; const pluginId = pluginContext?.meta.id ?? ''; @@ -38,13 +37,16 @@ export function usePluginComponents<Props extends object = {}>({ extensionPointId, }); - // Only log error for an invalid `extensionPointId` in DEV mode - if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - pointLog.error(errors.INVALID_EXTENSION_POINT_ID); + // Don't show extensions if the extension-point id is invalid in DEV mode + if (isGrafanaDevMode() && !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, log: pointLog })) { + return { + isLoading: false, + components: [], + }; } // Don't show extensions if the extension-point misses meta info (plugin.json) in DEV mode - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + if (isGrafanaDevMode() && pluginContext && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, diff --git a/public/app/features/plugins/extensions/usePluginFunctions.tsx b/public/app/features/plugins/extensions/usePluginFunctions.tsx index 68acee76221..18a8abd7aef 100644 --- a/public/app/features/plugins/extensions/usePluginFunctions.tsx +++ b/public/app/features/plugins/extensions/usePluginFunctions.tsx @@ -23,8 +23,7 @@ export function usePluginFunctions<Signature>({ const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps); return useMemo(() => { - // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. - const enableRestrictions = isGrafanaDevMode() && pluginContext; + const isInsidePlugin = Boolean(pluginContext); const results: Array<PluginExtensionFunction<Signature>> = []; const extensionsByPlugin: Record<string, number> = {}; const pluginId = pluginContext?.meta.id ?? ''; @@ -32,11 +31,15 @@ export function usePluginFunctions<Signature>({ pluginId, extensionPointId, }); - if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - pointLog.error(errors.INVALID_EXTENSION_POINT_ID); + + if (isGrafanaDevMode() && !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, log: pointLog })) { + return { + isLoading: false, + functions: [], + }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + if (isGrafanaDevMode() && pluginContext && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, diff --git a/public/app/features/plugins/extensions/usePluginLinks.test.tsx b/public/app/features/plugins/extensions/usePluginLinks.test.tsx index f9fb623f42a..b5d984b165c 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.test.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.test.tsx @@ -1,6 +1,6 @@ import { act, renderHook } from '@testing-library/react'; -import { PluginContextProvider, PluginMeta, PluginType } from '@grafana/data'; +import { PluginContextProvider, PluginExtensionPoints, PluginMeta, PluginType } from '@grafana/data'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; @@ -256,7 +256,7 @@ describe('usePluginLinks()', () => { pluginId: 'grafana', // Only core Grafana can register extensions without a plugin context configs: [ { - targets: 'grafana/extension-point/v1', + targets: PluginExtensionPoints.DashboardPanelMenu, title: '1', description: '1', path: `/a/grafana/${pluginId}/2`, @@ -264,11 +264,42 @@ describe('usePluginLinks()', () => { ], }); - let { result } = renderHook(() => usePluginLinks({ extensionPointId: 'grafana/extension-point/v1' }), { wrapper }); + let { result } = renderHook(() => usePluginLinks({ extensionPointId: PluginExtensionPoints.DashboardPanelMenu }), { + wrapper, + }); expect(result.current.links.length).toBe(1); expect(log.warning).not.toHaveBeenCalled(); }); + it('should not allow to create an extension point in core Grafana that is not exposed to plugins', () => { + // Imitate running in dev mode + jest.mocked(isGrafanaDevMode).mockReturnValue(true); + + // No plugin context -> used in Grafana core + wrapper = ({ children }: { children: React.ReactNode }) => ( + <ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider> + ); + + const extensionPointId = 'grafana/not-exposed-extension-point/v1'; + + // Adding an extension to the extension point + registries.addedLinksRegistry.register({ + pluginId: 'grafana', // Only core Grafana can register extensions without a plugin context + configs: [ + { + targets: extensionPointId, + title: '1', + description: '1', + path: `/a/grafana/${pluginId}/2`, + }, + ], + }); + + let { result } = renderHook(() => usePluginLinks({ extensionPointId }), { wrapper }); + expect(result.current.links.length).toBe(0); + expect(log.error).toHaveBeenCalled(); + }); + it('should not validate the extension point id if used in Grafana core (no plugin context)', () => { // Imitate running in dev mode jest.mocked(isGrafanaDevMode).mockReturnValue(true); diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index 6ecaf6b71b9..adec8114582 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -32,23 +32,21 @@ export function usePluginLinks({ const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId)); return useMemo(() => { - // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. - const enableRestrictions = isGrafanaDevMode() && pluginContext !== null; + const isInsidePlugin = Boolean(pluginContext); const pluginId = pluginContext?.meta.id ?? ''; const pointLog = log.child({ pluginId, extensionPointId, }); - if (enableRestrictions && !isExtensionPointIdValid({ extensionPointId, pluginId })) { - pointLog.error(errors.INVALID_EXTENSION_POINT_ID); + if (isGrafanaDevMode() && !isExtensionPointIdValid({ extensionPointId, pluginId, isInsidePlugin, log: pointLog })) { return { isLoading: false, links: [], }; } - if (enableRestrictions && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { + if (isGrafanaDevMode() && pluginContext && isExtensionPointMetaInfoMissing(extensionPointId, pluginContext)) { pointLog.error(errors.EXTENSION_POINT_META_INFO_MISSING); return { isLoading: false, diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 2755d98d064..33da958ae89 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { type Unsubscribable } from 'rxjs'; import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data'; @@ -7,6 +7,7 @@ import appEvents from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; import { log } from './logs/log'; +import { resetLogMock } from './logs/testUtils'; import { deepFreeze, handleErrorsInFn, @@ -26,18 +27,25 @@ import { jest.mock('app/features/plugins/pluginSettings', () => ({ ...jest.requireActual('app/features/plugins/pluginSettings'), - getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' } }), + getPluginSettings: () => Promise.resolve({ info: { version: '1.0.0' }, id: 'test-plugin' }), })); describe('Plugin Extensions / Utils', () => { + const originalEnv = config.buildInfo.env; + beforeEach(() => { jest.spyOn(log, 'error').mockImplementation(() => {}); jest.spyOn(log, 'warning').mockImplementation(() => {}); jest.spyOn(log, 'debug').mockImplementation(() => {}); + jest.spyOn(log, 'info').mockImplementation(() => {}); + jest.spyOn(log, 'trace').mockImplementation(() => {}); + jest.spyOn(log, 'fatal').mockImplementation(() => {}); }); afterEach(() => { jest.resetAllMocks(); + + config.buildInfo.env = originalEnv; }); describe('deepFreeze()', () => { @@ -391,14 +399,17 @@ describe('Plugin Extensions / Utils', () => { }); it('should be possible to modify values in proxied object, but logs an error', () => { - const proxy = getMutationObserverProxy({ a: 'a' }, { pluginId: 'myorg-cool-datasource', source: 'datasource' }); + const proxy = getMutationObserverProxy( + { a: 'a' }, + { pluginId: 'myorg-cool-datasource', source: 'datasource', pluginVersion: '1.2.3' } + ); expect(() => { proxy.a = 'b'; }).not.toThrow(); expect(log.error).toHaveBeenCalledWith( - `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource and version 1.2.3`, { stack: expect.any(String), } @@ -419,7 +430,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.debug).toHaveBeenCalledWith( - `Attempted to define object property "b" from extension with id myorg-cool-extension`, + `Attempted to define object property "b" from extension with id myorg-cool-extension and version unknown`, { stack: expect.any(String), } @@ -442,7 +453,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.error).toHaveBeenCalledWith( - `Attempted to delete object property "c" from extension with id unknown`, + `Attempted to delete object property "c" from extension with id unknown and version unknown`, { stack: expect.any(String), } @@ -465,7 +476,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.warning).toHaveBeenCalledWith( - `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource and version unknown`, { stack: expect.any(String), } @@ -486,7 +497,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.debug).toHaveBeenCalledWith( - `Attempted to define object property "b" from extension with id myorg-cool-extension`, + `Attempted to define object property "b" from extension with id myorg-cool-extension and version unknown`, { stack: expect.any(String), } @@ -509,7 +520,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.warning).toHaveBeenCalledWith( - `Attempted to delete object property "c" from extension with id unknown`, + `Attempted to delete object property "c" from extension with id unknown and version unknown`, { stack: expect.any(String), } @@ -540,7 +551,11 @@ describe('Plugin Extensions / Utils', () => { config.buildInfo.env = 'development'; const obj = { a: 'a' }; - const copy = writableProxy(obj, { source: 'datasource', pluginId: 'myorg-cool-datasource' }); + const copy = writableProxy(obj, { + source: 'datasource', + pluginId: 'myorg-cool-datasource', + pluginVersion: '1.2.3', + }); expect(copy).not.toBe(obj); expect(copy.a).toBe('a'); @@ -550,7 +565,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.error).toHaveBeenCalledWith( - `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource and version 1.2.3`, { stack: expect.any(String), } @@ -573,7 +588,7 @@ describe('Plugin Extensions / Utils', () => { }).not.toThrow(); expect(log.warning).toHaveBeenCalledWith( - `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource`, + `Attempted to mutate object property "a" from datasource with id myorg-cool-datasource and version unknown`, { stack: expect.any(String), } @@ -597,9 +612,12 @@ describe('Plugin Extensions / Utils', () => { expect(Object.isFrozen(copy.b)).toBe(true); expect(copy.b).toEqual({ c: 'c' }); - expect(log.debug).toHaveBeenCalledWith(`Attempted to define object property "a" from extension with id unknown`, { - stack: expect.any(String), - }); + expect(log.debug).toHaveBeenCalledWith( + `Attempted to define object property "a" from extension with id unknown and version unknown`, + { + stack: expect.any(String), + } + ); }); }); @@ -620,7 +638,11 @@ describe('Plugin Extensions / Utils', () => { it('should open modal with provided title and body', async () => { const pluginId = 'grafana-worldmap-panel'; - const openModal = createOpenModalFunction(pluginId); + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); openModal({ title: 'Title in modal', @@ -634,7 +656,11 @@ describe('Plugin Extensions / Utils', () => { it('should open modal with default width if not specified', async () => { const pluginId = 'grafana-worldmap-panel'; - const openModal = createOpenModalFunction(pluginId); + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); openModal({ title: 'Title in modal', @@ -650,7 +676,11 @@ describe('Plugin Extensions / Utils', () => { it('should open modal with specified width', async () => { const pluginId = 'grafana-worldmap-panel'; - const openModal = createOpenModalFunction(pluginId); + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); openModal({ title: 'Title in modal', @@ -666,7 +696,11 @@ describe('Plugin Extensions / Utils', () => { it('should open modal with specified height', async () => { const pluginId = 'grafana-worldmap-panel'; - const openModal = createOpenModalFunction(pluginId); + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); openModal({ title: 'Title in modal', @@ -682,7 +716,11 @@ describe('Plugin Extensions / Utils', () => { it('should open modal with the plugin context being available', async () => { const pluginId = 'grafana-worldmap-panel'; - const openModal = createOpenModalFunction(pluginId); + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); const ModalContent = () => { const context = usePluginContext(); @@ -698,6 +736,93 @@ describe('Plugin Extensions / Utils', () => { const modal = await screen.findByRole('dialog'); expect(modal).toHaveTextContent('Version: 1.0.0'); }); + + it('should add a wrapper div with a "data-plugin-sandbox" attribute', async () => { + const pluginId = 'grafana-worldmap-panel'; + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: 'Title in modal', + }); + + openModal({ + title: 'Title in modal', + body: () => <div>Text in body</div>, + }); + + expect(await screen.findByRole('dialog')).toBeVisible(); + + expect(screen.getByTestId('plugin-sandbox-wrapper')).toHaveAttribute( + 'data-plugin-sandbox', + 'grafana-worldmap-panel' + ); + }); + + it('should show an error alert in the modal IN DEV MODE if the extension throws an error', async () => { + config.buildInfo.env = 'development'; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const pluginId = 'grafana-worldmap-panel'; + const extensionTitle = 'Title in modal'; + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: extensionTitle, + }); + + const ModalContent = () => { + throw new Error('Test error'); + }; + + openModal({ + title: extensionTitle, + body: ModalContent, + }); + + await screen.findByRole('dialog'); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, { + message: 'Test error', + componentStack: expect.any(String), + digest: expect.any(String), + }); + + expect(screen.getByText(`Extension failed to load: "${pluginId}/${extensionTitle}"`)).toBeVisible(); + }); + + it('should also show an error alert in the modal IN PRODUCTION MODE if the extension throws an error', async () => { + config.buildInfo.env = 'production'; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const pluginId = 'grafana-worldmap-panel'; + const extensionTitle = 'Title in modal'; + const openModal = createOpenModalFunction({ + pluginId, + extensionPointId: 'myorg-extensions-app/link/v1', + title: extensionTitle, + }); + + const ModalContent = () => { + throw new Error('Test error'); + }; + + openModal({ + title: extensionTitle, + body: ModalContent, + }); + + await screen.findByRole('dialog'); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, { + message: 'Test error', + componentStack: expect.any(String), + digest: expect.any(String), + }); + + expect(screen.getByText(`Extension failed to load: "${pluginId}/${extensionTitle}"`)).toBeVisible(); + }); }); describe('wrapWithPluginContext()', () => { @@ -726,9 +851,18 @@ describe('Plugin Extensions / Utils', () => { ); }; + beforeEach(() => { + resetLogMock(log); + }); + it('should make the plugin context available for the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; - const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); + const Component = wrapWithPluginContext({ + pluginId, + extensionTitle: 'ExampleComponent', + Component: ExampleComponent, + log, + }); render(<Component a={{ b: { c: 'Grafana' } }} />); @@ -738,7 +872,12 @@ describe('Plugin Extensions / Utils', () => { it('should pass the properties into the wrapped component', async () => { const pluginId = 'grafana-worldmap-panel'; - const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); + const Component = wrapWithPluginContext({ + pluginId, + extensionTitle: 'ExampleComponent', + Component: ExampleComponent, + log, + }); render(<Component a={{ b: { c: 'Grafana' } }} />); @@ -749,7 +888,12 @@ describe('Plugin Extensions / Utils', () => { it('should not be possible to mutate the props in development mode, but it logs an error', async () => { config.buildInfo.env = 'development'; const pluginId = 'grafana-worldmap-panel'; - const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); + const Component = wrapWithPluginContext({ + pluginId, + extensionTitle: 'ExampleComponent', + Component: ExampleComponent, + log, + }); const props = { a: { b: { c: 'Grafana' } } }; render(<Component {...props} override />); @@ -759,7 +903,7 @@ describe('Plugin Extensions / Utils', () => { // Logs a warning expect(log.error).toHaveBeenCalledTimes(1); expect(log.error).toHaveBeenCalledWith( - `Attempted to mutate object property "c" from extension with id grafana-worldmap-panel`, + `Attempted to mutate object property "c" from extension with id grafana-worldmap-panel and version unknown`, { stack: expect.any(String), } @@ -772,7 +916,12 @@ describe('Plugin Extensions / Utils', () => { it('should not be possible to mutate the props in production mode either, but it logs a warning', async () => { config.buildInfo.env = 'production'; const pluginId = 'grafana-worldmap-panel'; - const Component = wrapWithPluginContext(pluginId, ExampleComponent, log); + const Component = wrapWithPluginContext({ + pluginId, + extensionTitle: 'ExampleComponent', + Component: ExampleComponent, + log, + }); const props = { a: { b: { c: 'Grafana' } } }; render(<Component {...props} override />); @@ -782,7 +931,7 @@ describe('Plugin Extensions / Utils', () => { // Logs a warning expect(log.warning).toHaveBeenCalledTimes(1); expect(log.warning).toHaveBeenCalledWith( - `Attempted to mutate object property "c" from extension with id grafana-worldmap-panel`, + `Attempted to mutate object property "c" from extension with id grafana-worldmap-panel and version unknown`, { stack: expect.any(String), } @@ -791,6 +940,64 @@ describe('Plugin Extensions / Utils', () => { // Not able to mutate the props in production mode either expect(props.a.b.c).toBe('Grafana'); }); + + it('should render an error alert IN DEV MODE if the extension throws an error', async () => { + config.buildInfo.env = 'development'; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const pluginId = 'grafana-worldmap-panel'; + const ComponentWithError = () => { + throw new Error('Test error'); + }; + const extensionTitle = 'ComponentWithError'; + const WrappedComponent = wrapWithPluginContext({ + pluginId, + extensionTitle, + Component: ComponentWithError, + log, + }); + + render(<WrappedComponent />); + + expect(await screen.findByText(`Extension failed to load: "${pluginId}/${extensionTitle}"`)).toBeVisible(); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, { + message: 'Test error', + componentStack: expect.any(String), + digest: expect.any(String), + }); + }); + + it('should not render anything IN PRODUCTION MODE if the extension throws an error, but still logs an error', async () => { + config.buildInfo.env = 'production'; + jest.spyOn(console, 'error').mockImplementation(() => {}); + + const pluginId = 'grafana-worldmap-panel'; + const ComponentWithError = () => { + throw new Error('Test error'); + }; + const extensionTitle = 'ComponentWithError'; + const WrappedComponent = wrapWithPluginContext({ + pluginId, + extensionTitle, + Component: ComponentWithError, + log, + }); + + render(<WrappedComponent />); + + await waitFor(() => + expect(screen.queryByText(`Extension failed to load: "${pluginId}/${extensionTitle}"`)).not.toBeInTheDocument() + ); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, { + message: 'Test error', + componentStack: expect.any(String), + digest: expect.any(String), + }); + }); }); describe('getAppPluginConfigs()', () => { diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index ec994d81383..f083059cece 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -20,8 +20,9 @@ import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime'; import { Modal } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events'; +import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events'; +import { ExtensionErrorBoundary } from './ExtensionErrorBoundary'; import { ExtensionsLog, log as baseLog } from './logs/log'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; import { assertIsNotPromise, assertLinkPathIsValid, assertStringProps, isPromise } from './validators'; @@ -38,17 +39,18 @@ export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') { }; } -export function createOpenModalFunction(pluginId: string): PluginExtensionEventHelpers['openModal'] { +export function createOpenModalFunction(config: AddedLinkRegistryItem): PluginExtensionEventHelpers['openModal'] { return async (options) => { const { title, body, width, height } = options; appEvents.publish( new ShowModalReactEvent({ - component: wrapWithPluginContext<ModalWrapperProps>( - pluginId, - getModalWrapper({ title, body, width, height }), - baseLog - ), + component: wrapWithPluginContext<ModalWrapperProps>({ + pluginId: config.pluginId, + extensionTitle: config.title, + Component: getModalWrapper({ title, body, width, height, config }), + log: baseLog, + }), }) ); }; @@ -58,7 +60,17 @@ type ModalWrapperProps = { onDismiss: () => void; }; -export const wrapWithPluginContext = <T,>(pluginId: string, Component: React.ComponentType<T>, log: ExtensionsLog) => { +export const wrapWithPluginContext = <T,>({ + pluginId, + extensionTitle, + Component, + log, +}: { + pluginId: string; + extensionTitle: string; + Component: React.ComponentType<T>; + log: ExtensionsLog; +}) => { const WrappedExtensionComponent = (props: T & React.JSX.IntrinsicAttributes) => { const { error, @@ -85,7 +97,9 @@ export const wrapWithPluginContext = <T,>(pluginId: string, Component: React.Com return ( <PluginContextProvider meta={pluginMeta}> - <Component {...writableProxy(props, { log, source: 'extension', pluginId })} /> + <ExtensionErrorBoundary pluginId={pluginId} extensionTitle={extensionTitle} log={log}> + <Component {...writableProxy(props, { log, source: 'extension', pluginId })} /> + </ExtensionErrorBoundary> </PluginContextProvider> ); }; @@ -102,13 +116,27 @@ const getModalWrapper = ({ body: Body, width, height, -}: PluginExtensionOpenModalOptions) => { + config, +}: PluginExtensionOpenModalOptions & { config: AddedLinkRegistryItem }) => { const className = css({ width, height }); const ModalWrapper = ({ onDismiss }: ModalWrapperProps) => { return ( <Modal title={title} className={className} isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}> - <Body onDismiss={onDismiss} /> + {/* + We also add an error boundary here (apart from the one in the `wrapWithPluginContext`) + so the error appears inside the modal (and not at the bottom of the page.) + */} + <ExtensionErrorBoundary + pluginId={config.pluginId} + extensionTitle={config.title} + fallbackAlwaysVisible={true} + log={baseLog} + > + <div data-plugin-sandbox={config.pluginId} data-testid="plugin-sandbox-wrapper"> + <Body onDismiss={onDismiss} /> + </div> + </ExtensionErrorBoundary> </Modal> ); }; @@ -223,6 +251,7 @@ interface ProxyOptions { log?: ExtensionsLog; source?: MutationSource; pluginId?: string; + pluginVersion?: string; } /** @@ -233,6 +262,7 @@ interface ProxyOptions { * @param options.log The logger to use * @param options.source The source of the mutation * @param options.pluginId The id of the plugin that is mutating the object + * @param options.pluginVersion The version of the plugin that is mutating the object * @returns A new proxy object that logs any attempted mutation to the original object */ export function getMutationObserverProxy<T extends object>(obj: T, options?: ProxyOptions): T { @@ -240,31 +270,40 @@ export function getMutationObserverProxy<T extends object>(obj: T, options?: Pro return obj; } - const { log = baseLog, source = 'extension', pluginId = 'unknown' } = options ?? {}; + const { log = baseLog, source = 'extension', pluginId = 'unknown', pluginVersion = 'unknown' } = options ?? {}; const cache = new WeakMap(); const logFunction = isGrafanaDevMode() ? log.error.bind(log) : log.warning.bind(log); // should show error during local development return new Proxy(obj, { deleteProperty(target, prop) { - logFunction(`Attempted to delete object property "${String(prop)}" from ${source} with id ${pluginId}`, { - stack: new Error().stack ?? '', - }); + logFunction( + `Attempted to delete object property "${String(prop)}" from ${source} with id ${pluginId} and version ${pluginVersion}`, + { + stack: new Error().stack ?? '', + } + ); Reflect.deleteProperty(target, prop); return true; }, defineProperty(target, prop, descriptor) { // because immer (used by RTK) calls Object.isFrozen and Object.freeze we know that defineProperty will be called // behind the scenes as well so we only log message with debug level to minimize the noise and false positives - log.debug(`Attempted to define object property "${String(prop)}" from ${source} with id ${pluginId}`, { - stack: new Error().stack ?? '', - }); + log.debug( + `Attempted to define object property "${String(prop)}" from ${source} with id ${pluginId} and version ${pluginVersion}`, + { + stack: new Error().stack ?? '', + } + ); Reflect.defineProperty(target, prop, descriptor); return true; }, set(target, prop, newValue) { - logFunction(`Attempted to mutate object property "${String(prop)}" from ${source} with id ${pluginId}`, { - stack: new Error().stack ?? '', - }); + logFunction( + `Attempted to mutate object property "${String(prop)}" from ${source} with id ${pluginId} and version ${pluginVersion}`, + { + stack: new Error().stack ?? '', + } + ); Reflect.set(target, prop, newValue); return true; }, @@ -290,7 +329,7 @@ export function getMutationObserverProxy<T extends object>(obj: T, options?: Pro if (isObject(value) || isArray(value)) { if (!cache.has(value)) { - cache.set(value, getMutationObserverProxy(value, { log, source, pluginId })); + cache.set(value, getMutationObserverProxy(value, { log, source, pluginId, pluginVersion })); } return cache.get(value); } @@ -308,6 +347,7 @@ export function getMutationObserverProxy<T extends object>(obj: T, options?: Pro * @param options.log The logger to use * @param options.source The source of the mutation * @param options.pluginId The id of the plugin that is mutating the object + * @param options.pluginVersion The version of the plugin that is mutating the object * @returns A new proxy object that logs any attempted mutation to the original object */ export function writableProxy<T>(value: T, options?: ProxyOptions): T { @@ -316,10 +356,10 @@ export function writableProxy<T>(value: T, options?: ProxyOptions): T { return value; } - const { log = baseLog, source = 'extension', pluginId = 'unknown' } = options ?? {}; + const { log = baseLog, source = 'extension', pluginId = 'unknown', pluginVersion = 'unknown' } = options ?? {}; // Default: we return a proxy of a deep-cloned version of the original object, which logs warnings when mutation is attempted - return getMutationObserverProxy(cloneDeep(value), { log, pluginId, source }); + return getMutationObserverProxy(cloneDeep(value), { log, pluginId, pluginVersion, source }); } function isRecord(value: unknown): value is Record<string | number | symbol, unknown> { @@ -491,7 +531,7 @@ export function getLinkExtensionOnClick( const helpers: PluginExtensionEventHelpers = { context, - openModal: createOpenModalFunction(pluginId), + openModal: createOpenModalFunction(config), openSidebar: (componentTitle, context) => { appEvents.publish( new OpenExtensionSidebarEvent({ @@ -501,6 +541,9 @@ export function getLinkExtensionOnClick( }) ); }, + closeSidebar: () => { + appEvents.publish(new CloseExtensionSidebarEvent()); + }, }; log.debug(`onClick '${config.title}' at '${extensionPointId}'`); diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx index 8e3350494d5..1b7e71eba7b 100644 --- a/public/app/features/plugins/extensions/validators.test.tsx +++ b/public/app/features/plugins/extensions/validators.test.tsx @@ -198,9 +198,8 @@ describe('Plugin Extension Validators', () => { describe('isExtensionPointIdValid()', () => { test.each([ - // We (for now allow core Grafana extension points to run without a version) - ['grafana/extension-point', ''], - ['grafana/extension-point', 'grafana'], + [PluginExtensionPoints.DashboardPanelMenu, ''], + [PluginExtensionPoints.DashboardPanelMenu, 'grafana'], ['myorg-extensions-app/extension-point', 'myorg-extensions-app'], ['myorg-extensions-app/extension-point/v1', 'myorg-extensions-app'], ['plugins/myorg-extensions-app/extension-point/v1', 'myorg-extensions-app'], @@ -217,6 +216,8 @@ describe('Plugin Extension Validators', () => { isExtensionPointIdValid({ extensionPointId, pluginId, + isInsidePlugin: pluginId !== 'grafana' && pluginId !== '', + log: createLogMock(), }) ).toBe(true); }); @@ -232,11 +233,18 @@ describe('Plugin Extension Validators', () => { 'extension-point/v1', 'myorgs-extensions-app', ], + [ + // Not exposed to plugins + 'grafana/not-exposed-extension-point/v1', + 'grafana', + ], ])('should return FALSE if the extension point id is invalid ("%s", "%s")', (extensionPointId, pluginId) => { expect( isExtensionPointIdValid({ extensionPointId, pluginId, + isInsidePlugin: pluginId !== 'grafana' && pluginId !== '', + log: createLogMock(), }) ).toBe(false); }); @@ -357,6 +365,21 @@ describe('Plugin Extension Validators', () => { expect(log.warning).toHaveBeenCalledTimes(1); expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); }); + + it('should return FALSE with links with the same title but different targets', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedLinks.push(extensionConfig); + const extensionConfig2 = { + ...extensionConfig, + targets: [PluginExtensionPoints.ExploreToolbarAction], + }; + config.apps[pluginId].extensions.addedLinks.push(extensionConfig2); + + const returnValue = isAddedLinkMetaInfoMissing(pluginId, extensionConfig2, log); + + expect(returnValue).toBe(false); + expect(log.error).toHaveBeenCalledTimes(0); + }); }); describe('isAddedComponentMetaInfoMissing()', () => { @@ -474,6 +497,21 @@ describe('Plugin Extension Validators', () => { expect(log.warning).toHaveBeenCalledTimes(1); expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); }); + + it('should return FALSE with components with the same title but different targets', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.addedComponents.push(extensionConfig); + const extensionConfig2 = { + ...extensionConfig, + targets: [PluginExtensionPoints.ExploreToolbarAction], + }; + config.apps[pluginId].extensions.addedComponents.push(extensionConfig2); + + const returnValue = isAddedComponentMetaInfoMissing(pluginId, extensionConfig2, log); + + expect(returnValue).toBe(false); + expect(log.error).toHaveBeenCalledTimes(0); + }); }); describe('isExposedComponentMetaInfoMissing()', () => { @@ -592,6 +630,21 @@ describe('Plugin Extension Validators', () => { expect(log.warning).toHaveBeenCalledTimes(1); expect(jest.mocked(log.warning).mock.calls[0][0]).toMatch('"description" doesn\'t match'); }); + + it('should return FALSE with components with the same title but different targets', () => { + const log = createLogMock(); + config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig); + const exposedComponentConfig2 = { + ...exposedComponentConfig, + targets: [PluginExtensionPoints.ExploreToolbarAction], + }; + config.apps[pluginId].extensions.exposedComponents.push(exposedComponentConfig2); + + const returnValue = isExposedComponentMetaInfoMissing(pluginId, exposedComponentConfig2, log); + + expect(returnValue).toBe(false); + expect(log.error).toHaveBeenCalledTimes(0); + }); }); describe('isExposedComponentDependencyMissing()', () => { diff --git a/public/app/features/plugins/extensions/validators.ts b/public/app/features/plugins/extensions/validators.ts index 4106544567f..081ac98fbab 100644 --- a/public/app/features/plugins/extensions/validators.ts +++ b/public/app/features/plugins/extensions/validators.ts @@ -68,15 +68,33 @@ export function isLinkPathValid(pluginId: string, path: string) { export function isExtensionPointIdValid({ extensionPointId, pluginId, + isInsidePlugin, + log, }: { extensionPointId: string; pluginId: string; + isInsidePlugin: boolean; + log: ExtensionsLog; }) { - if (extensionPointId.startsWith('grafana/')) { - return true; + const startsWithPluginId = + extensionPointId.startsWith(`${pluginId}/`) || extensionPointId.startsWith(`plugins/${pluginId}/`); + + if (isInsidePlugin && !startsWithPluginId) { + log.error(errors.INVALID_EXTENSION_POINT_ID_PLUGIN(pluginId, extensionPointId)); + return false; } - return Boolean(extensionPointId.startsWith(`plugins/${pluginId}/`) || extensionPointId.startsWith(`${pluginId}/`)); + if (!isInsidePlugin && !extensionPointId.startsWith('grafana/')) { + log.error(errors.INVALID_EXTENSION_POINT_ID_GRAFANA_PREFIX(extensionPointId)); + return false; + } + + if (!isInsidePlugin && !Object.values<string>(PluginExtensionPoints).includes(extensionPointId)) { + log.error(errors.INVALID_EXTENSION_POINT_ID_GRAFANA_EXPOSED); + return false; + } + + return true; } export function extensionPointEndsWithVersion(extensionPointId: string) { @@ -137,25 +155,25 @@ export const isAddedLinkMetaInfoMissing = ( ) => { const logPrefix = 'Could not register link extension. Reason:'; const app = config.apps[pluginId]; - const pluginJsonMetaInfo = app ? app.extensions.addedLinks.find(({ title }) => title === metaInfo.title) : null; + const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null; if (!app) { log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); return true; } - if (!pluginJsonMetaInfo) { + if (!pluginJsonMetaInfo || pluginJsonMetaInfo.length === 0) { log.error(`${logPrefix} ${errors.ADDED_LINK_META_INFO_MISSING}`); return true; } const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; - if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { + if (!targets.every((target) => pluginJsonMetaInfo.some(({ targets }) => targets.includes(target)))) { log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`); return true; } - if (pluginJsonMetaInfo.description !== metaInfo.description) { + if (pluginJsonMetaInfo.some(({ description }) => description !== metaInfo.description)) { log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); } @@ -169,25 +187,25 @@ export const isAddedFunctionMetaInfoMissing = ( ) => { const logPrefix = 'Could not register function extension. Reason:'; const app = config.apps[pluginId]; - const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.find(({ title }) => title === metaInfo.title) : null; + const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null; if (!app) { log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); return true; } - if (!pluginJsonMetaInfo) { + if (!pluginJsonMetaInfo || pluginJsonMetaInfo.length === 0) { log.error(`${logPrefix} ${errors.ADDED_FUNCTION_META_INFO_MISSING}`); return true; } const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; - if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { + if (!targets.every((target) => pluginJsonMetaInfo.some(({ targets }) => targets.includes(target)))) { log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`); return true; } - if (pluginJsonMetaInfo.description !== metaInfo.description) { + if (pluginJsonMetaInfo.some(({ description }) => description !== metaInfo.description)) { log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); } @@ -201,25 +219,27 @@ export const isAddedComponentMetaInfoMissing = ( ) => { const logPrefix = 'Could not register component extension. Reason:'; const app = config.apps[pluginId]; - const pluginJsonMetaInfo = app ? app.extensions.addedComponents.find(({ title }) => title === metaInfo.title) : null; + const pluginJsonMetaInfo = app + ? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title) + : null; if (!app) { log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); return true; } - if (!pluginJsonMetaInfo) { + if (!pluginJsonMetaInfo || pluginJsonMetaInfo.length === 0) { log.error(`${logPrefix} ${errors.ADDED_COMPONENT_META_INFO_MISSING}`); return true; } const targets = Array.isArray(metaInfo.targets) ? metaInfo.targets : [metaInfo.targets]; - if (!targets.every((target) => pluginJsonMetaInfo.targets.includes(target))) { + if (!targets.every((target) => pluginJsonMetaInfo.some(({ targets }) => targets.includes(target)))) { log.error(`${logPrefix} ${errors.TARGET_NOT_MATCHING_META_INFO}`); return true; } - if (pluginJsonMetaInfo.description !== metaInfo.description) { + if (pluginJsonMetaInfo.some(({ description }) => description !== metaInfo.description)) { log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); } @@ -233,24 +253,24 @@ export const isExposedComponentMetaInfoMissing = ( ) => { const logPrefix = 'Could not register exposed component extension. Reason:'; const app = config.apps[pluginId]; - const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.find(({ id }) => id === metaInfo.id) : null; + const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null; if (!app) { log.error(`${logPrefix} ${errors.APP_NOT_FOUND(pluginId)}`); return true; } - if (!pluginJsonMetaInfo) { + if (!pluginJsonMetaInfo || pluginJsonMetaInfo.length === 0) { log.error(`${logPrefix} ${errors.EXPOSED_COMPONENT_META_INFO_MISSING}`); return true; } - if (pluginJsonMetaInfo.title !== metaInfo.title) { + if (pluginJsonMetaInfo.some(({ title }) => title !== metaInfo.title)) { log.error(`${logPrefix} ${errors.TITLE_NOT_MATCHING_META_INFO}`); return true; } - if (pluginJsonMetaInfo.description !== metaInfo.description) { + if (pluginJsonMetaInfo.some(({ description }) => description !== metaInfo.description)) { log.warning(errors.DESCRIPTION_NOT_MATCHING_META_INFO); } diff --git a/public/app/features/plugins/importPanelPlugin.ts b/public/app/features/plugins/importPanelPlugin.ts index a8f770acdd5..2cf402cb2b6 100644 --- a/public/app/features/plugins/importPanelPlugin.ts +++ b/public/app/features/plugins/importPanelPlugin.ts @@ -3,7 +3,8 @@ import config from 'app/core/config'; import { getPanelPluginLoadError } from '../panel/components/PanelPluginError'; -import { importPluginModule } from './plugin_loader'; +import { importPluginModule } from './importer/importPluginModule'; +import { pluginImporter } from './importer/pluginImporter'; const promiseCache: Record<string, Promise<PanelPlugin>> = {}; const panelPluginCache: Record<string, PanelPlugin> = {}; @@ -50,10 +51,18 @@ export function importPanelPluginFromMeta(meta: PanelPluginMeta): Promise<PanelP } export function syncGetPanelPlugin(id: string): PanelPlugin | undefined { + if (config.featureToggles.enablePluginImporter) { + return pluginImporter.getPanel(id); + } + return panelPluginCache[id]; } function getPanelPlugin(meta: PanelPluginMeta): Promise<PanelPlugin> { + if (config.featureToggles.enablePluginImporter) { + return pluginImporter.importPanel(meta); + } + throwIfAngular(meta); const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch; diff --git a/public/app/features/plugins/importer/addTranslationsToI18n.test.ts b/public/app/features/plugins/importer/addTranslationsToI18n.test.ts new file mode 100644 index 00000000000..becc58c0ca8 --- /dev/null +++ b/public/app/features/plugins/importer/addTranslationsToI18n.test.ts @@ -0,0 +1,142 @@ +import { i18n } from 'i18next'; + +import * as i18nModule from '@grafana/i18n/internal'; + +import { server } from '../loader/pluginLoader.mock'; +import { SystemJS } from '../loader/systemjs'; +import { SystemJSWithLoaderHooks } from '../loader/types'; + +import { addTranslationsToI18n } from './addTranslationsToI18n'; + +describe('addTranslationsToI18n', () => { + const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype; + const originalFetch = systemJSPrototype.fetch; + const originalResolve = systemJSPrototype.resolve; + let addResourceBundleSpy: jest.SpyInstance; + + beforeAll(() => { + server.listen(); + systemJSPrototype.resolve = (moduleId: string) => moduleId; + systemJSPrototype.shouldFetch = () => true; + // because server.listen() patches fetch, we need to reassign this to the systemJSPrototype + // this is identical to what happens in the original code: https://github.com/systemjs/systemjs/blob/main/src/features/fetch-load.js#L12 + systemJSPrototype.fetch = window.fetch; + }); + + beforeEach(() => { + addResourceBundleSpy = jest + .spyOn(i18nModule, 'addResourceBundle') + .mockImplementation(() => ({}) as unknown as i18n); + }); + + afterEach(() => { + server.resetHandlers(); + jest.clearAllMocks(); + }); + + afterAll(() => { + SystemJS.constructor.prototype.resolve = originalResolve; + SystemJS.constructor.prototype.fetch = originalFetch; + server.close(); + }); + + it('should add translations that match the resolved language first', async () => { + const translations = { + 'en-US': '/public/plugins/test-panel/locales/en-US/test-panel.json', + 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/test-panel.json', + }; + + await addTranslationsToI18n({ + resolvedLanguage: 'pt-BR', + fallbackLanguage: 'en-US', + pluginId: 'test-panel', + translations, + }); + + expect(addResourceBundleSpy).toHaveBeenCalledTimes(1); + expect(addResourceBundleSpy).toHaveBeenNthCalledWith(1, 'pt-BR', 'test-panel', { testKey: 'valorDeTeste' }); + }); + + it('should add translations that match the fallback language if the resolved language is not in the translations', async () => { + const translations = { + 'en-US': '/public/plugins/test-panel/locales/en-US/test-panel.json', + 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/test-panel.json', + }; + + await addTranslationsToI18n({ + resolvedLanguage: 'sv-SE', + fallbackLanguage: 'en-US', + pluginId: 'test-panel', + translations, + }); + + expect(addResourceBundleSpy).toHaveBeenCalledTimes(1); + expect(addResourceBundleSpy).toHaveBeenNthCalledWith(1, 'en-US', 'test-panel', { testKey: 'testValue' }); + }); + + it('should warn if no translations are found', async () => { + const translations = { + 'en-US': '/public/plugins/test-panel/locales/en-US/test-panel.json', + 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/test-panel.json', + }; + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + await addTranslationsToI18n({ + resolvedLanguage: 'sv-SE', + fallbackLanguage: 'sv-SE', + pluginId: 'test-panel', + translations, + }); + + expect(consoleSpy).toHaveBeenCalledWith('Could not find any translation for plugin test-panel', { + resolvedLanguage: 'sv-SE', + fallbackLanguage: 'sv-SE', + }); + }); + + it('should warn if no exported default is found', async () => { + const translations = { + 'en-US': '/public/plugins/test-panel/locales/en-US/no-default-export.json', + 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/no-default-export.json', + }; + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + await addTranslationsToI18n({ + resolvedLanguage: 'en-US', + fallbackLanguage: 'en-US', + pluginId: 'test-panel', + translations, + }); + + expect(consoleSpy).toHaveBeenCalledWith('Could not find default export for plugin test-panel', { + resolvedLanguage: 'en-US', + fallbackLanguage: 'en-US', + path: '/public/plugins/test-panel/locales/en-US/no-default-export.json', + }); + }); + + it('should warn if translations cannot be loaded', async () => { + const translations = { + 'en-US': '/public/plugins/test-panel/locales/en-US/unknown.json', + 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/unknown.json', + }; + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + await addTranslationsToI18n({ + resolvedLanguage: 'en-US', + fallbackLanguage: 'pt-BR', + pluginId: 'test-panel', + translations, + }); + + expect(consoleSpy).toHaveBeenCalledWith('Could not load translation for plugin test-panel', { + resolvedLanguage: 'en-US', + fallbackLanguage: 'pt-BR', + error: new TypeError('Failed to fetch'), + path: '/public/plugins/test-panel/locales/en-US/unknown.json', + }); + }); +}); diff --git a/public/app/features/plugins/importer/addTranslationsToI18n.ts b/public/app/features/plugins/importer/addTranslationsToI18n.ts new file mode 100644 index 00000000000..177e6ba78b8 --- /dev/null +++ b/public/app/features/plugins/importer/addTranslationsToI18n.ts @@ -0,0 +1,49 @@ +import { addResourceBundle } from '@grafana/i18n/internal'; + +import { SystemJS } from '../loader/systemjs'; +import { resolveModulePath } from '../loader/utils'; + +interface AddTranslationsToI18nOptions { + resolvedLanguage: string; + fallbackLanguage: string; + pluginId: string; + translations: Record<string, string>; +} + +export async function addTranslationsToI18n({ + resolvedLanguage, + fallbackLanguage, + pluginId, + translations, +}: AddTranslationsToI18nOptions): Promise<void> { + const resolvedPath = translations[resolvedLanguage]; + const fallbackPath = translations[fallbackLanguage]; + const path = resolvedPath ?? fallbackPath; + + if (!path) { + console.warn(`Could not find any translation for plugin ${pluginId}`, { resolvedLanguage, fallbackLanguage }); + return; + } + + try { + const module = await SystemJS.import(resolveModulePath(path)); + if (!module.default) { + console.warn(`Could not find default export for plugin ${pluginId}`, { + resolvedLanguage, + fallbackLanguage, + path, + }); + return; + } + + const language = resolvedPath ? resolvedLanguage : fallbackLanguage; + addResourceBundle(language, pluginId, module.default); + } catch (error) { + console.warn(`Could not load translation for plugin ${pluginId}`, { + resolvedLanguage, + fallbackLanguage, + error, + path, + }); + } +} diff --git a/public/app/features/plugins/importer/importPluginModule.ts b/public/app/features/plugins/importer/importPluginModule.ts new file mode 100644 index 00000000000..9b220c97d6f --- /dev/null +++ b/public/app/features/plugins/importer/importPluginModule.ts @@ -0,0 +1,83 @@ +import { DEFAULT_LANGUAGE } from '@grafana/i18n'; +import { getResolvedLanguage } from '@grafana/i18n/internal'; +import { config } from '@grafana/runtime'; + +import builtInPlugins from '../built_in_plugins'; +import { registerPluginInCache } from '../loader/cache'; +import { SystemJS } from '../loader/systemjs'; +import { resolveModulePath } from '../loader/utils'; +import { importPluginModuleInSandbox } from '../sandbox/sandboxPluginLoader'; +import { shouldLoadPluginInFrontendSandbox } from '../sandbox/sandboxPluginLoaderRegistry'; +import { pluginsLogger } from '../utils'; + +import { addTranslationsToI18n } from './addTranslationsToI18n'; +import { PluginImportInfo } from './types'; + +export async function importPluginModule({ + path, + pluginId, + loadingStrategy, + version, + moduleHash, + translations, +}: PluginImportInfo): Promise<System.Module> { + if (version) { + registerPluginInCache({ path, version, loadingStrategy }); + } + + // Add locales to i18n for a plugin if the feature toggle is enabled and the plugin has locales + if (config.featureToggles.localizationForPlugins && translations) { + await addTranslationsToI18n({ + resolvedLanguage: getResolvedLanguage(), + fallbackLanguage: DEFAULT_LANGUAGE, + pluginId, + translations, + }); + } + + const builtIn = builtInPlugins[path]; + if (builtIn) { + // for handling dynamic imports + if (typeof builtIn === 'function') { + return await builtIn(); + } else { + return builtIn; + } + } + + const modulePath = resolveModulePath(path); + + // inject integrity hash into SystemJS import map + if (config.featureToggles.pluginsSriChecks) { + const resolvedModule = System.resolve(modulePath); + const integrityMap = System.getImportMap().integrity; + + if (moduleHash && integrityMap && !integrityMap[resolvedModule]) { + SystemJS.addImportMap({ + integrity: { + [resolvedModule]: moduleHash, + }, + }); + } + } + + // the sandboxing environment code cannot work in nodejs and requires a real browser + if (await shouldLoadPluginInFrontendSandbox({ pluginId })) { + return importPluginModuleInSandbox({ pluginId }); + } + + return SystemJS.import(modulePath).catch((e) => { + let error = new Error('Could not load plugin: ' + e); + console.error(error); + pluginsLogger.logError(error, { + path, + pluginId, + pluginVersion: version ?? '', + expectedHash: moduleHash ?? '', + loadingStrategy: loadingStrategy.toString(), + sriChecksEnabled: String(Boolean(config.featureToggles.pluginsSriChecks)), + newPluginLoadingEnabled: String(Boolean(config.featureToggles.enablePluginImporter)), + }); + throw error; + }); +} diff --git a/public/app/features/plugins/importer/pluginImporter.test.ts b/public/app/features/plugins/importer/pluginImporter.test.ts new file mode 100644 index 00000000000..a8dd2254b59 --- /dev/null +++ b/public/app/features/plugins/importer/pluginImporter.test.ts @@ -0,0 +1,415 @@ +import { + AppPlugin, + AppPluginMeta, + DataSourcePlugin, + DataSourcePluginMeta, + PanelPlugin, + PanelPluginMeta, + PluginLoadingStrategy, + PluginMeta, + PluginType, +} from '@grafana/data'; + +import { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, +} from '../extensions/registry/setup'; +import { pluginsLogger } from '../utils'; + +import * as importPluginModule from './importPluginModule'; +import { pluginImporter, clearCaches } from './pluginImporter'; + +describe('pluginImporter', () => { + beforeEach(() => { + jest.clearAllMocks(); + clearCaches(); + }); + + describe('importPanel', () => { + it('should import a panel plugin successfully with fallbackLoadingStrategy', async () => { + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ plugin: { ...panelPlugin } }); + + const result = await pluginImporter.importPanel({ ...panelPlugin }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + + expect(result).toEqual({ ...panelPlugin, meta: { ...panelPlugin } }); + }); + + it('should set correct loading strategy', async () => { + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ plugin: { ...panelPlugin } }); + const meta = { ...panelPlugin, loadingStrategy: PluginLoadingStrategy.script }; + + const result = await pluginImporter.importPanel({ ...meta }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'script', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + + expect(result).toEqual({ ...panelPlugin, meta: { ...panelPlugin, loadingStrategy: 'script' } }); + }); + + it('should log a warning and return a error component if module is missing exported plugin', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({}); + + const result = await pluginImporter.importPanel({ ...panelPlugin }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Error loading panel plugin: test-plugin', + new Error('missing export: plugin') + ); + + expect(result).toBeInstanceOf(PanelPlugin); + expect(result.loadError).toBe(true); + }); + }); + + describe('importDataSource', () => { + it('should import a data source plugin successfully with fallbackLoadingStrategy', async () => { + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ plugin: { ...dataSourcePlugin } }); + + const result = await pluginImporter.importDataSource({ ...dataSourcePlugin }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + + expect(result).toEqual({ ...dataSourcePlugin, meta: { ...dataSourcePlugin } }); + }); + + it('should import a data source plugin with Datasource prop', async () => { + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ Datasource: { ...dataSourcePlugin } }); + + const result = await pluginImporter.importDataSource({ ...dataSourcePlugin }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const DataSourceClass: any = { ...dataSourcePlugin }; + const expected = new DataSourcePlugin(DataSourceClass); + expect(result).toEqual({ + ...expected, + components: { + AnnotationsQueryCtrl: undefined, + ExploreQueryField: undefined, + QueryCtrl: undefined, + QueryEditor: undefined, + QueryEditorHelp: undefined, + VariableQueryEditor: undefined, + }, + meta: { ...dataSourcePlugin }, + }); + }); + + it('should set correct loading strategy', async () => { + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ plugin: { ...dataSourcePlugin } }); + const meta = { ...dataSourcePlugin, loadingStrategy: PluginLoadingStrategy.script }; + + const result = await pluginImporter.importDataSource({ ...meta }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'script', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + + expect(result).toEqual({ ...dataSourcePlugin, meta: { ...dataSourcePlugin, loadingStrategy: 'script' } }); + }); + + it('should throw error if module is missing exported plugin', async () => { + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({}); + + expect(async () => { + await pluginImporter.importDataSource({ ...dataSourcePlugin }); + }).rejects.toThrow(new Error('Plugin module is missing DataSourcePlugin or Datasource constructor export')); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + }); + }); + + describe('importApp', () => { + it('should import a app plugin successfully with fallbackLoadingStrategy', async () => { + const init = jest.fn(); + const setComponentsFromLegacyExports = jest.fn(); + const plugin = { + plugin: { + ...appPlugin, + init, + setComponentsFromLegacyExports, + exposedComponentConfigs: [{}], + addedComponentConfigs: [{}], + addedLinkConfigs: [{}], + addedFunctionConfigs: [{}], + }, + }; + const exposedComponentsRegistrySpy = jest + .spyOn(exposedComponentsRegistry, 'register') + .mockImplementation(() => {}); + const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {}); + const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {}); + const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {}); + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({ ...plugin }); + + const result = await pluginImporter.importApp({ ...appPlugin }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + expect(init).toHaveBeenCalledWith({ ...appPlugin }); + expect(setComponentsFromLegacyExports).toHaveBeenCalledWith({ ...plugin }); + expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + + expect(result).toEqual({ + ...appPlugin, + meta: { ...appPlugin }, + init, + setComponentsFromLegacyExports, + exposedComponentConfigs: [{}], + addedComponentConfigs: [{}], + addedLinkConfigs: [{}], + addedFunctionConfigs: [{}], + }); + }); + + it('should set correct loading strategy', async () => { + const init = jest.fn(); + const setComponentsFromLegacyExports = jest.fn(); + const plugin = { + plugin: { + ...appPlugin, + init, + setComponentsFromLegacyExports, + exposedComponentConfigs: [{}], + addedComponentConfigs: [{}], + addedLinkConfigs: [{}], + addedFunctionConfigs: [{}], + }, + }; + const exposedComponentsRegistrySpy = jest + .spyOn(exposedComponentsRegistry, 'register') + .mockImplementation(() => {}); + const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {}); + const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {}); + const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {}); + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({ ...plugin }); + const meta = { ...appPlugin, loadingStrategy: PluginLoadingStrategy.script }; + + const result = await pluginImporter.importApp({ ...meta }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'script', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + expect(init).toHaveBeenCalledWith({ ...meta }); + expect(setComponentsFromLegacyExports).toHaveBeenCalledWith({ ...plugin }); + expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [{}] }); + + expect(result).toEqual({ + ...appPlugin, + meta: { ...appPlugin, loadingStrategy: 'script' }, + init, + setComponentsFromLegacyExports, + exposedComponentConfigs: [{}], + addedComponentConfigs: [{}], + addedLinkConfigs: [{}], + addedFunctionConfigs: [{}], + }); + }); + + it('should import an empty app plugin if missing exported plugin', async () => { + const exposedComponentsRegistrySpy = jest + .spyOn(exposedComponentsRegistry, 'register') + .mockImplementation(() => {}); + const addedComponentsRegistrySpy = jest.spyOn(addedComponentsRegistry, 'register').mockImplementation(() => {}); + const addedLinksRegistrySpy = jest.spyOn(addedLinksRegistry, 'register').mockImplementation(() => {}); + const addedFunctionsRegistrySpy = jest.spyOn(addedFunctionsRegistry, 'register').mockImplementation(() => {}); + const spy = jest.spyOn(importPluginModule, 'importPluginModule').mockResolvedValue({}); + + const result = await pluginImporter.importApp({ ...appPlugin }); + + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + expect(exposedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); + expect(addedComponentsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); + expect(addedLinksRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); + expect(addedFunctionsRegistrySpy).toHaveBeenCalledWith({ pluginId: 'test-plugin', configs: [] }); + + expect(result).toEqual({ ...new AppPlugin(), meta: { ...appPlugin } }); + }); + }); + + describe('caches', () => { + it('should return a cached plugin if it exsits', async () => { + const logSpy = jest.spyOn(pluginsLogger, 'logDebug').mockImplementation(() => {}); + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ plugin: { ...panelPlugin } }); + + const original = await pluginImporter.importPanel({ ...panelPlugin }); + const cached = await pluginImporter.importPanel({ ...panelPlugin }); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + expect(logSpy).toHaveBeenCalledWith(`Retrieving plugin from cache`, { + expectedHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + loadingStrategy: 'fetch', + newPluginLoadingEnabled: 'false', + path: 'public/plugins/test-plugin/module.js', + pluginId: 'test-plugin', + pluginVersion: '1.0.0', + sriChecksEnabled: 'false', + }); + expect(cached).toBe(original); + }); + + it('should return an inflight plugin load if it exsits', async () => { + const logSpy = jest.spyOn(pluginsLogger, 'logDebug').mockImplementation(() => {}); + const spy = jest + .spyOn(importPluginModule, 'importPluginModule') + .mockResolvedValue({ plugin: { ...panelPlugin } }); + + const original = pluginImporter.importPanel({ ...panelPlugin }); + const cached = pluginImporter.importPanel({ ...panelPlugin }); + await Promise.all([original, cached]); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith({ + path: 'public/plugins/test-plugin/module.js', + version: '1.0.0', + loadingStrategy: 'fetch', + pluginId: 'test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + }); + expect(logSpy).toHaveBeenCalledWith(`Retrieving plugin from inflight plugin load request`, { + expectedHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + loadingStrategy: 'fetch', + newPluginLoadingEnabled: 'false', + path: 'public/plugins/test-plugin/module.js', + pluginId: 'test-plugin', + pluginVersion: '1.0.0', + sriChecksEnabled: 'false', + }); + expect(cached).toBe(original); + }); + }); +}); + +const baseMeta: PluginMeta = { + id: 'test-plugin', + name: 'Test Plugin', + type: '' as PluginType, + module: 'public/plugins/test-plugin/module.js', + baseUrl: 'public/plugins/test-plugin', + moduleHash: 'cc3e6f370520e1efc6043f1874d735fabc710d4b', + translations: { 'en-US': 'public/plugins/test-plugin/locales/en-US/test-plugin.json' }, + info: { + author: { name: 'Test Author' }, + description: 'Test Description', + links: [], + logos: { large: '', small: '' }, + screenshots: [], + updated: '2023-01-01', + version: '1.0.0', + }, +}; + +const panelPlugin: PanelPluginMeta = { + ...baseMeta, + type: PluginType.panel, + sort: 0, +}; + +const dataSourcePlugin: DataSourcePluginMeta = { + ...baseMeta, + type: PluginType.datasource, +}; + +const appPlugin: AppPluginMeta = { + ...baseMeta, + type: PluginType.app, +}; diff --git a/public/app/features/plugins/importer/pluginImporter.ts b/public/app/features/plugins/importer/pluginImporter.ts new file mode 100644 index 00000000000..742a6739463 --- /dev/null +++ b/public/app/features/plugins/importer/pluginImporter.ts @@ -0,0 +1,185 @@ +import { + AppPlugin, + AppPluginMeta, + DataQuery, + DataSourceApi, + DataSourceJsonData, + DataSourcePlugin, + DataSourcePluginMeta, + PanelPlugin, + PanelPluginMeta, + PluginLoadingStrategy, + PluginMeta, + throwIfAngular, +} from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { GenericDataSourcePlugin } from 'app/features/datasources/types'; +import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError'; + +import { + addedComponentsRegistry, + addedFunctionsRegistry, + addedLinksRegistry, + exposedComponentsRegistry, +} from '../extensions/registry/setup'; +import { pluginsLogger } from '../utils'; + +import { importPluginModule } from './importPluginModule'; +import { PluginImporter, PostImportStrategy, PreImportStrategy } from './types'; + +const defaultPreImport: PreImportStrategy = (plugin) => { + throwIfAngular(plugin); + const fallbackLoadingStrategy = plugin.loadingStrategy ?? PluginLoadingStrategy.fetch; + + const args = { + path: plugin.module, + version: plugin.info?.version, + loadingStrategy: fallbackLoadingStrategy, + pluginId: plugin.id, + moduleHash: plugin.moduleHash, + translations: plugin.translations, + }; + + return args; +}; + +const panelPluginPostImport: PostImportStrategy<PanelPlugin, PanelPluginMeta> = async (meta, module) => { + try { + const pluginExports = await module; + + if (pluginExports.plugin) { + const plugin: PanelPlugin = pluginExports.plugin; + plugin.meta = meta; + pluginsCache.set(meta.id, plugin); + return plugin; + } + + throwIfAngular(pluginExports); + throw new Error('missing export: plugin'); + } catch (error) { + // TODO, maybe a different error plugin + console.warn('Error loading panel plugin: ' + meta.id, error); + return getPanelPluginLoadError(meta, error); + } +}; + +const datasourcePluginPostImport: PostImportStrategy<GenericDataSourcePlugin, DataSourcePluginMeta> = async ( + meta, + module +) => { + const pluginExports = await module; + + if (pluginExports.plugin) { + const dsPlugin: GenericDataSourcePlugin = pluginExports.plugin; + dsPlugin.meta = meta; + pluginsCache.set(meta.id, dsPlugin); + return dsPlugin; + } + + if (pluginExports.Datasource) { + const dsPlugin = new DataSourcePlugin<DataSourceApi<DataQuery, DataSourceJsonData>, DataQuery, DataSourceJsonData>( + pluginExports.Datasource + ); + dsPlugin.setComponentsFromLegacyExports(pluginExports); + dsPlugin.meta = meta; + pluginsCache.set(meta.id, dsPlugin); + return dsPlugin; + } + + throw new Error('Plugin module is missing DataSourcePlugin or Datasource constructor export'); +}; + +const appPluginPostImport: PostImportStrategy<AppPlugin, AppPluginMeta> = async (meta, module) => { + const pluginExports = await module; + + const { plugin = new AppPlugin() } = pluginExports; + plugin.init(meta); + plugin.meta = meta; + plugin.setComponentsFromLegacyExports(pluginExports); + + exposedComponentsRegistry.register({ pluginId: meta.id, configs: plugin.exposedComponentConfigs || [] }); + addedComponentsRegistry.register({ pluginId: meta.id, configs: plugin.addedComponentConfigs || [] }); + addedLinksRegistry.register({ pluginId: meta.id, configs: plugin.addedLinkConfigs || [] }); + addedFunctionsRegistry.register({ pluginId: meta.id, configs: plugin.addedFunctionConfigs || [] }); + + pluginsCache.set(meta.id, plugin); + return plugin; +}; + +const promisesCache: Map<string, Promise<PanelPlugin | GenericDataSourcePlugin | AppPlugin>> = new Map(); + +const getPromiseFromCache = <M extends PluginMeta, P extends PanelPlugin | GenericDataSourcePlugin | AppPlugin>( + meta: M +): Promise<P> => { + const cached = promisesCache.get(meta.id); + if (cached) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return cached as Promise<P>; + } + + throw new Error(`Trying to get unknown plugin type ${meta.type} from cache for plugin ${meta.id}`); +}; + +const pluginsCache: Map<string, PanelPlugin | GenericDataSourcePlugin | AppPlugin> = new Map(); + +const getPluginFromCache = <P extends PanelPlugin | GenericDataSourcePlugin | AppPlugin>(id: string): P | undefined => { + const cached = pluginsCache.get(id); + if (cached) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return cached as P; + } + + return undefined; +}; + +const importPlugin = <M extends PluginMeta, P extends PanelPlugin | GenericDataSourcePlugin | AppPlugin>( + meta: M, + postImportStrategy: PostImportStrategy<P, M>, + preImportStrategy: PreImportStrategy<M> = defaultPreImport +): Promise<P> => { + const cached = getPluginFromCache<P>(meta.id); + if (cached) { + pluginsLogger.logDebug(`Retrieving plugin from cache`, { + path: meta.module, + pluginId: meta.id, + pluginVersion: meta.info?.version ?? '', + expectedHash: meta.moduleHash ?? '', + loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch, + sriChecksEnabled: String(Boolean(config.featureToggles.pluginsSriChecks)), + newPluginLoadingEnabled: String(Boolean(config.featureToggles.enablePluginImporter)), + }); + return Promise.resolve(cached); + } + + if (promisesCache.has(meta.id)) { + pluginsLogger.logDebug(`Retrieving plugin from inflight plugin load request`, { + path: meta.module, + pluginId: meta.id, + pluginVersion: meta.info?.version ?? '', + expectedHash: meta.moduleHash ?? '', + loadingStrategy: meta.loadingStrategy ?? PluginLoadingStrategy.fetch, + sriChecksEnabled: String(Boolean(config.featureToggles.pluginsSriChecks)), + newPluginLoadingEnabled: String(Boolean(config.featureToggles.enablePluginImporter)), + }); + return getPromiseFromCache(meta); + } + + const args = preImportStrategy(meta); + const module = importPluginModule(args); + const plugin = postImportStrategy(meta, module); + promisesCache.set(meta.id, plugin); + + return getPromiseFromCache(meta); +}; + +export const pluginImporter: PluginImporter = { + importPanel: (meta: PanelPluginMeta) => importPlugin(meta, panelPluginPostImport), + importDataSource: (meta: DataSourcePluginMeta) => importPlugin(meta, datasourcePluginPostImport), + importApp: (meta: AppPluginMeta) => importPlugin(meta, appPluginPostImport), + getPanel: (id: string) => getPluginFromCache<PanelPlugin>(id), // we need this sync because how the panel plugins are loaded in PanelRenderer +}; + +export const clearCaches = () => { + promisesCache.clear(); + pluginsCache.clear(); +}; diff --git a/public/app/features/plugins/importer/types.ts b/public/app/features/plugins/importer/types.ts new file mode 100644 index 00000000000..01fcb78452e --- /dev/null +++ b/public/app/features/plugins/importer/types.ts @@ -0,0 +1,53 @@ +import { + AppPlugin, + AppPluginMeta, + DataSourcePluginMeta, + PanelPlugin, + PanelPluginMeta, + PluginLoadingStrategy, + PluginMeta, +} from '@grafana/data'; +import { GenericDataSourcePlugin } from 'app/features/datasources/types'; + +export interface PluginImporter { + /** + * Imports a panel plugin from module.js + * @param meta - The plugin meta + * @returns a Promise<PanelPlugin> + */ + importPanel: (meta: PanelPluginMeta) => Promise<PanelPlugin>; + /** + * Imports a datasource plugin from module.js + * @param meta - The plugin meta + * @returns a Promise<GenericDataSourcePlugin> + */ + importDataSource: (meta: DataSourcePluginMeta) => Promise<GenericDataSourcePlugin>; + /** + * Imports an app plugin from module.js + * @param meta - The plugin meta + * @returns a Promise<AppPlugin> + */ + importApp: (meta: AppPluginMeta) => Promise<AppPlugin>; + /** + * Retrieves a panel plugin from the cache, if it doesn't exist in the cache it returns undefined + * @param id - The plugin id + * @returns a PanelPlugin or undefined + */ + getPanel: (id: string) => PanelPlugin | undefined; +} + +export interface PluginImportInfo { + path: string; + pluginId: string; + loadingStrategy: PluginLoadingStrategy; + version?: string; + moduleHash?: string; + translations?: Record<string, string>; +} + +export type PreImportStrategy<M extends PluginMeta = PluginMeta> = (meta: M) => PluginImportInfo; + +export type PostImportStrategy< + P extends PanelPlugin | GenericDataSourcePlugin | AppPlugin, + M extends PluginMeta = PluginMeta, +> = (meta: M, module: Promise<System.Module>) => Promise<P>; diff --git a/public/app/features/plugins/loader/utils.ts b/public/app/features/plugins/loader/utils.ts index 2407852f6ee..b8986e8ade3 100644 --- a/public/app/features/plugins/loader/utils.ts +++ b/public/app/features/plugins/loader/utils.ts @@ -1,6 +1,6 @@ import { config } from '@grafana/runtime'; -import { sandboxPluginDependencies } from '../sandbox/plugin_dependencies'; +import { sandboxPluginDependencies } from '../sandbox/pluginDependencies'; import { SHARED_DEPENDENCY_PREFIX } from './constants'; import { SystemJS } from './systemjs'; diff --git a/public/app/features/plugins/tests/plugin_loader.test.ts b/public/app/features/plugins/pluginLoader.test.ts similarity index 90% rename from public/app/features/plugins/tests/plugin_loader.test.ts rename to public/app/features/plugins/pluginLoader.test.ts index 9f6c2e3739b..ea91ffb7d3e 100644 --- a/public/app/features/plugins/tests/plugin_loader.test.ts +++ b/public/app/features/plugins/pluginLoader.test.ts @@ -9,11 +9,11 @@ jest.mock('app/core/core', () => { import { AppPluginMeta, PluginMetaInfo, PluginType, AppPlugin } from '@grafana/data'; // Loaded after the `unmock` above -import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from '../extensions/registry/setup'; -import { SystemJS } from '../loader/systemjs'; -import { importAppPlugin } from '../plugin_loader'; +import { addedComponentsRegistry, addedLinksRegistry, exposedComponentsRegistry } from './extensions/registry/setup'; +import { SystemJS } from './loader/systemjs'; +import { importAppPlugin } from './pluginLoader'; -jest.mock('../extensions/registry/setup'); +jest.mock('./extensions/registry/setup'); describe('Load App', () => { const app = new AppPlugin(); diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/pluginLoader.ts similarity index 52% rename from public/app/features/plugins/plugin_loader.ts rename to public/app/features/plugins/pluginLoader.ts index 6a415316caf..2a5e50a93e5 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/pluginLoader.ts @@ -8,31 +8,27 @@ import { PluginMeta, throwIfAngular, } from '@grafana/data'; -import { DEFAULT_LANGUAGE } from '@grafana/i18n'; -import { addResourceBundle, getResolvedLanguage } from '@grafana/i18n/internal'; import { config } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { GenericDataSourcePlugin } from '../datasources/types'; -import builtInPlugins from './built_in_plugins'; import { addedComponentsRegistry, addedFunctionsRegistry, addedLinksRegistry, exposedComponentsRegistry, } from './extensions/registry/setup'; -import { getPluginFromCache, registerPluginInCache } from './loader/cache'; +import { importPluginModule } from './importer/importPluginModule'; +import { pluginImporter } from './importer/pluginImporter'; +import { getPluginFromCache } 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 { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; -import { shouldLoadPluginInFrontendSandbox } from './sandbox/sandbox_plugin_loader_registry'; -import { pluginsLogger } from './utils'; +import { buildImportMap } from './loader/utils'; const imports = buildImportMap(sharedDependenciesMap); @@ -78,84 +74,11 @@ systemJSPrototype.resolve = decorateSystemJSResolve.bind(systemJSPrototype, syst // Any css files loaded via SystemJS have their styles applied onload. systemJSPrototype.onload = decorateSystemJsOnload; -type PluginImportInfo = { - path: string; - pluginId: string; - loadingStrategy: PluginLoadingStrategy; - version?: string; - moduleHash?: string; - translations?: Record<string, string>; -}; - -export async function importPluginModule({ - path, - pluginId, - loadingStrategy, - version, - moduleHash, - translations, -}: PluginImportInfo): Promise<System.Module> { - if (version) { - registerPluginInCache({ path, version, loadingStrategy }); - } - - // Add locales to i18n for a plugin if the feature toggle is enabled and the plugin has locales - if (config.featureToggles.localizationForPlugins && translations) { - await addTranslationsToI18n({ - resolvedLanguage: getResolvedLanguage(), - fallbackLanguage: DEFAULT_LANGUAGE, - pluginId, - translations, - }); - } - - const builtIn = builtInPlugins[path]; - if (builtIn) { - // for handling dynamic imports - if (typeof builtIn === 'function') { - return await builtIn(); - } else { - return builtIn; - } - } - - const modulePath = resolveModulePath(path); - - // inject integrity hash into SystemJS import map - if (config.featureToggles.pluginsSriChecks) { - const resolvedModule = System.resolve(modulePath); - const integrityMap = System.getImportMap().integrity; - - if (moduleHash && integrityMap && !integrityMap[resolvedModule]) { - SystemJS.addImportMap({ - integrity: { - [resolvedModule]: moduleHash, - }, - }); - } - } - - // the sandboxing environment code cannot work in nodejs and requires a real browser - if (await shouldLoadPluginInFrontendSandbox({ pluginId })) { - return importPluginModuleInSandbox({ pluginId }); - } - - return SystemJS.import(modulePath).catch((e) => { - let error = new Error('Could not load plugin: ' + e); - console.error(error); - pluginsLogger.logError(error, { - path, - pluginId, - pluginVersion: version ?? '', - expectedHash: moduleHash ?? '', - loadingStrategy: loadingStrategy.toString(), - sriChecksEnabled: (config.featureToggles.pluginsSriChecks ?? false).toString(), - }); - throw error; - }); -} - export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<GenericDataSourcePlugin> { + if (config.featureToggles.enablePluginImporter) { + return pluginImporter.importDataSource(meta); + } + throwIfAngular(meta); const fallbackLoadingStrategy = meta.loadingStrategy ?? PluginLoadingStrategy.fetch; @@ -187,16 +110,25 @@ export function importDataSourcePlugin(meta: DataSourcePluginMeta): Promise<Gene }); } -// Only successfully loaded plugins are cached -const importedAppPlugins: Record<string, AppPlugin> = {}; +// Cache for import promises to prevent duplicate imports +const importPromises: Record<string, Promise<AppPlugin>> = {}; export async function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> { - const pluginId = meta.id; - - if (importedAppPlugins[pluginId]) { - return importedAppPlugins[pluginId]; + if (config.featureToggles.enablePluginImporter) { + return pluginImporter.importApp(meta); } + const pluginId = meta.id; + + // We are caching the import promises to prevent duplicate imports + if (importPromises[pluginId] === undefined) { + importPromises[pluginId] = doImportAppPlugin(meta); + } + + return importPromises[pluginId]; +} + +async function doImportAppPlugin(meta: PluginMeta): Promise<AppPlugin> { throwIfAngular(meta); const pluginExports = await importPluginModule({ @@ -214,69 +146,21 @@ export async function importAppPlugin(meta: PluginMeta): Promise<AppPlugin> { plugin.setComponentsFromLegacyExports(pluginExports); exposedComponentsRegistry.register({ - pluginId, + pluginId: meta.id, configs: plugin.exposedComponentConfigs || [], }); addedComponentsRegistry.register({ - pluginId, + pluginId: meta.id, configs: plugin.addedComponentConfigs || [], }); addedLinksRegistry.register({ - pluginId, + pluginId: meta.id, configs: plugin.addedLinkConfigs || [], }); addedFunctionsRegistry.register({ - pluginId, + pluginId: meta.id, configs: plugin.addedFunctionConfigs || [], }); - importedAppPlugins[pluginId] = plugin; - return plugin; } - -interface AddTranslationsToI18nOptions { - resolvedLanguage: string; - fallbackLanguage: string; - pluginId: string; - translations: Record<string, string>; -} - -// exported for testing purposes only -export async function addTranslationsToI18n({ - resolvedLanguage, - fallbackLanguage, - pluginId, - translations, -}: AddTranslationsToI18nOptions): Promise<void> { - const resolvedPath = translations[resolvedLanguage]; - const fallbackPath = translations[fallbackLanguage]; - const path = resolvedPath ?? fallbackPath; - - if (!path) { - console.warn(`Could not find any translation for plugin ${pluginId}`, { resolvedLanguage, fallbackLanguage }); - return; - } - - try { - const module = await SystemJS.import(resolveModulePath(path)); - if (!module.default) { - console.warn(`Could not find default export for plugin ${pluginId}`, { - resolvedLanguage, - fallbackLanguage, - path, - }); - return; - } - - const language = resolvedPath ? resolvedLanguage : fallbackLanguage; - addResourceBundle(language, pluginId, module.default); - } catch (error) { - console.warn(`Could not load translation for plugin ${pluginId}`, { - resolvedLanguage, - fallbackLanguage, - error, - path, - }); - } -} diff --git a/public/app/features/plugins/pluginPreloader.test.ts b/public/app/features/plugins/pluginPreloader.test.ts index 2aab1cd3dc3..4890184a638 100644 --- a/public/app/features/plugins/pluginPreloader.test.ts +++ b/public/app/features/plugins/pluginPreloader.test.ts @@ -11,8 +11,8 @@ import { import type { AppPluginConfig } from '@grafana/runtime'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; +import { importAppPlugin } from './pluginLoader'; import { clearPreloadedPluginsCache, preloadPlugins } from './pluginPreloader'; -import { importAppPlugin } from './plugin_loader'; jest.mock('app/core/services/context_srv', () => ({ contextSrv: { @@ -26,7 +26,7 @@ jest.mock('app/features/plugins/pluginSettings', () => ({ getPluginSettings: jest.fn(), })); -jest.mock('./plugin_loader', () => ({ +jest.mock('./pluginLoader', () => ({ importAppPlugin: jest.fn(), })); diff --git a/public/app/features/plugins/pluginPreloader.ts b/public/app/features/plugins/pluginPreloader.ts index b8284584a5c..f1bec151de1 100644 --- a/public/app/features/plugins/pluginPreloader.ts +++ b/public/app/features/plugins/pluginPreloader.ts @@ -7,7 +7,7 @@ import type { AppPluginConfig } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { importAppPlugin } from './plugin_loader'; +import { importAppPlugin } from './pluginLoader'; export type PluginPreloadResult = { pluginId: string; diff --git a/public/app/features/plugins/plugin_loader.test.ts b/public/app/features/plugins/plugin_loader.test.ts deleted file mode 100644 index 8fa6b6901e2..00000000000 --- a/public/app/features/plugins/plugin_loader.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { i18n } from 'i18next'; - -import * as i18nModule from '@grafana/i18n/internal'; - -import { server } from './loader/pluginLoader.mock'; -import { SystemJS } from './loader/systemjs'; -import { SystemJSWithLoaderHooks } from './loader/types'; -import { addTranslationsToI18n } from './plugin_loader'; - -describe('plugin_loader', () => { - describe('addTranslationsToI18n', () => { - const systemJSPrototype: SystemJSWithLoaderHooks = SystemJS.constructor.prototype; - const originalFetch = systemJSPrototype.fetch; - const originalResolve = systemJSPrototype.resolve; - let addResourceBundleSpy: jest.SpyInstance; - - beforeAll(() => { - server.listen(); - systemJSPrototype.resolve = (moduleId: string) => moduleId; - systemJSPrototype.shouldFetch = () => true; - // because server.listen() patches fetch, we need to reassign this to the systemJSPrototype - // this is identical to what happens in the original code: https://github.com/systemjs/systemjs/blob/main/src/features/fetch-load.js#L12 - systemJSPrototype.fetch = window.fetch; - }); - - beforeEach(() => { - addResourceBundleSpy = jest - .spyOn(i18nModule, 'addResourceBundle') - .mockImplementation(() => ({}) as unknown as i18n); - }); - - afterEach(() => { - server.resetHandlers(); - jest.clearAllMocks(); - }); - - afterAll(() => { - SystemJS.constructor.prototype.resolve = originalResolve; - SystemJS.constructor.prototype.fetch = originalFetch; - server.close(); - }); - - it('should add translations that match the resolved language first', async () => { - const translations = { - 'en-US': '/public/plugins/test-panel/locales/en-US/test-panel.json', - 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/test-panel.json', - }; - - await addTranslationsToI18n({ - resolvedLanguage: 'pt-BR', - fallbackLanguage: 'en-US', - pluginId: 'test-panel', - translations, - }); - - expect(addResourceBundleSpy).toHaveBeenCalledTimes(1); - expect(addResourceBundleSpy).toHaveBeenNthCalledWith(1, 'pt-BR', 'test-panel', { testKey: 'valorDeTeste' }); - }); - - it('should add translations that match the fallback language if the resolved language is not in the translations', async () => { - const translations = { - 'en-US': '/public/plugins/test-panel/locales/en-US/test-panel.json', - 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/test-panel.json', - }; - - await addTranslationsToI18n({ - resolvedLanguage: 'sv-SE', - fallbackLanguage: 'en-US', - pluginId: 'test-panel', - translations, - }); - - expect(addResourceBundleSpy).toHaveBeenCalledTimes(1); - expect(addResourceBundleSpy).toHaveBeenNthCalledWith(1, 'en-US', 'test-panel', { testKey: 'testValue' }); - }); - - it('should warn if no translations are found', async () => { - const translations = { - 'en-US': '/public/plugins/test-panel/locales/en-US/test-panel.json', - 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/test-panel.json', - }; - - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - await addTranslationsToI18n({ - resolvedLanguage: 'sv-SE', - fallbackLanguage: 'sv-SE', - pluginId: 'test-panel', - translations, - }); - - expect(consoleSpy).toHaveBeenCalledWith('Could not find any translation for plugin test-panel', { - resolvedLanguage: 'sv-SE', - fallbackLanguage: 'sv-SE', - }); - }); - - it('should warn if no exported default is found', async () => { - const translations = { - 'en-US': '/public/plugins/test-panel/locales/en-US/no-default-export.json', - 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/no-default-export.json', - }; - - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - await addTranslationsToI18n({ - resolvedLanguage: 'en-US', - fallbackLanguage: 'en-US', - pluginId: 'test-panel', - translations, - }); - - expect(consoleSpy).toHaveBeenCalledWith('Could not find default export for plugin test-panel', { - resolvedLanguage: 'en-US', - fallbackLanguage: 'en-US', - path: '/public/plugins/test-panel/locales/en-US/no-default-export.json', - }); - }); - - it('should warn if translations cannot be loaded', async () => { - const translations = { - 'en-US': '/public/plugins/test-panel/locales/en-US/unknown.json', - 'pt-BR': '/public/plugins/test-panel/locales/pt-BR/unknown.json', - }; - - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - await addTranslationsToI18n({ - resolvedLanguage: 'en-US', - fallbackLanguage: 'pt-BR', - pluginId: 'test-panel', - translations, - }); - - expect(consoleSpy).toHaveBeenCalledWith('Could not load translation for plugin test-panel', { - resolvedLanguage: 'en-US', - fallbackLanguage: 'pt-BR', - error: new TypeError('Failed to fetch'), - path: '/public/plugins/test-panel/locales/en-US/unknown.json', - }); - }); - }); -}); diff --git a/public/app/features/plugins/sandbox/code_loader.ts b/public/app/features/plugins/sandbox/codeLoader.ts similarity index 100% rename from public/app/features/plugins/sandbox/code_loader.ts rename to public/app/features/plugins/sandbox/codeLoader.ts diff --git a/public/app/features/plugins/sandbox/distortion_map.ts b/public/app/features/plugins/sandbox/distortions.ts similarity index 99% rename from public/app/features/plugins/sandbox/distortion_map.ts rename to public/app/features/plugins/sandbox/distortions.ts index 902f87afc09..b258f36978e 100644 --- a/public/app/features/plugins/sandbox/distortion_map.ts +++ b/public/app/features/plugins/sandbox/distortions.ts @@ -4,9 +4,9 @@ import { cloneDeep, isFunction } from 'lodash'; import { Monaco } from '@grafana/ui'; -import { loadScriptIntoSandbox } from './code_loader'; +import { loadScriptIntoSandbox } from './codeLoader'; import { forbiddenElements } from './constants'; -import { recursivePatchObjectAsLiveTarget } from './document_sandbox'; +import { recursivePatchObjectAsLiveTarget } from './documentSandbox'; import { SandboxEnvironment, SandboxPluginMeta } from './types'; import { logWarning, unboxRegexesFromMembraneProxy } from './utils'; diff --git a/public/app/features/plugins/sandbox/document_sandbox.ts b/public/app/features/plugins/sandbox/documentSandbox.ts similarity index 100% rename from public/app/features/plugins/sandbox/document_sandbox.ts rename to public/app/features/plugins/sandbox/documentSandbox.ts diff --git a/public/app/features/plugins/sandbox/plugin_dependencies.ts b/public/app/features/plugins/sandbox/pluginDependencies.ts similarity index 100% rename from public/app/features/plugins/sandbox/plugin_dependencies.ts rename to public/app/features/plugins/sandbox/pluginDependencies.ts diff --git a/public/app/features/plugins/sandbox/sandbox_components.tsx b/public/app/features/plugins/sandbox/sandboxComponents.tsx similarity index 74% rename from public/app/features/plugins/sandbox/sandbox_components.tsx rename to public/app/features/plugins/sandbox/sandboxComponents.tsx index 7e11e8428bc..29205b06bc0 100644 --- a/public/app/features/plugins/sandbox/sandbox_components.tsx +++ b/public/app/features/plugins/sandbox/sandboxComponents.tsx @@ -2,7 +2,12 @@ import { isFunction } from 'lodash'; import { ComponentType, FC } from 'react'; import * as React from 'react'; -import { GrafanaPlugin, PluginType } from '@grafana/data'; +import { + GrafanaPlugin, + PluginExtensionAddedComponentConfig, + PluginExtensionExposedComponentConfig, + PluginType, +} from '@grafana/data'; import { SandboxPluginMeta, SandboxedPluginObject } from './types'; import { isSandboxedPluginObject } from './utils'; @@ -58,6 +63,35 @@ export async function sandboxPluginComponents( Reflect.set(pluginObject, 'root', withSandboxWrapper(Reflect.get(pluginObject, 'root'), meta)); } + // Extensions: added components + if (Reflect.has(pluginObject, 'addedComponentConfigs')) { + const addedComponents: PluginExtensionAddedComponentConfig[] = Reflect.get(pluginObject, 'addedComponentConfigs'); + for (const addedComponent of addedComponents) { + if (Reflect.has(addedComponent, 'component')) { + Reflect.set(addedComponent, 'component', withSandboxWrapper(Reflect.get(addedComponent, 'component'), meta)); + } + } + Reflect.set(pluginObject, 'addedComponentConfigs', addedComponents); + } + + // Extensions: exposed components + if (Reflect.has(pluginObject, 'exposedComponentConfigs')) { + const exposedComponents: PluginExtensionExposedComponentConfig[] = Reflect.get( + pluginObject, + 'exposedComponentConfigs' + ); + for (const exposedComponent of exposedComponents) { + if (Reflect.has(exposedComponent, 'component')) { + Reflect.set( + exposedComponent, + 'component', + withSandboxWrapper(Reflect.get(exposedComponent, 'component'), meta) + ); + } + } + Reflect.set(pluginObject, 'exposedComponentConfigs', exposedComponents); + } + // config pages if (Reflect.has(pluginObject, 'configPages')) { const configPages: NonNullable<GrafanaPlugin['configPages']> = Reflect.get(pluginObject, 'configPages') ?? []; diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandboxPluginLoader.ts similarity index 97% rename from public/app/features/plugins/sandbox/sandbox_plugin_loader.ts rename to public/app/features/plugins/sandbox/sandboxPluginLoader.ts index 791e791ec3a..5b62216ff63 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoader.ts @@ -5,8 +5,8 @@ import { BootData } from '@grafana/data'; import { config } from '@grafana/runtime'; import { defaultTrustedTypesPolicy } from 'app/core/trustedTypePolicies'; -import { getPluginCode, getPluginLoadData, patchSandboxEnvironmentPrototype } from './code_loader'; -import { getGeneralSandboxDistortionMap, distortLiveApis } from './distortion_map'; +import { getPluginCode, getPluginLoadData, patchSandboxEnvironmentPrototype } from './codeLoader'; +import { getGeneralSandboxDistortionMap, distortLiveApis } from './distortions'; import { getSafeSandboxDomElement, isDomElement, @@ -14,9 +14,9 @@ import { markDomElementStyleAsALiveTarget, patchObjectAsLiveTarget, patchWebAPIs, -} from './document_sandbox'; -import { sandboxPluginDependencies } from './plugin_dependencies'; -import { sandboxPluginComponents } from './sandbox_components'; +} from './documentSandbox'; +import { sandboxPluginDependencies } from './pluginDependencies'; +import { sandboxPluginComponents } from './sandboxComponents'; import { CompartmentDependencyModule, PluginFactoryFunction, SandboxEnvironment, SandboxPluginMeta } from './types'; import { logError, logInfo } from './utils'; diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts similarity index 99% rename from public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts rename to public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts index e7164a5a860..a36480579d7 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.test.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.test.ts @@ -11,7 +11,7 @@ import { setSandboxEnabledCheck, isPluginFrontendSandboxEnabled, isPluginFrontendSandboxEligible, -} from './sandbox_plugin_loader_registry'; +} from './sandboxPluginLoaderRegistry'; jest.mock('@grafana/runtime', () => ({ config: { diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts b/public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts similarity index 100% rename from public/app/features/plugins/sandbox/sandbox_plugin_loader_registry.ts rename to public/app/features/plugins/sandbox/sandboxPluginLoaderRegistry.ts diff --git a/public/app/features/plugins/utils.ts b/public/app/features/plugins/utils.ts index 845785ce876..9961ee71016 100644 --- a/public/app/features/plugins/utils.ts +++ b/public/app/features/plugins/utils.ts @@ -2,8 +2,8 @@ import { GrafanaPlugin, NavModel, NavModelItem, PanelPluginMeta, PluginType } fr import { createMonitoringLogger } from '@grafana/runtime'; import { importPanelPluginFromMeta } from './importPanelPlugin'; +import { importAppPlugin, importDataSourcePlugin } from './pluginLoader'; import { getPluginSettings } from './pluginSettings'; -import { importAppPlugin, importDataSourcePlugin } from './plugin_loader'; export async function loadPlugin(pluginId: string): Promise<GrafanaPlugin> { const info = await getPluginSettings(pluginId); diff --git a/public/app/features/preferences/api/user/endpoints.gen.ts b/public/app/features/preferences/api/user/endpoints.gen.ts index 1ad485b471c..39b3ecf7560 100644 --- a/public/app/features/preferences/api/user/endpoints.gen.ts +++ b/public/app/features/preferences/api/user/endpoints.gen.ts @@ -44,10 +44,10 @@ export type Preferences = { homeDashboardUID?: string; /** Selected language (beta) */ language?: string; - /** Selected locale (beta) */ - locale?: string; navbar?: NavbarPreference; queryHistory?: QueryHistoryPreference; + /** Selected locale (beta) */ + regionalFormat?: string; /** light, dark, empty is default */ theme?: string; /** The timezone selection @@ -72,22 +72,26 @@ export type SuccessResponseBody = { export type CookieType = string; export type PatchPrefsCmd = { cookies?: CookieType[]; + /** The numerical :id of a favorited dashboard */ + homeDashboardId?: number; homeDashboardUID?: string; language?: string; - locale?: string; navbar?: NavbarPreference; queryHistory?: QueryHistoryPreference; + regionalFormat?: string; theme?: 'light' | 'dark'; timezone?: 'utc' | 'browser'; weekStart?: string; }; export type UpdatePrefsCmd = { cookies?: CookieType[]; + /** The numerical :id of a favorited dashboard */ + homeDashboardId?: number; homeDashboardUID?: string; language?: string; - locale?: string; navbar?: NavbarPreference; queryHistory?: QueryHistoryPreference; + regionalFormat?: string; theme?: 'light' | 'dark' | 'system'; timezone?: 'utc' | 'browser'; weekStart?: string; diff --git a/public/app/features/profile/ChangePasswordForm.tsx b/public/app/features/profile/ChangePasswordForm.tsx index fd7d7e58974..6f4ae223651 100644 --- a/public/app/features/profile/ChangePasswordForm.tsx +++ b/public/app/features/profile/ChangePasswordForm.tsx @@ -9,7 +9,7 @@ import { strongPasswordValidationRegister, } from 'app/core/components/ValidationLabels/ValidationLabels'; import config from 'app/core/config'; -import { UserDTO } from 'app/types'; +import { UserDTO } from 'app/types/user'; import { PasswordField } from '../../core/components/PasswordField/PasswordField'; diff --git a/public/app/features/profile/ChangePasswordPage.tsx b/public/app/features/profile/ChangePasswordPage.tsx index 3b7452cbfc5..0be35c8c0fc 100644 --- a/public/app/features/profile/ChangePasswordPage.tsx +++ b/public/app/features/profile/ChangePasswordPage.tsx @@ -2,7 +2,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { useMount } from 'react-use'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { ChangePasswordForm } from './ChangePasswordForm'; import { changePassword, loadUser } from './state/actions'; diff --git a/public/app/features/profile/UserOrganizations.tsx b/public/app/features/profile/UserOrganizations.tsx index 43638455d41..8f808173438 100644 --- a/public/app/features/profile/UserOrganizations.tsx +++ b/public/app/features/profile/UserOrganizations.tsx @@ -3,7 +3,7 @@ import { PureComponent } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { Button, LoadingPlaceholder, ScrollContainer } from '@grafana/ui'; -import { UserDTO, UserOrg } from 'app/types'; +import { UserDTO, UserOrg } from 'app/types/user'; export interface Props { user: UserDTO | null; diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index 3b99618268d..25b8c9a0b2b 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -3,7 +3,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Field, FieldSet, Icon, Input, Tooltip } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import config from 'app/core/config'; -import { UserDTO } from 'app/types'; +import { UserDTO } from 'app/types/user'; import { ProfileUpdateFields } from './types'; diff --git a/public/app/features/profile/UserProfileEditPage.tsx b/public/app/features/profile/UserProfileEditPage.tsx index c5f84818cb2..1dfa6045e81 100644 --- a/public/app/features/profile/UserProfileEditPage.tsx +++ b/public/app/features/profile/UserProfileEditPage.tsx @@ -6,7 +6,7 @@ import { usePluginComponents } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import UserOrganizations from './UserOrganizations'; import UserProfileEditForm from './UserProfileEditForm'; diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index 0eac0199b33..375db224fd0 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Icon, LoadingPlaceholder, ScrollContainer } from '@grafana/ui'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { formatDate } from 'app/core/internationalization/dates'; -import { UserSession } from 'app/types'; +import { UserSession } from 'app/types/user'; interface Props { sessions: UserSession[]; diff --git a/public/app/features/profile/UserTeams.tsx b/public/app/features/profile/UserTeams.tsx index 65d14160d7b..74b9c1e9aff 100644 --- a/public/app/features/profile/UserTeams.tsx +++ b/public/app/features/profile/UserTeams.tsx @@ -2,7 +2,7 @@ import { PureComponent } from 'react'; import { Trans, t } from '@grafana/i18n'; import { LoadingPlaceholder, ScrollContainer } from '@grafana/ui'; -import { Team } from 'app/types'; +import { Team } from 'app/types/teams'; export interface Props { teams: Team[]; diff --git a/public/app/features/profile/api.ts b/public/app/features/profile/api.ts index bb7e30d6b34..a5ab3247c9e 100644 --- a/public/app/features/profile/api.ts +++ b/public/app/features/profile/api.ts @@ -1,6 +1,6 @@ import { getBackendSrv } from '@grafana/runtime'; - -import { Team, UserDTO, UserOrg, UserSession } from '../../types'; +import { Team } from 'app/types/teams'; +import { UserDTO, UserOrg, UserSession } from 'app/types/user'; import { ChangePasswordFields, ProfileUpdateFields } from './types'; diff --git a/public/app/features/profile/state/actions.ts b/public/app/features/profile/state/actions.ts index e1e5f25600c..c98d764be78 100644 --- a/public/app/features/profile/state/actions.ts +++ b/public/app/features/profile/state/actions.ts @@ -1,6 +1,7 @@ import { config } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; +import { UserOrg } from 'app/types/user'; -import { ThunkResult, UserOrg } from '../../../types'; import { api } from '../api'; import { ChangePasswordFields, ProfileUpdateFields } from '../types'; diff --git a/public/app/features/profile/state/reducers.test.ts b/public/app/features/profile/state/reducers.test.ts index 67ff64f9e56..226b8e94d88 100644 --- a/public/app/features/profile/state/reducers.test.ts +++ b/public/app/features/profile/state/reducers.test.ts @@ -1,5 +1,6 @@ +import { OrgRole } from '@grafana/data'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { OrgRole } from '../../../types'; import { getMockTeam } from '../../teams/mocks/teamMocks'; import { diff --git a/public/app/features/profile/state/reducers.ts b/public/app/features/profile/state/reducers.ts index f4de9c34f0b..6d1bb9096e6 100644 --- a/public/app/features/profile/state/reducers.ts +++ b/public/app/features/profile/state/reducers.ts @@ -5,7 +5,9 @@ import { dateTimeFormatTimeAgo, setWeekStart, TimeZone } from '@grafana/data'; import { getWeekStart, WeekStart } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types'; +import { ThunkResult } from 'app/types/store'; +import { Team } from 'app/types/teams'; +import { UserDTO, UserOrg, UserSession } from 'app/types/user'; export interface UserState { orgId: number; diff --git a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx index c96311f7ea6..dd68b0f304f 100644 --- a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx +++ b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx @@ -1,8 +1,10 @@ import { css } from '@emotion/css'; +import { skipToken } from '@reduxjs/toolkit/query'; import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { Alert, Stack, useStyles2 } from '@grafana/ui'; import { useGetFrontendSettingsQuery, Repository } from 'app/api/clients/provisioning/v0alpha1'; import provisioningSvg from 'img/provisioning/provisioning.svg'; @@ -121,7 +123,10 @@ interface Props { export default function GettingStarted({ items }: Props) { const styles = useStyles2(getStyles); - const settingsQuery = useGetFrontendSettingsQuery(undefined, { refetchOnMountOrArgChange: true }); + const settingsArg = config.featureToggles.provisioning ? undefined : skipToken; + const settingsQuery = useGetFrontendSettingsQuery(settingsArg, { + refetchOnMountOrArgChange: true, + }); const legacyStorage = settingsQuery.data?.legacyStorage; const hasItems = Boolean(settingsQuery.data?.items?.length); const { hasPublicAccess, hasImageRenderer, hasRequiredFeatures } = getConfigurationStatus(); diff --git a/public/app/features/provisioning/Wizard/BootstrapStep.test.tsx b/public/app/features/provisioning/Wizard/BootstrapStep.test.tsx index ae51dcf0c67..29ad097ae63 100644 --- a/public/app/features/provisioning/Wizard/BootstrapStep.test.tsx +++ b/public/app/features/provisioning/Wizard/BootstrapStep.test.tsx @@ -187,6 +187,7 @@ describe('BootstrapStep', () => { settingsData: { legacyStorage: true, items: [], + availableRepositoryTypes: [], }, }); @@ -239,6 +240,7 @@ describe('BootstrapStep', () => { settingsData: { legacyStorage: true, items: [], + availableRepositoryTypes: [], }, }); diff --git a/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts b/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts index bf64a049370..aaa13265ae7 100644 --- a/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts +++ b/public/app/features/provisioning/hooks/useGetResourceRepositoryView.ts @@ -1,5 +1,6 @@ import { skipToken } from '@reduxjs/toolkit/query/react'; +import { config } from '@grafana/runtime'; import { Folder, useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; import { RepositoryView, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; import { AnnoKeyManagerIdentity } from 'app/features/apiserver/types'; @@ -18,12 +19,19 @@ interface RepositoryViewData { // This is safe to call as a viewer (you do not need full access to the Repository configs) export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRepositoryArgs): RepositoryViewData => { - const { data: settingsData, isLoading: isSettingsLoading } = useGetFrontendSettingsQuery(); - const skipFolderQuery = !folderName; + const provisioningEnabled = config.featureToggles.provisioning; + const { data: settingsData, isLoading: isSettingsLoading } = useGetFrontendSettingsQuery( + !provisioningEnabled ? skipToken : undefined + ); + const skipFolderQuery = !folderName || !provisioningEnabled; const { data: folder, isLoading: isFolderLoading } = useGetFolderQuery( skipFolderQuery ? skipToken : { name: folderName } ); + if (!provisioningEnabled) { + return { isLoading: false, isInstanceManaged: false }; + } + if (isSettingsLoading || isFolderLoading) { return { isLoading: true, isInstanceManaged: false }; } diff --git a/public/app/features/provisioning/hooks/usePullRequestParam.ts b/public/app/features/provisioning/hooks/usePullRequestParam.ts index 911a8ce988a..e4432232900 100644 --- a/public/app/features/provisioning/hooks/usePullRequestParam.ts +++ b/public/app/features/provisioning/hooks/usePullRequestParam.ts @@ -4,10 +4,10 @@ import { useUrlParams } from 'app/core/navigation/hooks'; export const usePullRequestParam = () => { const [params] = useUrlParams(); const prParam = params.get('pull_request_url'); + const newPrParam = params.get('new_pull_request_url'); - if (!prParam) { - return undefined; - } - - return textUtil.sanitizeUrl(decodeURIComponent(prParam)); + return { + prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined, + newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined, + }; }; diff --git a/public/app/features/provisioning/utils/routes.ts b/public/app/features/provisioning/utils/routes.ts index c0e828a66a6..891ecd090d9 100644 --- a/public/app/features/provisioning/utils/routes.ts +++ b/public/app/features/provisioning/utils/routes.ts @@ -1,6 +1,6 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport'; import { RouteDescriptor } from 'app/core/navigation/types'; -import { DashboardRoutes } from 'app/types'; +import { DashboardRoutes } from 'app/types/dashboard'; import { checkRequiredFeatures } from '../GettingStarted/features'; import { PROVISIONING_URL, CONNECT_URL, GETTING_STARTED_URL } from '../constants'; diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index b5275a367e9..8737449101f 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -26,7 +26,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions } from 'app/types/query'; import { PanelQueryRunner } from '../state/PanelQueryRunner'; import { updateQueries } from '../state/updateQueries'; diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index b28c4ce7128..2404236e483 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -5,7 +5,7 @@ import { rangeUtil, PanelData, DataSourceApi, GrafanaTheme2 } from '@grafana/dat import { Trans, t } from '@grafana/i18n'; import { Input, InlineSwitch, useStyles2, InlineLabel } from '@grafana/ui'; import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions } from 'app/types/query'; interface Props { options: QueryGroupOptions; diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts index 3c07c21bf5c..4c0f49aacd9 100644 --- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts @@ -12,7 +12,7 @@ import { import { Annotation } from 'app/features/alerting/unified/utils/constants'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import * as store from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { PromAlertingRuleState, PromRulesResponse, PromRuleType } from 'app/types/unified-alerting-dto'; import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput'; diff --git a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts index 18c001c983c..8610d84f8e1 100644 --- a/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts +++ b/public/app/features/query/state/DashboardQueryRunner/UnifiedAlertStatesWorker.ts @@ -11,7 +11,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d import { prometheusRuleType } from 'app/features/alerting/unified/utils/rules'; import { promAlertStateToAlertState } from 'app/features/dashboard-scene/scene/AlertStatesDataLayer'; import { dispatch } from 'app/store/store'; -import { AccessControlAction } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { RuleNamespace } from 'app/types/unified-alerting'; import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; diff --git a/public/app/features/scopes/selector/RecentScopes.tsx b/public/app/features/scopes/selector/RecentScopes.tsx index e8bc2633398..da7d2070954 100644 --- a/public/app/features/scopes/selector/RecentScopes.tsx +++ b/public/app/features/scopes/selector/RecentScopes.tsx @@ -73,5 +73,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), legend: css({ marginBottom: 0, + padding: `${theme.spacing(0.5)} 0`, }), }); diff --git a/public/app/features/scopes/tests/utils/render.tsx b/public/app/features/scopes/tests/utils/render.tsx index e4c6993928b..ed126b5b2d7 100644 --- a/public/app/features/scopes/tests/utils/render.tsx +++ b/public/app/features/scopes/tests/utils/render.tsx @@ -8,7 +8,7 @@ import { sceneGraph } from '@grafana/scenes'; import { defaultDashboard } from '@grafana/schema'; import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; import { transformSaveModelToScene } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene'; -import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types'; +import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types/dashboard'; import { defaultScopesServices, ScopesContextProvider } from '../../ScopesContextProvider'; diff --git a/public/app/features/search/service/deletedDashboardsCache.ts b/public/app/features/search/service/deletedDashboardsCache.ts index cab6774b97b..7559d00605c 100644 --- a/public/app/features/search/service/deletedDashboardsCache.ts +++ b/public/app/features/search/service/deletedDashboardsCache.ts @@ -1,7 +1,6 @@ import { isResourceList } from 'app/features/apiserver/guards'; import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; - -import { DashboardDataDTO } from '../../../types'; +import { DashboardDataDTO } from 'app/types/dashboard'; import { SearchHit } from './unified'; import { resourceToSearchResult } from './utils'; diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts index d8f468ee774..ba0dbc9c894 100644 --- a/public/app/features/search/service/sql.ts +++ b/public/app/features/search/service/sql.ts @@ -2,7 +2,7 @@ import { DataFrame, DataFrameView, FieldType, getDisplayProcessor, SelectableVal import { config } from '@grafana/runtime'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; import { backendSrv } from 'app/core/services/backend_srv'; -import { PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; import { DEFAULT_MAX_VALUES, GENERAL_FOLDER_UID, TYPE_KIND_MAP } from '../constants'; import { DashboardSearchHit, DashboardSearchItemType } from '../types'; diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index cb360ba8deb..3ebda023037 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -1,6 +1,6 @@ import { DataFrameView, SelectableValue } from '@grafana/data'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; -import { PermissionLevelString } from 'app/types'; +import { PermissionLevelString } from 'app/types/acl'; import { ManagerKind } from '../../apiserver/types'; diff --git a/public/app/features/search/service/utils.ts b/public/app/features/search/service/utils.ts index 94fb1e7d345..981127f6688 100644 --- a/public/app/features/search/service/utils.ts +++ b/public/app/features/search/service/utils.ts @@ -2,8 +2,8 @@ import { DataFrameView, IconName, fuzzySearch } from '@grafana/data'; import { isSharedWithMe } from 'app/features/browse-dashboards/components/utils'; import { DashboardViewItemWithUIItems } from 'app/features/browse-dashboards/types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { DashboardDataDTO } from 'app/types/dashboard'; -import { DashboardDataDTO } from '../../../types'; import { AnnoKeyFolder, ResourceList } from '../../apiserver/types'; import { DashboardSearchHit, DashboardSearchItemType, DashboardViewItem, DashboardViewItemKind } from '../types'; diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index fb210f4b0c0..b65b380cebf 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config, getBackendSrv, locationService } from '@grafana/runtime'; import { Button, Input, Field, FieldSet } from '@grafana/ui'; @@ -10,7 +11,8 @@ import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { RolePickerSelect } from 'app/core/components/RolePickerDrawer/RolePickerSelect'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO, ServiceAccountCreateApiResponse } from 'app/types/serviceaccount'; import { OrgRolePicker } from '../admin/OrgRolePicker'; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index ee61c2af792..4de16f29479 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -2,7 +2,9 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { ApiKey, OrgRole, ServiceAccountDTO } from 'app/types'; +import { OrgRole } from '@grafana/data'; +import { ApiKey } from 'app/types/apiKeys'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; import { ServiceAccountPageUnconnected, Props } from './ServiceAccountPage'; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index bebd5adaf51..b21f6c9fe6d 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -7,7 +7,10 @@ import { Trans, t } from '@grafana/i18n'; import { Button, ConfirmModal, IconButton, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, ApiKey, ServiceAccountDTO, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ApiKey } from 'app/types/apiKeys'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; +import { StoreState } from 'app/types/store'; import { ServiceAccountPermissions } from './ServiceAccountPermissions'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; @@ -228,8 +231,14 @@ export const ServiceAccountPageUnconnected = ({ 'serviceaccounts.service-account-page-unconnected.title-delete-service-account', 'Delete service account' )} - body="Are you sure you want to delete this service account?" - confirmText="Delete service account" + body={t( + 'serviceaccounts.service-account-page-unconnected.body-delete-service-account', + 'Are you sure you want to delete this service account?' + )} + confirmText={t( + 'serviceaccounts.service-account-page-unconnected.confirmText-delete-service-account', + 'Delete service account' + )} onConfirm={handleServiceAccountDelete} onDismiss={showDeleteServiceAccountModal(false)} /> @@ -239,8 +248,14 @@ export const ServiceAccountPageUnconnected = ({ 'serviceaccounts.service-account-page-unconnected.title-disable-service-account', 'Disable service account' )} - body="Are you sure you want to disable this service account?" - confirmText="Disable service account" + body={t( + 'serviceaccounts.service-account-page-unconnected.body-disable-service-account', + 'Are you sure you want to disable this service account?' + )} + confirmText={t( + 'serviceaccounts.service-account-page-unconnected.confirmText-disable-service-account', + 'Disable service account' + )} onConfirm={handleServiceAccountDisable} onDismiss={showDisableServiceAccountModal(false)} /> diff --git a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx index e6f3adc78b3..303dfc79716 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPermissions.tsx @@ -1,8 +1,8 @@ import { t } from '@grafana/i18n'; import { Permissions } from 'app/core/components/AccessControl'; import { contextSrv } from 'app/core/services/context_srv'; - -import { AccessControlAction, ServiceAccountDTO } from '../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; type ServiceAccountPermissionsProps = { serviceAccount: ServiceAccountDTO; diff --git a/public/app/features/serviceaccounts/ServiceAccountTable.tsx b/public/app/features/serviceaccounts/ServiceAccountTable.tsx index 648cb391fb8..8d7363fa37f 100644 --- a/public/app/features/serviceaccounts/ServiceAccountTable.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountTable.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import Skeleton from 'react-loading-skeleton'; +import { OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Avatar, @@ -16,7 +17,8 @@ import { } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, OrgRole, Role, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; import { OrgRolePicker } from '../admin/OrgRolePicker'; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx index f5f563321af..d4b870c2921 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx @@ -2,7 +2,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; -import { OrgRole, ServiceAccountDTO, ServiceAccountStateFilter } from 'app/types'; +import { OrgRole } from '@grafana/data'; +import { ServiceAccountStateFilter, ServiceAccountDTO } from 'app/types/serviceaccount'; import { Props, ServiceAccountsListPageUnconnected } from './ServiceAccountsListPage'; diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 6a8292abd9c..9929f0dfe91 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -1,5 +1,4 @@ import { css } from '@emotion/css'; -import pluralize from 'pluralize'; import { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; @@ -20,7 +19,9 @@ import { import { Page } from 'app/core/components/Page/Page'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { StoreState, ServiceAccountDTO, AccessControlAction, ServiceAccountStateFilter } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountStateFilter, ServiceAccountDTO } from 'app/types/serviceaccount'; +import { StoreState } from 'app/types/store'; import { ServiceAccountTable } from './ServiceAccountTable'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; @@ -268,15 +269,25 @@ export const ServiceAccountsListPageUnconnected = ({ <> <ConfirmModal isOpen={isRemoveModalOpen} - body={`Are you sure you want to delete '${currentServiceAccount.name}'${ + body={ !!currentServiceAccount.tokens - ? ` and ${currentServiceAccount.tokens} accompanying ${pluralize( - 'token', - currentServiceAccount.tokens - )}` - : '' - }?`} - confirmText="Delete" + ? t( + 'serviceaccounts.service-accounts-list-page-unconnected.body-delete', + 'Are you sure you want to delete {{serviceAccountName}} and {{count}} accompanying tokens?', + { + serviceAccountName: currentServiceAccount.name, + count: currentServiceAccount.tokens, + } + ) + : t( + 'serviceaccounts.service-accounts-list-page-unconnected.body-delete-with-tokens', + 'Are you sure you want to delete {{serviceAccountName}}?', + { + serviceAccountName: currentServiceAccount.name, + } + ) + } + confirmText={t('serviceaccounts.service-accounts-list-page-unconnected.confirmText-delete', 'Delete')} title={t( 'serviceaccounts.service-accounts-list-page-unconnected.title-delete-service-account', 'Delete service account' @@ -290,8 +301,15 @@ export const ServiceAccountsListPageUnconnected = ({ 'serviceaccounts.service-accounts-list-page-unconnected.title-disable-service-account', 'Disable service account' )} - body={`Are you sure you want to disable '${currentServiceAccount.name}'?`} - confirmText="Disable service account" + body={t( + 'serviceaccounts.service-accounts-list-page-unconnected.body-disable-service-account', + "Are you sure you want to disable '{{accountToDisable}}'?", + { accountToDisable: currentServiceAccount.name } + )} + confirmText={t( + 'serviceaccounts.service-accounts-list-page-unconnected.confirmText-disable-service-account', + 'Disable service account' + )} onConfirm={onDisable} onDismiss={onDisableModalClose} /> diff --git a/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx b/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx index 9e52f25df92..563d0d8b89d 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountProfile.tsx @@ -6,7 +6,8 @@ import { Trans, t } from '@grafana/i18n'; import { Label, TextLink, useStyles2 } from '@grafana/ui'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Role, ServiceAccountDTO } from 'app/types'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; import { ServiceAccountProfileRow } from './ServiceAccountProfileRow'; import { ServiceAccountRoleRow } from './ServiceAccountRoleRow'; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountProfileRow.tsx b/public/app/features/serviceaccounts/components/ServiceAccountProfileRow.tsx index a855b936e61..428c78175a8 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountProfileRow.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountProfileRow.tsx @@ -85,7 +85,7 @@ export const ServiceAccountProfileRow = ({ label, value, inputType, disabled, on {onChange && ( <ConfirmButton closeOnConfirm - confirmText="Save" + confirmText={t('serviceaccounts.service-account-profile-row.confirmText-save', 'Save')} onConfirm={onSave} onClick={onEditClick} onCancel={onCancelClick} diff --git a/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx b/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx index 015fd003b33..33a33229b87 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountRoleRow.tsx @@ -1,9 +1,11 @@ +import { OrgRole } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Label } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { contextSrv } from 'app/core/core'; import { OrgRolePicker } from 'app/features/admin/OrgRolePicker'; -import { AccessControlAction, OrgRole, Role, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; interface Props { label: string; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx b/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx index 4c065d0d1df..66b2e818efd 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountTokensTable.tsx @@ -3,7 +3,7 @@ import { css, cx } from '@emotion/css'; import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { DeleteButton, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui'; -import { ApiKey } from 'app/types'; +import { ApiKey } from 'app/types/apiKeys'; interface Props { tokens: ApiKey[]; diff --git a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx index e9a2478557b..9bf659f1500 100644 --- a/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx +++ b/public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx @@ -9,7 +9,8 @@ import { SkeletonComponent, attachSkeleton } from '@grafana/ui/unstable'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { contextSrv } from 'app/core/core'; import { OrgRolePicker } from 'app/features/admin/OrgRolePicker'; -import { AccessControlAction, Role, ServiceAccountDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; type ServiceAccountListItemProps = { serviceAccount: ServiceAccountDTO; diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 06591867aa4..5e75bc5378f 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -3,7 +3,9 @@ import { debounce } from 'lodash'; import { getBackendSrv } from '@grafana/runtime'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, ServiceAccountDTO, ServiceAccountStateFilter, ThunkResult } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ServiceAccountDTO, ServiceAccountStateFilter } from 'app/types/serviceaccount'; +import { ThunkResult } from 'app/types/store'; import { ServiceAccountToken } from '../components/CreateTokenModal'; diff --git a/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts b/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts index 4644d3ae136..0921d2e28c4 100644 --- a/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts +++ b/public/app/features/serviceaccounts/state/actionsServiceAccountPage.ts @@ -1,6 +1,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { ServiceAccountDTO, ThunkResult } from 'app/types'; +import { ServiceAccountDTO } from 'app/types/serviceaccount'; +import { ThunkResult } from 'app/types/store'; import { ServiceAccountToken } from '../components/CreateTokenModal'; diff --git a/public/app/features/serviceaccounts/state/reducers.ts b/public/app/features/serviceaccounts/state/reducers.ts index 5794233d434..705c07c4287 100644 --- a/public/app/features/serviceaccounts/state/reducers.ts +++ b/public/app/features/serviceaccounts/state/reducers.ts @@ -1,13 +1,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { Role } from 'app/types/accessControl'; +import { ApiKey } from 'app/types/apiKeys'; import { - ApiKey, - Role, - ServiceAccountDTO, ServiceAccountProfileState, + ServiceAccountDTO, ServiceAccountsState, ServiceAccountStateFilter, -} from 'app/types'; +} from 'app/types/serviceaccount'; // serviceAccountsProfilePage export const initialStateProfile: ServiceAccountProfileState = { diff --git a/public/app/features/support-bundles/SupportBundles.tsx b/public/app/features/support-bundles/SupportBundles.tsx index 2963dad4d0f..b21604caae0 100644 --- a/public/app/features/support-bundles/SupportBundles.tsx +++ b/public/app/features/support-bundles/SupportBundles.tsx @@ -6,7 +6,8 @@ import { Trans, t } from '@grafana/i18n'; import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; import { loadBundles, removeBundle, checkBundles } from './state/actions'; diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index 1d916884cb8..4461870556a 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -5,7 +5,7 @@ import { Trans, t } from '@grafana/i18n'; import { Button, Field, Checkbox, LinkButton, Stack, Alert } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { loadSupportBundleCollectors, createSupportBundle } from './state/actions'; diff --git a/public/app/features/support-bundles/state/actions.ts b/public/app/features/support-bundles/state/actions.ts index f42babe81ec..d3270e1a5da 100644 --- a/public/app/features/support-bundles/state/actions.ts +++ b/public/app/features/support-bundles/state/actions.ts @@ -1,7 +1,8 @@ import { throttle } from 'lodash'; import { getBackendSrv, locationService } from '@grafana/runtime'; -import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest, ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; +import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest } from 'app/types/supportBundles'; import { collectorsFetchBegin, diff --git a/public/app/features/support-bundles/state/reducers.ts b/public/app/features/support-bundles/state/reducers.ts index 21e32402ec0..81a1e0d5e80 100644 --- a/public/app/features/support-bundles/state/reducers.ts +++ b/public/app/features/support-bundles/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { SupportBundle, SupportBundleCollector, SupportBundlesState } from 'app/types'; +import { SupportBundlesState, SupportBundle, SupportBundleCollector } from 'app/types/supportBundles'; export const initialState: SupportBundlesState = { supportBundles: [], diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index c52a71e4bcb..0744271c048 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -10,7 +10,8 @@ import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; import { updateTeamRoles } from 'app/core/components/RolePicker/api'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, Role, TeamDTO } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { TeamDTO } from 'app/types/teams'; const pageNav: NavModelItem = { icon: 'users-alt', diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index 841a85b5d4b..bda03e89d5d 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { TeamGroup } from '../../types'; +import { TeamGroup } from 'app/types/teams'; import { Props, TeamGroupSync } from './TeamGroupSync'; import { getMockTeamGroups } from './mocks/teamMocks'; diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index e45c525144f..e87a1d4380f 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -9,8 +9,8 @@ import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { UpgradeBox, UpgradeContent, UpgradeContentProps } from 'app/core/components/Upgrade/UpgradeBox'; import { highlightTrial } from 'app/features/admin/utils'; - -import { StoreState, TeamGroup } from '../../types'; +import { StoreState } from 'app/types/store'; +import { TeamGroup } from 'app/types/teams'; import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; import { getTeamGroups } from './state/selectors'; diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 4826c2bc24f..a5f8adc3f6e 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -3,8 +3,7 @@ import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { contextSrv } from 'app/core/services/context_srv'; - -import { Team } from '../../types'; +import { Team } from 'app/types/teams'; import { Props, TeamList } from './TeamList'; import { getMockTeam, getMultipleMockTeams } from './mocks/teamMocks'; diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index dd93bd941e7..b1a5952eca8 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -24,7 +24,9 @@ import { import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Role, StoreState, TeamWithRoles } from 'app/types'; +import { Role, AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; +import { TeamWithRoles } from 'app/types/teams'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; import { EnterpriseAuthFeaturesCard } from '../admin/EnterpriseAuthFeaturesCard'; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 940f6950d1a..89a5cb5e032 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -9,7 +9,8 @@ import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox'; import config from 'app/core/config'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, StoreState, useDispatch, useSelector } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState, useDispatch, useSelector } from 'app/types/store'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync'; import TeamPermissions from './TeamPermissions'; diff --git a/public/app/features/teams/TeamPermissions.tsx b/public/app/features/teams/TeamPermissions.tsx index 349b2531dab..47823bfcae6 100644 --- a/public/app/features/teams/TeamPermissions.tsx +++ b/public/app/features/teams/TeamPermissions.tsx @@ -1,7 +1,7 @@ import { Permissions } from 'app/core/components/AccessControl'; import { contextSrv } from 'app/core/services/context_srv'; - -import { AccessControlAction, Team } from '../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Team } from 'app/types/teams'; type TeamPermissionsProps = { team: Team; diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index fa960f25336..79e9ff95dd9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -7,7 +7,8 @@ import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction, Team } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { Team } from 'app/types/teams'; import { updateTeam } from './state/actions'; diff --git a/public/app/features/teams/mocks/teamMocks.ts b/public/app/features/teams/mocks/teamMocks.ts index 3da74f056f1..d6472499cb2 100644 --- a/public/app/features/teams/mocks/teamMocks.ts +++ b/public/app/features/teams/mocks/teamMocks.ts @@ -1,6 +1,7 @@ import { randomBytes } from 'crypto'; -import { Team, TeamGroup, TeamMember, TeamPermissionLevel } from 'app/types'; +import { TeamPermissionLevel } from 'app/types/acl'; +import { Team, TeamMember, TeamGroup } from 'app/types/teams'; function generateShortUid(): string { return randomBytes(3).toString('hex'); // Generate a short UID diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 967c87ac904..e7a6c5a923a 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -5,7 +5,9 @@ import { FetchDataArgs } from '@grafana/ui'; import { updateNavIndex } from 'app/core/actions'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { AccessControlAction, TeamWithRoles, TeamMember, ThunkResult, Team } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ThunkResult } from 'app/types/store'; +import { Team, TeamMember, TeamWithRoles } from 'app/types/teams'; import { buildNavModel } from './navModel'; import { diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts index 74decae3e94..b6267c73d0e 100644 --- a/public/app/features/teams/state/navModel.ts +++ b/public/app/features/teams/state/navModel.ts @@ -5,7 +5,9 @@ import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import config from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; import { highlightTrial } from 'app/features/admin/utils'; -import { AccessControlAction, Team, TeamPermissionLevel } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { TeamPermissionLevel } from 'app/types/acl'; +import { Team } from 'app/types/teams'; import userProfilePng from 'img/user_profile.png'; const loadingTeam = { diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 483b22700f0..e42e467c6b1 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,5 +1,6 @@ +import { TeamsState, TeamState } from 'app/types/teams'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { TeamsState, TeamState } from '../../../types'; import { getMockTeam, getMockTeamGroups, getMockTeamMember } from '../mocks/teamMocks'; import { diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 4602f33540c..b98c6d21095 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types'; +import { TeamsState, Team, TeamState, TeamMember, TeamGroup } from 'app/types/teams'; export const initialTeamsState: TeamsState = { teams: [], diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 58f4dbbeeef..b1fed3d1ef3 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,4 +1,5 @@ -import { TeamState } from '../../../types'; +import { TeamState } from 'app/types/teams'; + import { getMockTeam } from '../mocks/teamMocks'; import { getTeam } from './selectors'; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index ec8e6eddfc4..ec99297158c 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,4 +1,4 @@ -import { Team, TeamState } from 'app/types'; +import { Team, TeamState } from 'app/types/teams'; export const getTeamGroups = (state: TeamState) => state.groups; diff --git a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx index a23cf8cb061..366da659cf2 100644 --- a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx @@ -24,23 +24,37 @@ import { Trans, t } from '@grafana/i18n'; import { Button, RadioButtonGroup, InlineField, Box } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/filterByValue.svg'; +import lightImage from '../images/light/filterByValue.svg'; import { DataFrameFieldsInfo, FilterByValueFilterEditor } from './FilterByValueFilterEditor'; -const filterTypes: Array<SelectableValue<FilterByValueType>> = [ - { label: 'Include', value: FilterByValueType.include }, - { label: 'Exclude', value: FilterByValueType.exclude }, -]; - -const filterMatch: Array<SelectableValue<FilterByValueMatch>> = [ - { label: 'Match all', value: FilterByValueMatch.all }, - { label: 'Match any', value: FilterByValueMatch.any }, -]; - export const FilterByValueTransformerEditor = (props: TransformerUIProps<FilterByValueTransformerOptions>) => { const { input, options, onChange } = props; const fieldsInfo = useFieldsInfo(input); + const filterTypes: Array<SelectableValue<FilterByValueType>> = [ + { + label: t('transformers.filter-by-value-transformer-editor.filter-types.label.include', 'Include'), + value: FilterByValueType.include, + }, + { + label: t('transformers.filter-by-value-transformer-editor.filter-types.label.exclude', 'Exclude'), + value: FilterByValueType.exclude, + }, + ]; + + const filterMatch: Array<SelectableValue<FilterByValueMatch>> = [ + { + label: t('transformers.filter-by-value-transformer-editor.filter-match.label.match-all', 'Match all'), + value: FilterByValueMatch.all, + }, + { + label: t('transformers.filter-by-value-transformer-editor.filter-match.label.match-any', 'Match any'), + value: FilterByValueMatch.any, + }, + ]; + const onAddFilter = useCallback(() => { const frame = input[0]; const field = frame.fields.find((f) => f.type !== FieldType.time); @@ -136,16 +150,21 @@ export const FilterByValueTransformerEditor = (props: TransformerUIProps<FilterB ); }; -export const filterByValueTransformRegistryItem: TransformerRegistryItem<FilterByValueTransformerOptions> = { - id: DataTransformerID.filterByValue, - editor: FilterByValueTransformerEditor, - transformation: standardTransformers.filterByValueTransformer, - name: standardTransformers.filterByValueTransformer.name, - description: - 'Removes rows of the query results using user-defined filters. This is useful if you can not filter your data in the data source.', - categories: new Set([TransformerCategory.Filter]), - help: getTransformationContent(DataTransformerID.filterByValue).helperDocs, -}; +export const getFilterByValueTransformRegistryItem: () => TransformerRegistryItem<FilterByValueTransformerOptions> = + () => ({ + id: DataTransformerID.filterByValue, + editor: FilterByValueTransformerEditor, + transformation: standardTransformers.filterByValueTransformer, + name: t('transformers.filter-by-value-transformer-editor.name.filter-data-by-values', 'Filter data by values'), + description: t( + 'transformers.filter-by-value-transformer-editor.description.remove-rows-query-results-user-defined-filters', + 'Remove rows from the query results using user-defined filters.' + ), + categories: new Set([TransformerCategory.Filter]), + help: getTransformationContent(DataTransformerID.filterByValue).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); const useFieldsInfo = (data: DataFrame[]): DataFrameFieldsInfo => { return useMemo(() => { diff --git a/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx b/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx index a8810d11807..e12a376629e 100644 --- a/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx +++ b/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx @@ -10,10 +10,12 @@ import { } from '@grafana/data'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/heatmap.svg'; +import lightImage from '../images/light/heatmap.svg'; import { getDefaultOptions, getTransformerOptionPane } from '../spatial/optionsHelper'; import { addHeatmapCalculationOptions } from './editor/helper'; -import { HeatmapTransformerOptions, heatmapTransformer } from './heatmap'; +import { HeatmapTransformerOptions, getHeatmapTransformer } from './heatmap'; // Nothing defined in state const supplier = ( @@ -42,13 +44,18 @@ export const HeatmapTransformerEditor = (props: TransformerUIProps<HeatmapTransf ); }; -export const heatmapTransformRegistryItem: TransformerRegistryItem<HeatmapTransformerOptions> = { - id: heatmapTransformer.id, - editor: HeatmapTransformerEditor, - transformation: heatmapTransformer, - name: heatmapTransformer.name, - description: heatmapTransformer.description, - state: PluginState.alpha, - categories: new Set([TransformerCategory.CreateNewVisualization]), - help: getTransformationContent(heatmapTransformer.id).helperDocs, +export const getHeatmapTransformRegistryItem: () => TransformerRegistryItem<HeatmapTransformerOptions> = () => { + const heatmapTransformer = getHeatmapTransformer(); + return { + id: heatmapTransformer.id, + editor: HeatmapTransformerEditor, + transformation: heatmapTransformer, + name: heatmapTransformer.name, + description: heatmapTransformer.description, + state: PluginState.alpha, + categories: new Set([TransformerCategory.CreateNewVisualization]), + help: getTransformationContent(heatmapTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; }; diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index ed37d76651e..7968444302d 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -16,6 +16,7 @@ import { TimeRange, } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { ScaleDistribution, HeatmapCellLayout, @@ -30,10 +31,13 @@ export interface HeatmapTransformerOptions extends HeatmapCalculationOptions { keepOriginalData?: boolean; } -export const heatmapTransformer: SynchronousDataTransformerInfo<HeatmapTransformerOptions> = { +export const getHeatmapTransformer: () => SynchronousDataTransformerInfo<HeatmapTransformerOptions> = () => ({ id: DataTransformerID.heatmap, - name: 'Create heatmap', - description: 'Generate heatmap data from source data.', + name: t('transformers.get-heatmap-transformer.name.create-heatmap', 'Create heatmap'), + description: t( + 'transformers.get-heatmap-transformer.description.generate-heatmap-data-from-source', + 'Generate heatmap data from source data.' + ), defaultOptions: {}, isApplicable: (data) => { const { xField, yField, xs, ys } = findHeatmapFields(data); @@ -51,7 +55,7 @@ export const heatmapTransformer: SynchronousDataTransformerInfo<HeatmapTransform isApplicableDescription: 'The Heatmap transformation requires fields with Heatmap compatible data. No fields with Heatmap data could be found.', operator: (options, ctx) => (source) => - source.pipe(map((data) => heatmapTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getHeatmapTransformer().transformer(options, ctx)(data))), transformer: (options: HeatmapTransformerOptions) => { return (data: DataFrame[]) => { @@ -62,7 +66,7 @@ export const heatmapTransformer: SynchronousDataTransformerInfo<HeatmapTransform return [v]; }; }, -}; +}); function parseNumeric(v?: string | null) { return v === '+Inf' ? Infinity : v === '-Inf' ? -Infinity : +(v ?? 0); diff --git a/public/app/features/transformers/configFromQuery/ConfigFromQueryTransformerEditor.tsx b/public/app/features/transformers/configFromQuery/ConfigFromQueryTransformerEditor.tsx index cdecf142199..2a5c3b0999a 100644 --- a/public/app/features/transformers/configFromQuery/ConfigFromQueryTransformerEditor.tsx +++ b/public/app/features/transformers/configFromQuery/ConfigFromQueryTransformerEditor.tsx @@ -14,8 +14,10 @@ import { fieldMatchersUI, InlineField, InlineFieldRow, Select, useStyles2 } from import { getTransformationContent } from '../docs/getTransformationContent'; import { FieldToConfigMappingEditor } from '../fieldToConfigMapping/FieldToConfigMappingEditor'; +import darkImage from '../images/dark/configFromData.svg'; +import lightImage from '../images/light/configFromData.svg'; -import { configFromDataTransformer, ConfigFromQueryTransformOptions } from './configFromQuery'; +import { getConfigFromDataTransformer, ConfigFromQueryTransformOptions } from './configFromQuery'; export interface Props extends TransformerUIProps<ConfigFromQueryTransformOptions> {} @@ -98,16 +100,22 @@ export function ConfigFromQueryTransformerEditor({ input, onChange, options }: P ); } -export const configFromQueryTransformRegistryItem: TransformerRegistryItem<ConfigFromQueryTransformOptions> = { - id: configFromDataTransformer.id, - editor: ConfigFromQueryTransformerEditor, - transformation: configFromDataTransformer, - name: configFromDataTransformer.name, - description: configFromDataTransformer.description, - state: PluginState.beta, - categories: new Set([TransformerCategory.CalculateNewFields]), - help: getTransformationContent(configFromDataTransformer.id).helperDocs, -}; +export const getConfigFromQueryTransformRegistryItem: () => TransformerRegistryItem<ConfigFromQueryTransformOptions> = + () => { + const configFromDataTransformer = getConfigFromDataTransformer(); + return { + id: configFromDataTransformer.id, + editor: ConfigFromQueryTransformerEditor, + transformation: configFromDataTransformer, + name: configFromDataTransformer.name, + description: configFromDataTransformer.description, + state: PluginState.beta, + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(configFromDataTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; + }; const getStyles = (theme: GrafanaTheme2) => ({ matcherOptions: css({ diff --git a/public/app/features/transformers/configFromQuery/configFromQuery.ts b/public/app/features/transformers/configFromQuery/configFromQuery.ts index 44cd15c2117..4e8ae24b311 100644 --- a/public/app/features/transformers/configFromQuery/configFromQuery.ts +++ b/public/app/features/transformers/configFromQuery/configFromQuery.ts @@ -10,6 +10,7 @@ import { MatcherConfig, reduceField, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { evaluateFieldMappings, @@ -89,10 +90,13 @@ export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions, return output; } -export const configFromDataTransformer: DataTransformerInfo<ConfigFromQueryTransformOptions> = { +export const getConfigFromDataTransformer: () => DataTransformerInfo<ConfigFromQueryTransformOptions> = () => ({ id: DataTransformerID.configFromData, - name: 'Config from query results', - description: 'Set unit, min, max and more from data.', + name: t('transformers.get-config-from-data-transformer.name.config-from-query-results', 'Config from query results'), + description: t( + 'transformers.get-config-from-data-transformer.description.set-unit-min-max-and-more', + 'Set unit, min, max and more.' + ), defaultOptions: { configRefId: 'config', mappings: [], @@ -103,4 +107,4 @@ export const configFromDataTransformer: DataTransformerInfo<ConfigFromQueryTrans * be applied, just return the input series */ operator: (options) => (source) => source.pipe(map((data) => extractConfigFromQuery(options, data))), -}; +}); diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index f9dd0074977..a441149469d 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -581,7 +581,7 @@ This transformation goes in two steps. First you specify one or multiple fields | 2020-07-07 10:31:22 | **_server 3_** | 55 | OK | | 2020-07-07 09:30:57 | **_server 3_** | 62 | Rebooting | -All rows with the same value of Server ID are grouped together. +All rows with the same value of Server ID are grouped together. Optionally, you can add a count of how may values fall in the selected group. After choosing which field you want to group your data by, you can add various calculations on the other fields, and apply the calculation to each group of rows. For instance, we could want to calculate the average CPU temperature for each of those servers. So we can add the _mean_ calculation applied on the CPU Temperature field to get the following: @@ -591,6 +591,14 @@ After choosing which field you want to group your data by, you can add various c | server 2 | 88.6 | | server 3 | 59.6 | +If you had added the count stat to the group by transformation, there would be an extra column showing that the count of each server from above was 3. + +| Server ID | CPU Temperature (mean) | Server ID (count) | +| --------- | ---------------------- | ----------------- | +| server 1 | 82 | 3 | +| server 2 | 88.6 | 3 | +| server 3 | 59.6 | 3 | + And we can add more than one calculation. For instance: - For field Time, we can calculate the _Last_ value, to know when the last data point was received for each server diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx index 55566e530a1..64d1fffba80 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx @@ -27,6 +27,8 @@ import { getTemplateSrv, config as cfg } from '@grafana/runtime'; import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui'; import { getTransformationContent } from '../../docs/getTransformationContent'; +import darkImage from '../../images/dark/calculateField.svg'; +import lightImage from '../../images/light/calculateField.svg'; import { BinaryOperationOptionsEditor } from './BinaryOperationOptionsEditor'; import { CumulativeOptionsEditor } from './CumulativeOptionsEditor'; @@ -42,20 +44,6 @@ interface CalculateFieldTransformerEditorState { selected: string[]; } -const calculationModes = [ - { value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' }, - { value: CalculateFieldMode.UnaryOperation, label: 'Unary operation' }, - { value: CalculateFieldMode.ReduceRow, label: 'Reduce row' }, - { value: CalculateFieldMode.Index, label: 'Row index' }, -]; - -if (cfg.featureToggles.addFieldFromCalculationStatFunctions) { - calculationModes.push( - { value: CalculateFieldMode.CumulativeFunctions, label: 'Cumulative functions' }, - { value: CalculateFieldMode.WindowFunctions, label: 'Window functions' } - ); -} - const okTypes = new Set<FieldType>([FieldType.time, FieldType.number, FieldType.string, FieldType.boolean]); export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformerEditorProps) => { @@ -63,6 +51,44 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer const configuredOptions = options?.reduce?.include; const [state, setState] = useState<CalculateFieldTransformerEditorState>({ names: [], selected: [] }); + const calculationModes = [ + { + value: CalculateFieldMode.BinaryOperation, + label: t( + 'transformers.calculate-field-transformer-editor.calculation-modes.label.binary-operation', + 'Binary operation' + ), + }, + { + value: CalculateFieldMode.UnaryOperation, + label: t( + 'transformers.calculate-field-transformer-editor.calculation-modes.label.unary-operation', + 'Unary operation' + ), + }, + { + value: CalculateFieldMode.ReduceRow, + label: t('transformers.calculate-field-transformer-editor.calculation-modes.label.reduce-row', 'Reduce row'), + }, + { + value: CalculateFieldMode.Index, + label: t('transformers.calculate-field-transformer-editor.calculation-modes.label.row-index', 'Row index'), + }, + ]; + + if (cfg.featureToggles.addFieldFromCalculationStatFunctions) { + calculationModes.push( + { + value: CalculateFieldMode.CumulativeFunctions, + label: t('transformers.calculate-field-transformer-editor.label.cumulative-functions', 'Cumulative functions'), + }, + { + value: CalculateFieldMode.WindowFunctions, + label: t('transformers.calculate-field-transformer-editor.label.window-functions', 'Window functions'), + } + ); + } + useEffect(() => { const ctx = { interpolate: (v: string) => v }; const subscription = of(input) @@ -240,12 +266,21 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer ); }; -export const calculateFieldTransformRegistryItem: TransformerRegistryItem<CalculateFieldTransformerOptions> = { - id: DataTransformerID.calculateField, - editor: CalculateFieldTransformerEditor, - transformation: standardTransformers.calculateFieldTransformer, - name: standardTransformers.calculateFieldTransformer.name, - description: 'Use the row values to calculate a new field.', - categories: new Set([TransformerCategory.CalculateNewFields]), - help: getTransformationContent(DataTransformerID.calculateField).helperDocs, -}; +export const getCalculateFieldTransformRegistryItem: () => TransformerRegistryItem<CalculateFieldTransformerOptions> = + () => ({ + id: DataTransformerID.calculateField, + editor: CalculateFieldTransformerEditor, + transformation: standardTransformers.calculateFieldTransformer, + name: t( + 'transformers.get-calculate-field-transform-registry-item.name.add-field-from-calculation', + 'Add field from calculation' + ), + description: t( + 'transformers.get-calculate-field-transform-registry-item.description.values-calculate-field', + 'Use the row values to calculate a new field.' + ), + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(DataTransformerID.calculateField).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts deleted file mode 100644 index 0772bb9cd00..00000000000 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { - CalculateFieldTransformerEditor, - calculateFieldTransformRegistryItem, -} from './CalculateFieldTransformerEditor'; - -export { CalculateFieldTransformerEditor, calculateFieldTransformRegistryItem }; diff --git a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx index d9dc1cff6ec..e31ea83a565 100644 --- a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx @@ -13,15 +13,11 @@ import { t } from '@grafana/i18n'; import { InlineField, Input, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/concatenate.svg'; +import lightImage from '../images/light/concatenate.svg'; interface ConcatenateTransformerEditorProps extends TransformerUIProps<ConcatenateTransformerOptions> {} -const nameModes: Array<SelectableValue<ConcatenateFrameNameMode>> = [ - { value: ConcatenateFrameNameMode.FieldName, label: 'Copy frame name to field name' }, - { value: ConcatenateFrameNameMode.Label, label: 'Add a label with the frame name' }, - { value: ConcatenateFrameNameMode.Drop, label: 'Ignore the frame name' }, -]; - export class ConcatenateTransformerEditor extends PureComponent<ConcatenateTransformerEditorProps> { constructor(props: ConcatenateTransformerEditorProps) { super(props); @@ -50,6 +46,29 @@ export class ConcatenateTransformerEditor extends PureComponent<ConcatenateTrans render() { const { options } = this.props; + const nameModes: Array<SelectableValue<ConcatenateFrameNameMode>> = [ + { + value: ConcatenateFrameNameMode.FieldName, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.copy-frame-name-to-field', + 'Copy frame name to field name' + ), + }, + { + value: ConcatenateFrameNameMode.Label, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.label-frame', + 'Add a label with the frame name' + ), + }, + { + value: ConcatenateFrameNameMode.Drop, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.ignore-the-frame-name', + 'Ignore the frame name' + ), + }, + ]; const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName; @@ -82,13 +101,19 @@ export class ConcatenateTransformerEditor extends PureComponent<ConcatenateTrans } } -export const concatenateTransformRegistryItem: TransformerRegistryItem<ConcatenateTransformerOptions> = { - id: DataTransformerID.concatenate, - editor: ConcatenateTransformerEditor, - transformation: standardTransformers.concatenateTransformer, - name: standardTransformers.concatenateTransformer.name, - description: - 'Combine all fields into a single frame. Values will be appended with undefined values if not the same length.', - categories: new Set([TransformerCategory.Combine]), - help: getTransformationContent(DataTransformerID.concatenate).helperDocs, -}; +export const getConcatenateTransformRegistryItem: () => TransformerRegistryItem<ConcatenateTransformerOptions> = + () => ({ + id: DataTransformerID.concatenate, + editor: ConcatenateTransformerEditor, + transformation: standardTransformers.concatenateTransformer, + name: t('transformers.editors.concatenate-transformer-editor.name.concatenate-fields', 'Concatenate fields'), + description: t( + 'transformers.editors.concatenate-transformer-editor.description.combine-all-fields', + 'Combine all fields into a single frame.' + ), + categories: new Set([TransformerCategory.Combine]), + help: getTransformationContent(DataTransformerID.concatenate).helperDocs, + tags: new Set([t('transformers.editors.concatenate-transformer-editor.tags.combine', 'Combine')]), + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx index eb55c1d9d6f..d437144b415 100644 --- a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx @@ -13,12 +13,14 @@ import { getTimeZones, } from '@grafana/data'; import { ConvertFieldTypeOptions, ConvertFieldTypeTransformerOptions } from '@grafana/data/internal'; -import { t } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { Button, InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; import { allFieldTypeIconOptions, FieldNamePicker } from '@grafana/ui/internal'; -import { findField } from 'app/features/dimensions'; +import { findField } from 'app/features/dimensions/utils'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/convertFieldType.svg'; +import lightImage from '../images/light/convertFieldType.svg'; import { getTimezoneOptions } from '../utils'; import { EnumMappingEditor } from './EnumMappingEditor'; @@ -51,7 +53,7 @@ export const ConvertFieldTypeTransformerEditor = ({ const onSelectField = useCallback( (idx: number) => (value: string | undefined) => { - const conversions = options.conversions; + const conversions = [...options.conversions]; conversions[idx] = { ...conversions[idx], targetField: value ?? '', dateFormat: undefined }; onChange({ ...options, @@ -259,18 +261,27 @@ export const ConvertFieldTypeTransformerEditor = ({ 'Add a convert field type transformer' )} > - {'Convert field type'} + <Trans i18nKey="transformers.convert-field-type-transformer-editor.convert-field-type"> + Convert field type + </Trans> </Button> </> ); }; -export const convertFieldTypeTransformRegistryItem: TransformerRegistryItem<ConvertFieldTypeTransformerOptions> = { - id: DataTransformerID.convertFieldType, - editor: ConvertFieldTypeTransformerEditor, - transformation: standardTransformers.convertFieldTypeTransformer, - name: standardTransformers.convertFieldTypeTransformer.name, - description: standardTransformers.convertFieldTypeTransformer.description, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.convertFieldType).helperDocs, -}; +export const getConvertFieldTypeTransformRegistryItem: () => TransformerRegistryItem<ConvertFieldTypeTransformerOptions> = + () => ({ + id: DataTransformerID.convertFieldType, + editor: ConvertFieldTypeTransformerEditor, + transformation: standardTransformers.convertFieldTypeTransformer, + name: t('transformers.convert-field-type-transformer-editor.name.convert-field-type', 'Convert field type'), + description: t( + 'transformers.convert-field-type-transformer-editor.description.convert-to-specified-field-type', + 'Convert a field to a specified field type.' + ), + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.convertFieldType).helperDocs, + tags: new Set([t('transformers.convert-field-type-transformer-editor.tags.format-field', 'Format field')]), + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx b/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx index c68ac0dbe41..25cff1951b8 100644 --- a/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx @@ -17,6 +17,8 @@ import { getTemplateSrv } from '@grafana/runtime'; import { Input, FilterPill, InlineFieldRow, InlineField, InlineSwitch, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/filterFieldsByName.svg'; +import lightImage from '../images/light/filterFieldsByName.svg'; interface FilterByNameTransformerEditorProps extends TransformerUIProps<FilterFieldsByNameTransformerOptions> {} @@ -253,12 +255,18 @@ export class FilterByNameTransformerEditor extends React.PureComponent< } } -export const filterFieldsByNameTransformRegistryItem: TransformerRegistryItem<FilterFieldsByNameTransformerOptions> = { - id: DataTransformerID.filterFieldsByName, - editor: FilterByNameTransformerEditor, - transformation: standardTransformers.filterFieldsByNameTransformer, - name: standardTransformers.filterFieldsByNameTransformer.name, - description: 'Removes part of the query results using a regex pattern. The pattern can be inclusive or exclusive.', - categories: new Set([TransformerCategory.Filter]), - help: getTransformationContent(DataTransformerID.filterFieldsByName).helperDocs, -}; +export const getFilterFieldsByNameTransformRegistryItem: () => TransformerRegistryItem<FilterFieldsByNameTransformerOptions> = + () => ({ + id: DataTransformerID.filterFieldsByName, + editor: FilterByNameTransformerEditor, + transformation: standardTransformers.filterFieldsByNameTransformer, + name: t('transformers.filter-by-name-transformer-editor.name.filter-fields-by-name', 'Filter fields by name'), + description: t( + 'transformers.filter-by-name-transformer-editor.description.remove-part-query-results-regex-pattern', + 'Remove parts of the query results using a regex pattern.' + ), + categories: new Set([TransformerCategory.Filter]), + help: getTransformationContent(DataTransformerID.filterFieldsByName).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx b/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx index 05b9e196b83..d4ec966661c 100644 --- a/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx @@ -7,9 +7,12 @@ import { FrameMatcherID, } from '@grafana/data'; import { FilterFramesByRefIdTransformerOptions } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { FrameMultiSelectionEditor } from 'app/plugins/panel/geomap/editor/FrameSelectionEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/filterByRefId.svg'; +import lightImage from '../images/light/filterByRefId.svg'; export const FilterByRefIdTransformerEditor = (props: TransformerUIProps<FilterFramesByRefIdTransformerOptions>) => { return ( @@ -29,14 +32,18 @@ export const FilterByRefIdTransformerEditor = (props: TransformerUIProps<FilterF ); }; -export const filterFramesByRefIdTransformRegistryItem: TransformerRegistryItem<FilterFramesByRefIdTransformerOptions> = - { +export const getFilterFramesByRefIdTransformRegistryItem: () => TransformerRegistryItem<FilterFramesByRefIdTransformerOptions> = + () => ({ id: DataTransformerID.filterByRefId, editor: FilterByRefIdTransformerEditor, transformation: standardTransformers.filterFramesByRefIdTransformer, - name: standardTransformers.filterFramesByRefIdTransformer.name, - description: - 'Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel.', + name: t('transformers.filter-by-ref-id-transformer-editor.name.filter-data-by-query', 'Filter data by query'), + description: t( + 'transformers.filter-by-ref-id-transformer-editor.description.filter-data-by-query-useful-sharing-results', + 'Remove rows from the data based on origin query' + ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.filterByRefId).helperDocs, - }; + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx b/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx index 5bfaaf40d71..9bee2e9bd29 100644 --- a/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx @@ -18,23 +18,32 @@ import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/internal'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; -const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { - settings: { - width: 30, - filter: (f) => f.type === FieldType.string, - placeholderText: 'Select text field', - noFieldsMessage: 'No text fields found', - }, - name: '', - id: '', - editor: () => null, -}; +import darkImage from '../images/dark/formatString.svg'; +import lightImage from '../images/light/formatString.svg'; function FormatStringTransfomerEditor({ input, options, onChange, }: TransformerUIProps<FormatStringTransformerOptions>) { + const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { + settings: { + width: 30, + filter: (f) => f.type === FieldType.string, + placeholderText: t( + 'transformers.format-string-transfomer-editor.field-name-picker-settings.placeholderText.select-text-field', + 'Select text field' + ), + noFieldsMessage: t( + 'transformers.format-string-transfomer-editor.field-name-picker-settings.noFieldsMessage.no-text-fields-found', + 'No text fields found' + ), + }, + name: '', + id: '', + editor: () => null, + }; + const onSelectField = useCallback( (value: string | undefined) => { const val = value ?? ''; @@ -113,12 +122,18 @@ function FormatStringTransfomerEditor({ ); } -export const formatStringTransformerRegistryItem: TransformerRegistryItem<FormatStringTransformerOptions> = { - id: DataTransformerID.formatString, - editor: FormatStringTransfomerEditor, - transformation: standardTransformers.formatStringTransformer, - name: standardTransformers.formatStringTransformer.name, - state: PluginState.beta, - description: standardTransformers.formatStringTransformer.description, - categories: new Set([TransformerCategory.Reformat]), -}; +export const getFormatStringTransformerRegistryItem: () => TransformerRegistryItem<FormatStringTransformerOptions> = + () => ({ + id: DataTransformerID.formatString, + editor: FormatStringTransfomerEditor, + transformation: standardTransformers.formatStringTransformer, + name: t('transformers.format-string-transformer-editor.name.format-string', 'Format string'), + state: PluginState.beta, + description: t( + 'transformers.format-string-transformer-editor.description.manipulate-string-fields-formatting', + 'Manipulate string fields formatting.' + ), + categories: new Set([TransformerCategory.Reformat]), + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx index 82304ce8870..01baaae10cd 100644 --- a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx @@ -14,6 +14,8 @@ import { Trans, t } from '@grafana/i18n'; import { Select, InlineFieldRow, InlineField, Input, TextLink } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/formatTime.svg'; +import lightImage from '../images/light/formatTime.svg'; import { getTimezoneOptions } from '../utils'; export function FormatTimeTransfomerEditor({ @@ -117,12 +119,18 @@ export function FormatTimeTransfomerEditor({ ); } -export const formatTimeTransformerRegistryItem: TransformerRegistryItem<FormatTimeTransformerOptions> = { - id: DataTransformerID.formatTime, - editor: FormatTimeTransfomerEditor, - transformation: standardTransformers.formatTimeTransformer, - name: standardTransformers.formatTimeTransformer.name, - state: PluginState.alpha, - description: standardTransformers.formatTimeTransformer.description, - help: getTransformationContent(DataTransformerID.formatTime).helperDocs, -}; +export const getFormatTimeTransformerRegistryItem: () => TransformerRegistryItem<FormatTimeTransformerOptions> = + () => ({ + id: DataTransformerID.formatTime, + editor: FormatTimeTransfomerEditor, + transformation: standardTransformers.formatTimeTransformer, + name: t('transformers.format-time-transformer-editor.name.format-time', 'Format time'), + state: PluginState.alpha, + description: t( + 'transformers.format-time-transformer-editor.description.set-based-on-time', + 'Set the output format of a time field' + ), + help: getTransformationContent(DataTransformerID.formatTime).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx index 91c01232de2..ebd2a55661d 100644 --- a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx @@ -16,6 +16,8 @@ import { t } from '@grafana/i18n'; import { useTheme2, Select, StatsPicker, InlineField, Stack, Alert } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/groupBy.svg'; +import lightImage from '../images/light/groupBy.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; interface FieldProps { @@ -24,11 +26,7 @@ interface FieldProps { onConfigChange: (config: GroupByFieldOptions) => void; } -export const GroupByTransformerEditor = ({ - input, - options, - onChange, -}: TransformerUIProps<GroupByTransformerOptions>) => { +const GroupByTransformerEditor = ({ input, options, onChange }: TransformerUIProps<GroupByTransformerOptions>) => { const fieldNames = useAllFieldNamesFromDataFrames(input, true); const onConfigChange = useCallback( @@ -86,12 +84,7 @@ export const GroupByTransformerEditor = ({ ); }; -const options = [ - { label: 'Group by', value: GroupByOperationID.groupBy }, - { label: 'Calculate', value: GroupByOperationID.aggregate }, -]; - -export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { +const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { const theme = useTheme2(); const styles = getStyles(theme); @@ -106,6 +99,17 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: [config, onConfigChange] ); + const options = [ + { + label: t('transformers.group-by-field-configuration.options.label.group-by', 'Group by'), + value: GroupByOperationID.groupBy, + }, + { + label: t('transformers.group-by-field-configuration.options.label.calculate', 'Calculate'), + value: GroupByOperationID.aggregate, + }, + ]; + return ( <InlineField className={styles.label} label={fieldName} grow shrink> <Stack gap={0.5} direction="row"> @@ -119,7 +123,7 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: /> </div> - {config?.operation === GroupByOperationID.aggregate && ( + {config?.operation && ( <StatsPicker className={styles.aggregations} placeholder={t('transformers.group-by-field-configuration.placeholder-select-stats', 'Select stats')} @@ -128,6 +132,9 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: onChange={(stats) => { onConfigChange({ ...config, aggregations: stats as ReducerID[] }); }} + filterOptions={(option) => + config?.operation === GroupByOperationID.groupBy ? option.id === ReducerID.count : true + } /> )} </Stack> @@ -153,16 +160,21 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export const groupByTransformRegistryItem: TransformerRegistryItem<GroupByTransformerOptions> = { +export const getGroupByTransformRegistryItem: () => TransformerRegistryItem<GroupByTransformerOptions> = () => ({ id: DataTransformerID.groupBy, editor: GroupByTransformerEditor, transformation: standardTransformers.groupByTransformer, - name: standardTransformers.groupByTransformer.name, - description: standardTransformers.groupByTransformer.description, + name: t('transformers.group-by-transformer-editor.name.group-by', 'Group by'), + description: t( + 'transformers.group-by-transformer-editor.description.group-series-by-field-calculate-stats', + 'Group data by a field value and create aggregate data.' + ), categories: new Set([ TransformerCategory.Combine, TransformerCategory.CalculateNewFields, TransformerCategory.Reformat, ]), help: getTransformationContent(DataTransformerID.groupBy).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx b/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx index f0c0b01a007..bdeebc3f8e7 100644 --- a/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx @@ -22,6 +22,8 @@ import { import { t } from '@grafana/i18n'; import { useTheme2, Select, StatsPicker, InlineField, Field, Switch, Alert, Stack } from '@grafana/ui'; +import darkImage from '../images/dark/groupToNestedTable.svg'; +import lightImage from '../images/light/groupToNestedTable.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; interface FieldProps { @@ -123,11 +125,6 @@ export const GroupToNestedTableTransformerEditor = ({ ); }; -const options = [ - { label: 'Group by', value: GroupByOperationID.groupBy }, - { label: 'Calculate', value: GroupByOperationID.aggregate }, -]; - export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { const theme = useTheme2(); const styles = getStyles(theme); @@ -141,6 +138,17 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: [config, onConfigChange] ); + const options = [ + { + label: t('transformers.group-by-field-configuration.options.label.group-by', 'Group by'), + value: GroupByOperationID.groupBy, + }, + { + label: t('transformers.group-by-field-configuration.options.label.calculate', 'Calculate'), + value: GroupByOperationID.aggregate, + }, + ]; + return ( <InlineField className={styles.label} label={fieldName} grow shrink> <Stack gap={0.5} direction="row" wrap={false}> @@ -187,16 +195,25 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export const groupToNestedTableTransformRegistryItem: TransformerRegistryItem<GroupByTransformerOptions> = { - id: DataTransformerID.groupToNestedTable, - editor: GroupToNestedTableTransformerEditor, - transformation: standardTransformers.groupToNestedTable, - name: standardTransformers.groupToNestedTable.name, - description: standardTransformers.groupToNestedTable.description, - categories: new Set([ - TransformerCategory.Combine, - TransformerCategory.CalculateNewFields, - TransformerCategory.Reformat, - ]), - state: PluginState.beta, -}; +export const getGroupToNestedTableTransformRegistryItem: () => TransformerRegistryItem<GroupByTransformerOptions> = + () => ({ + id: DataTransformerID.groupToNestedTable, + editor: GroupToNestedTableTransformerEditor, + transformation: standardTransformers.groupToNestedTable, + name: t( + 'transformers.group-to-nested-table-transformer-editor.name.group-to-nested-tables', + 'Group to nested tables' + ), + description: t( + 'transformers.group-to-nested-table-transformer-editor.description.group-by-field-value', + 'Group data by a field value and create nested tables with the grouped data.' + ), + categories: new Set([ + TransformerCategory.Combine, + TransformerCategory.CalculateNewFields, + TransformerCategory.Reformat, + ]), + state: PluginState.beta, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx index 60cae734a2a..6eb793e082a 100644 --- a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx @@ -15,6 +15,8 @@ import { getTemplateSrv } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/groupingToMatrix.svg'; +import lightImage from '../images/light/groupingToMatrix.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; export const GroupingToMatrixTransformerEditor = ({ @@ -148,12 +150,18 @@ export const GroupingToMatrixTransformerEditor = ({ ); }; -export const groupingToMatrixTransformRegistryItem: TransformerRegistryItem<GroupingToMatrixTransformerOptions> = { - id: DataTransformerID.groupingToMatrix, - editor: GroupingToMatrixTransformerEditor, - transformation: standardTransformers.groupingToMatrixTransformer, - name: standardTransformers.groupingToMatrixTransformer.name, - description: 'Takes a three fields combination and produces a Matrix.', - categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.groupingToMatrix).helperDocs, -}; +export const getGroupingToMatrixTransformRegistryItem: () => TransformerRegistryItem<GroupingToMatrixTransformerOptions> = + () => ({ + id: DataTransformerID.groupingToMatrix, + editor: GroupingToMatrixTransformerEditor, + transformation: standardTransformers.groupingToMatrixTransformer, + name: t('transformers.grouping-to-matrix-transformer-editor.name.grouping-to-matrix', 'Grouping to matrix'), + description: t( + 'transformers.grouping-to-matrix-transformer-editor.description.summarize-and-reorganize-data', + 'Summarize and reorganize data based on three fields.' + ), + categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.groupingToMatrix).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx index c70c8e9244c..c2629e2f145 100644 --- a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx +++ b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx @@ -12,6 +12,8 @@ import { t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/histogram.svg'; +import lightImage from '../images/light/histogram.svg'; import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput'; import { getVariableSuggestions, numberOrVariableValidator } from '../utils'; @@ -136,12 +138,17 @@ export const HistogramTransformerEditor = ({ ); }; -export const histogramTransformRegistryItem: TransformerRegistryItem<HistogramTransformerInputs> = { +export const getHistogramTransformRegistryItem: () => TransformerRegistryItem<HistogramTransformerInputs> = () => ({ id: DataTransformerID.histogram, editor: HistogramTransformerEditor, transformation: standardTransformers.histogramTransformer, - name: standardTransformers.histogramTransformer.name, - description: standardTransformers.histogramTransformer.description, + name: t('transformers.histogram-transformer-editor.name.histogram', 'Histogram'), + description: t( + 'transformers.histogram-transformer-editor.description.calculate-histogram-from-input-data', + 'Calculate a histogram from input data.' + ), categories: new Set([TransformerCategory.CreateNewVisualization]), help: getTransformationContent(DataTransformerID.histogram).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx index 244cea37072..ba83c09f2a2 100644 --- a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx @@ -15,31 +15,40 @@ import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; - -const modes = [ - { - value: JoinMode.outer, - label: 'OUTER (TIME SERIES)', - description: - 'Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series.', - }, - { - value: JoinMode.outerTabular, - label: 'OUTER (TABULAR)', - description: - 'Join on a field value with duplicated values. Non performant outer join best used for tabular(SQL like) data.', - }, - { - value: JoinMode.inner, - label: 'INNER', - description: 'Combine data from two tables whenever there are matching values in a fields common to both tables.', - }, -]; +import darkImage from '../images/dark/joinByField.svg'; +import lightImage from '../images/light/joinByField.svg'; export function SeriesToFieldsTransformerEditor({ input, options, onChange }: TransformerUIProps<JoinByFieldOptions>) { const names = useFieldDisplayNames(input); const fieldNames = useSelectOptions(names); + const modes = [ + { + value: JoinMode.outer, + label: t('transformers.series-to-fields-transformer-editor.modes.label.outer-time-series', 'Outer (time series)'), + description: t( + 'transformers.series-to-fields-transformer-editor.modes.description.keep-all-rows', + 'Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series.' + ), + }, + { + value: JoinMode.outerTabular, + label: t('transformers.series-to-fields-transformer-editor.modes.label.outer-tabular', 'Outer (tabular)'), + description: t( + 'transformers.series-to-fields-transformer-editor.modes.description.join-on-a-field', + 'Join on a field value with duplicated values. Non performant outer join best used for tabular(SQL like) data.' + ), + }, + { + value: JoinMode.inner, + label: t('transformers.series-to-fields-transformer-editor.modes.label.inner', 'Inner'), + description: t( + 'transformers.series-to-fields-transformer-editor.modes.description.combine-data-from-two-tables', + 'Combine data from two tables whenever there are matching values in a fields common to both tables.' + ), + }, + ]; + const variables = getTemplateSrv() .getVariables() .map((v) => { @@ -98,13 +107,18 @@ export function SeriesToFieldsTransformerEditor({ input, options, onChange }: Tr ); } -export const joinByFieldTransformerRegistryItem: TransformerRegistryItem<JoinByFieldOptions> = { +export const getJoinByFieldTransformerRegistryItem: () => TransformerRegistryItem<JoinByFieldOptions> = () => ({ id: DataTransformerID.joinByField, aliasIds: [DataTransformerID.seriesToColumns], editor: SeriesToFieldsTransformerEditor, transformation: standardTransformers.joinByFieldTransformer, - name: standardTransformers.joinByFieldTransformer.name, - description: standardTransformers.joinByFieldTransformer.description, + name: t('transformers.join-by-field-transformer-editor.name.join-by-field', 'Join by field'), + description: t( + 'transformers.join-by-field-transformer-editor.description.combine-rows-from-2-tables', + 'Combine rows from 2+ tables, based on a related field.' + ), categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.joinByField).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx index bf1eb69bf72..42944c4fb15 100644 --- a/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx @@ -13,11 +13,8 @@ import { t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, RadioButtonGroup, Select, FilterPill, Stack } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; - -const modes: Array<SelectableValue<LabelsToFieldsMode>> = [ - { value: LabelsToFieldsMode.Columns, label: 'Columns' }, - { value: LabelsToFieldsMode.Rows, label: 'Rows' }, -]; +import darkImage from '../images/dark/labelsToFields.svg'; +import lightImage from '../images/light/labelsToFields.svg'; export const LabelsAsFieldsTransformerEditor = ({ input, @@ -26,6 +23,17 @@ export const LabelsAsFieldsTransformerEditor = ({ }: TransformerUIProps<LabelsToFieldsOptions>) => { const labelWidth = 20; + const modes: Array<SelectableValue<LabelsToFieldsMode>> = [ + { + value: LabelsToFieldsMode.Columns, + label: t('transformers.labels-as-fields-transformer-editor.modes.label.columns', 'Columns'), + }, + { + value: LabelsToFieldsMode.Rows, + label: t('transformers.labels-as-fields-transformer-editor.modes.label.rows', 'Rows'), + }, + ]; + const { labelNames, selected } = useMemo(() => { let labelNames: Array<SelectableValue<string>> = []; let uniqueLabels: Record<string, boolean> = {}; @@ -133,13 +141,17 @@ export const LabelsAsFieldsTransformerEditor = ({ ); }; -export const labelsToFieldsTransformerRegistryItem: TransformerRegistryItem<LabelsToFieldsOptions> = { +export const getLabelsToFieldsTransformerRegistryItem: () => TransformerRegistryItem<LabelsToFieldsOptions> = () => ({ id: DataTransformerID.labelsToFields, editor: LabelsAsFieldsTransformerEditor, transformation: standardTransformers.labelsToFieldsTransformer, - name: standardTransformers.labelsToFieldsTransformer.name, - description: `Groups series by time and return labels or tags as fields. - Useful for showing time series with labels in a table where each label key becomes a separate column.`, + name: t('transformers.labels-to-fields-transformer-editor.name.labels-to-fields', 'Labels to fields'), + description: t( + 'transformers.labels-to-fields-transformer-editor.description.groups-series-time-return-labels-tags-fields', + 'Group series by time and return labels or tags as fields.' + ), categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.labelsToFields).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/LimitTransformerEditor.tsx b/public/app/features/transformers/editors/LimitTransformerEditor.tsx index 65313152a49..6c2cfc7dbb9 100644 --- a/public/app/features/transformers/editors/LimitTransformerEditor.tsx +++ b/public/app/features/transformers/editors/LimitTransformerEditor.tsx @@ -12,6 +12,8 @@ import { t } from '@grafana/i18n'; import { InlineFieldRow } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/limit.svg'; +import lightImage from '../images/light/limit.svg'; import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput'; import { getVariableSuggestions, numberOrVariableValidator } from '../utils'; @@ -45,12 +47,17 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps ); }; -export const limitTransformRegistryItem: TransformerRegistryItem<LimitTransformerOptions> = { +export const getLimitTransformRegistryItem: () => TransformerRegistryItem<LimitTransformerOptions> = () => ({ id: DataTransformerID.limit, editor: LimitTransformerEditor, transformation: standardTransformers.limitTransformer, - name: standardTransformers.limitTransformer.name, - description: `Limit the number of items displayed.`, + name: t('transformers.limit-transformer-editor.name.limit', 'Limit'), + description: t( + 'transformers.limit-transformer-editor.description.limit-number-items-displayed', + 'Limit the number of items displayed.' + ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.limit).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/MergeTransformerEditor.tsx b/public/app/features/transformers/editors/MergeTransformerEditor.tsx index 99666ad801e..de7a53e9d88 100644 --- a/public/app/features/transformers/editors/MergeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/MergeTransformerEditor.tsx @@ -6,10 +6,12 @@ import { TransformerCategory, } from '@grafana/data'; import { MergeTransformerOptions } from '@grafana/data/internal'; -import { Trans } from '@grafana/i18n'; +import { Trans, t } from '@grafana/i18n'; import { FieldValidationMessage } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/merge.svg'; +import lightImage from '../images/light/merge.svg'; export const MergeTransformerEditor = ({ input, options, onChange }: TransformerUIProps<MergeTransformerOptions>) => { if (input.length <= 1) { @@ -25,13 +27,17 @@ export const MergeTransformerEditor = ({ input, options, onChange }: Transformer return null; }; -export const mergeTransformerRegistryItem: TransformerRegistryItem<MergeTransformerOptions> = { +export const getMergeTransformerRegistryItem: () => TransformerRegistryItem<MergeTransformerOptions> = () => ({ id: DataTransformerID.merge, editor: MergeTransformerEditor, transformation: standardTransformers.mergeTransformer, - name: standardTransformers.mergeTransformer.name, - description: `Merge many series/tables and return a single table where mergeable values will be combined into the same row. - Useful for showing multiple series, tables or a combination of both visualized in a table.`, + name: t('transformers.merge-transformer-editor.name.merge', 'Merge series/tables'), + description: t( + 'transformers.merge-transformer-editor.description.merge-multiple-series', + 'Merge multiple series. Values will be combined into one row.' + ), categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.merge).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx index 48c18bdc493..cebdf95c955 100644 --- a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx @@ -36,6 +36,8 @@ import { import { createFieldsOrdererAuto } from '../../../../../packages/grafana-data/src/transformations/transformers/order'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/organize.svg'; +import lightImage from '../images/light/organize.svg'; import { getAllFieldNamesFromDataFrames, getDistinctLabels, useAllFieldNamesFromDataFrames } from '../utils'; interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {} @@ -495,13 +497,18 @@ const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string return fieldNames.sort(comparer); }; -export const organizeFieldsTransformRegistryItem: TransformerRegistryItem<OrganizeFieldsTransformerOptions> = { - id: DataTransformerID.organize, - editor: OrganizeFieldsTransformerEditor, - transformation: standardTransformers.organizeFieldsTransformer, - name: standardTransformers.organizeFieldsTransformer.name, - description: - "Allows the user to re-order, hide, or rename fields / columns. Useful when data source doesn't allow overrides for visualizing data.", - categories: new Set([TransformerCategory.ReorderAndRename]), - help: getTransformationContent(DataTransformerID.organize).helperDocs, -}; +export const getOrganizeFieldsTransformRegistryItem: () => TransformerRegistryItem<OrganizeFieldsTransformerOptions> = + () => ({ + id: DataTransformerID.organize, + editor: OrganizeFieldsTransformerEditor, + transformation: standardTransformers.organizeFieldsTransformer, + name: t('transformers.organize-fields-transformer-editor.name.organize-fields', 'Organize fields by name'), + description: t( + 'transformers.organize-fields-transformer-editor.description.reorder-hide-or-rename-fields', + 'Re-order, hide, or rename fields.' + ), + categories: new Set([TransformerCategory.ReorderAndRename]), + help: getTransformationContent(DataTransformerID.organize).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/ReduceTransformerEditor.tsx b/public/app/features/transformers/editors/ReduceTransformerEditor.tsx index fe69d0a474d..71ae8a6a08c 100644 --- a/public/app/features/transformers/editors/ReduceTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ReduceTransformerEditor.tsx @@ -15,6 +15,8 @@ import { t } from '@grafana/i18n'; import { InlineField, Select, StatsPicker, InlineSwitch } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/reduce.svg'; +import lightImage from '../images/light/reduce.svg'; // TODO: Minimal implementation, needs some <3 export const ReduceTransformerEditor = ({ options, onChange }: TransformerUIProps<ReduceTransformerOptions>) => { @@ -117,12 +119,17 @@ export const ReduceTransformerEditor = ({ options, onChange }: TransformerUIProp ); }; -export const reduceTransformRegistryItem: TransformerRegistryItem<ReduceTransformerOptions> = { +export const getReduceTransformRegistryItem: () => TransformerRegistryItem<ReduceTransformerOptions> = () => ({ id: DataTransformerID.reduce, editor: ReduceTransformerEditor, transformation: standardTransformers.reduceTransformer, - name: standardTransformers.reduceTransformer.name, - description: standardTransformers.reduceTransformer.description, + name: t('transformers.reduce-transformer-editor.name.reduce', 'Reduce'), + description: t( + 'transformers.reduce-transformer-editor.description.reduce-to-single-value', + 'Reduce all rows or data points to a single value (ex. max, mean).' + ), categories: new Set([TransformerCategory.CalculateNewFields]), help: getTransformationContent(DataTransformerID.reduce).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/RenameByRegexTransformer.tsx b/public/app/features/transformers/editors/RenameByRegexTransformer.tsx index c68bdf998f9..7955b83418c 100644 --- a/public/app/features/transformers/editors/RenameByRegexTransformer.tsx +++ b/public/app/features/transformers/editors/RenameByRegexTransformer.tsx @@ -13,6 +13,8 @@ import { t } from '@grafana/i18n'; import { InlineField, Input } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/renameByRegex.svg'; +import lightImage from '../images/light/renameByRegex.svg'; interface RenameByRegexTransformerEditorProps extends TransformerUIProps<RenameByRegexTransformerOptions> {} @@ -120,12 +122,18 @@ export class RenameByRegexTransformerEditor extends React.PureComponent< } } -export const renameByRegexTransformRegistryItem: TransformerRegistryItem<RenameByRegexTransformerOptions> = { - id: DataTransformerID.renameByRegex, - editor: RenameByRegexTransformerEditor, - transformation: standardTransformers.renameByRegexTransformer, - name: standardTransformers.renameByRegexTransformer.name, - description: 'Renames part of the query result by using regular expression with placeholders.', - categories: new Set([TransformerCategory.ReorderAndRename]), - help: getTransformationContent(DataTransformerID.renameByRegex).helperDocs, -}; +export const getRenameByRegexTransformRegistryItem: () => TransformerRegistryItem<RenameByRegexTransformerOptions> = + () => ({ + id: DataTransformerID.renameByRegex, + editor: RenameByRegexTransformerEditor, + transformation: standardTransformers.renameByRegexTransformer, + name: t('transformers.rename-by-regex-transformer.name.rename-fields-by-regex', 'Rename fields by regex'), + description: t( + 'transformers.rename-by-regex-transformer.description.rename-parts-using-regex', + 'Rename parts of the query results using a regular expression and replacement pattern.' + ), + categories: new Set([TransformerCategory.ReorderAndRename]), + help: getTransformationContent(DataTransformerID.renameByRegex).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx b/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx index 8b3b66e64ed..154761870dc 100644 --- a/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx @@ -6,8 +6,11 @@ import { TransformerCategory, } from '@grafana/data'; import { SeriesToRowsTransformerOptions } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/seriesToRows.svg'; +import lightImage from '../images/light/seriesToRows.svg'; export const SeriesToRowsTransformerEditor = ({ input, @@ -17,13 +20,18 @@ export const SeriesToRowsTransformerEditor = ({ return null; }; -export const seriesToRowsTransformerRegistryItem: TransformerRegistryItem<SeriesToRowsTransformerOptions> = { - id: DataTransformerID.seriesToRows, - editor: SeriesToRowsTransformerEditor, - transformation: standardTransformers.seriesToRowsTransformer, - name: standardTransformers.seriesToRowsTransformer.name, - description: `Merge many series and return a single series with time, metric and value as columns. - Useful for showing multiple time series visualized in a table.`, - categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.seriesToRows).helperDocs, -}; +export const getSeriesToRowsTransformerRegistryItem: () => TransformerRegistryItem<SeriesToRowsTransformerOptions> = + () => ({ + id: DataTransformerID.seriesToRows, + editor: SeriesToRowsTransformerEditor, + transformation: standardTransformers.seriesToRowsTransformer, + name: t('transformers.series-to-rows-transformer-editor.name.series-to-rows', 'Series to rows'), + description: t( + 'transformers.series-to-rows-transformer-editor.description.merge-multiple-series', + 'Merge multiple series. Return time, metric and values as a row.' + ), + categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.seriesToRows).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }); diff --git a/public/app/features/transformers/editors/SortByTransformerEditor.tsx b/public/app/features/transformers/editors/SortByTransformerEditor.tsx index b3cd482e83d..c2382e6e27c 100644 --- a/public/app/features/transformers/editors/SortByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SortByTransformerEditor.tsx @@ -13,6 +13,8 @@ import { getTemplateSrv } from '@grafana/runtime'; import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/sortBy.svg'; +import lightImage from '../images/light/sortBy.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; export const SortByTransformerEditor = ({ input, options, onChange }: TransformerUIProps<SortByTransformerOptions>) => { @@ -64,12 +66,14 @@ export const SortByTransformerEditor = ({ input, options, onChange }: Transforme ); }; -export const sortByTransformRegistryItem: TransformerRegistryItem<SortByTransformerOptions> = { +export const getSortByTransformRegistryItem: () => TransformerRegistryItem<SortByTransformerOptions> = () => ({ id: DataTransformerID.sortBy, editor: SortByTransformerEditor, transformation: standardTransformers.sortByTransformer, - name: standardTransformers.sortByTransformer.name, - description: standardTransformers.sortByTransformer.description, + name: t('transformers.sort-by-transformer-editor.name.sort-by', 'Sort by'), + description: t('transformers.sort-by-transformer-editor.description.sort-fields', 'Sort fields in a frame.'), categories: new Set([TransformerCategory.ReorderAndRename]), help: getTransformationContent(DataTransformerID.sortBy).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/editors/TransposeTransformerEditor.tsx b/public/app/features/transformers/editors/TransposeTransformerEditor.tsx index 67af215029b..baeee2461e5 100644 --- a/public/app/features/transformers/editors/TransposeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/TransposeTransformerEditor.tsx @@ -9,7 +9,10 @@ import { TransposeTransformerOptions } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, Input } from '@grafana/ui'; -export const TransposeTransfomerEditor = ({ options, onChange }: TransformerUIProps<TransposeTransformerOptions>) => { +import darkImage from '../images/dark/transpose.svg'; +import lightImage from '../images/light/transpose.svg'; + +export const TransposeTransformerEditor = ({ options, onChange }: TransformerUIProps<TransposeTransformerOptions>) => { return ( <> <InlineFieldRow> @@ -43,11 +46,21 @@ export const TransposeTransfomerEditor = ({ options, onChange }: TransformerUIPr ); }; -export const transposeTransformerRegistryItem: TransformerRegistryItem<TransposeTransformerOptions> = { +export const getTransposeTransformerRegistryItem: () => TransformerRegistryItem<TransposeTransformerOptions> = () => ({ id: DataTransformerID.transpose, - editor: TransposeTransfomerEditor, + editor: TransposeTransformerEditor, transformation: standardTransformers.transposeTransformer, - name: standardTransformers.transposeTransformer.name, - description: standardTransformers.transposeTransformer.description, + name: t('transformers.transpose-transformer-editor.name.transpose', 'Transpose'), + description: t( + 'transformers.transpose-transformer-editor.description.transpose-data-frame', + 'Transpose the data frame.' + ), categories: new Set([TransformerCategory.Reformat]), -}; + tags: new Set([ + t('transformers.transpose-transformer-editor.tags.pivot', 'Pivot'), + t('transformers.transpose-transformer-editor.tags.translate', 'Translate'), + t('transformers.transpose-transformer-editor.tags.transform', 'Transform'), + ]), + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx index d49720810e0..f4d1773b8f2 100644 --- a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx @@ -14,27 +14,32 @@ import { InlineField, InlineFieldRow, Select, InlineSwitch, Input, Combobox, Com import { FieldNamePicker } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/extractFields.svg'; +import lightImage from '../images/light/extractFields.svg'; import { JSONPathEditor } from './components/JSONPathEditor'; import { extractFieldsTransformer } from './extractFields'; import { fieldExtractors } from './fieldExtractors'; import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types'; -const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { - settings: { - width: 30, - placeholderText: 'Select field', - }, - name: '', - id: '', - editor: () => null, -}; - export const extractFieldsTransformerEditor = ({ input, options = { delimiter: ',' }, onChange, }: TransformerUIProps<ExtractFieldsOptions>) => { + const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { + settings: { + width: 30, + placeholderText: t( + 'transformers.extract-fields-transformer-editor.field-name-picker-settings.placeholderText.select-field', + 'Select field' + ), + }, + name: '', + id: '', + editor: () => null, + }; + const onPickSourceField = (source?: string) => { onChange({ ...options, @@ -177,12 +182,17 @@ export const extractFieldsTransformerEditor = ({ ); }; -export const extractFieldsTransformRegistryItem: TransformerRegistryItem<ExtractFieldsOptions> = { +export const getExtractFieldsTransformRegistryItem: () => TransformerRegistryItem<ExtractFieldsOptions> = () => ({ id: DataTransformerID.extractFields, editor: extractFieldsTransformerEditor, transformation: extractFieldsTransformer, - name: extractFieldsTransformer.name, - description: `Parse fields from content (JSON, labels, etc).`, + name: t('transformers.extract-fields-transformer-editor.name.extract-fields', 'Extract fields'), + description: t( + 'transformers.extract-fields-transformer-editor.description.parse-fields-from-content', + 'Parse fields from content (JSON, labels, etc).' + ), categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.extractFields).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/extractFields/extractFields.ts b/public/app/features/transformers/extractFields/extractFields.ts index 70ab7f5cfa1..c50c575939a 100644 --- a/public/app/features/transformers/extractFields/extractFields.ts +++ b/public/app/features/transformers/extractFields/extractFields.ts @@ -11,7 +11,7 @@ import { SynchronousDataTransformerInfo, } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { findField } from 'app/features/dimensions'; +import { findField } from 'app/features/dimensions/utils'; import { fieldExtractors } from './fieldExtractors'; import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types'; diff --git a/public/img/transformations/dark/calculateField.svg b/public/app/features/transformers/images/dark/calculateField.svg similarity index 100% rename from public/img/transformations/dark/calculateField.svg rename to public/app/features/transformers/images/dark/calculateField.svg diff --git a/public/img/transformations/dark/concatenate.svg b/public/app/features/transformers/images/dark/concatenate.svg similarity index 100% rename from public/img/transformations/dark/concatenate.svg rename to public/app/features/transformers/images/dark/concatenate.svg diff --git a/public/img/transformations/dark/configFromData.svg b/public/app/features/transformers/images/dark/configFromData.svg similarity index 100% rename from public/img/transformations/dark/configFromData.svg rename to public/app/features/transformers/images/dark/configFromData.svg diff --git a/public/img/transformations/dark/convertFieldType.svg b/public/app/features/transformers/images/dark/convertFieldType.svg similarity index 100% rename from public/img/transformations/dark/convertFieldType.svg rename to public/app/features/transformers/images/dark/convertFieldType.svg diff --git a/public/img/transformations/dark/extractFields.svg b/public/app/features/transformers/images/dark/extractFields.svg similarity index 100% rename from public/img/transformations/dark/extractFields.svg rename to public/app/features/transformers/images/dark/extractFields.svg diff --git a/public/img/transformations/dark/fieldLookup.svg b/public/app/features/transformers/images/dark/fieldLookup.svg similarity index 100% rename from public/img/transformations/dark/fieldLookup.svg rename to public/app/features/transformers/images/dark/fieldLookup.svg diff --git a/public/img/transformations/dark/filterByRefId.svg b/public/app/features/transformers/images/dark/filterByRefId.svg similarity index 100% rename from public/img/transformations/dark/filterByRefId.svg rename to public/app/features/transformers/images/dark/filterByRefId.svg diff --git a/public/img/transformations/dark/filterByValue.svg b/public/app/features/transformers/images/dark/filterByValue.svg similarity index 100% rename from public/img/transformations/dark/filterByValue.svg rename to public/app/features/transformers/images/dark/filterByValue.svg diff --git a/public/img/transformations/dark/filterFieldsByName.svg b/public/app/features/transformers/images/dark/filterFieldsByName.svg similarity index 100% rename from public/img/transformations/dark/filterFieldsByName.svg rename to public/app/features/transformers/images/dark/filterFieldsByName.svg diff --git a/public/img/transformations/dark/formatString.svg b/public/app/features/transformers/images/dark/formatString.svg similarity index 100% rename from public/img/transformations/dark/formatString.svg rename to public/app/features/transformers/images/dark/formatString.svg diff --git a/public/img/transformations/dark/formatTime.svg b/public/app/features/transformers/images/dark/formatTime.svg similarity index 100% rename from public/img/transformations/dark/formatTime.svg rename to public/app/features/transformers/images/dark/formatTime.svg diff --git a/public/img/transformations/dark/groupBy.svg b/public/app/features/transformers/images/dark/groupBy.svg similarity index 100% rename from public/img/transformations/dark/groupBy.svg rename to public/app/features/transformers/images/dark/groupBy.svg diff --git a/public/img/transformations/dark/groupByCalculation.svg b/public/app/features/transformers/images/dark/groupByCalculation.svg similarity index 100% rename from public/img/transformations/dark/groupByCalculation.svg rename to public/app/features/transformers/images/dark/groupByCalculation.svg diff --git a/public/img/transformations/dark/groupToNestedTable.svg b/public/app/features/transformers/images/dark/groupToNestedTable.svg similarity index 100% rename from public/img/transformations/dark/groupToNestedTable.svg rename to public/app/features/transformers/images/dark/groupToNestedTable.svg diff --git a/public/img/transformations/dark/groupingToMatrix.svg b/public/app/features/transformers/images/dark/groupingToMatrix.svg similarity index 100% rename from public/img/transformations/dark/groupingToMatrix.svg rename to public/app/features/transformers/images/dark/groupingToMatrix.svg diff --git a/public/img/transformations/dark/heatmap.svg b/public/app/features/transformers/images/dark/heatmap.svg similarity index 100% rename from public/img/transformations/dark/heatmap.svg rename to public/app/features/transformers/images/dark/heatmap.svg diff --git a/public/img/transformations/dark/histogram.svg b/public/app/features/transformers/images/dark/histogram.svg similarity index 100% rename from public/img/transformations/dark/histogram.svg rename to public/app/features/transformers/images/dark/histogram.svg diff --git a/public/img/transformations/dark/joinByField.svg b/public/app/features/transformers/images/dark/joinByField.svg similarity index 100% rename from public/img/transformations/dark/joinByField.svg rename to public/app/features/transformers/images/dark/joinByField.svg diff --git a/public/img/transformations/dark/joinByLabels.svg b/public/app/features/transformers/images/dark/joinByLabels.svg similarity index 100% rename from public/img/transformations/dark/joinByLabels.svg rename to public/app/features/transformers/images/dark/joinByLabels.svg diff --git a/public/img/transformations/dark/labelsToFields.svg b/public/app/features/transformers/images/dark/labelsToFields.svg similarity index 100% rename from public/img/transformations/dark/labelsToFields.svg rename to public/app/features/transformers/images/dark/labelsToFields.svg diff --git a/public/img/transformations/dark/limit.svg b/public/app/features/transformers/images/dark/limit.svg similarity index 100% rename from public/img/transformations/dark/limit.svg rename to public/app/features/transformers/images/dark/limit.svg diff --git a/public/img/transformations/dark/merge.svg b/public/app/features/transformers/images/dark/merge.svg similarity index 100% rename from public/img/transformations/dark/merge.svg rename to public/app/features/transformers/images/dark/merge.svg diff --git a/public/img/transformations/dark/organize.svg b/public/app/features/transformers/images/dark/organize.svg similarity index 100% rename from public/img/transformations/dark/organize.svg rename to public/app/features/transformers/images/dark/organize.svg diff --git a/public/img/transformations/dark/partitionByValues.svg b/public/app/features/transformers/images/dark/partitionByValues.svg similarity index 100% rename from public/img/transformations/dark/partitionByValues.svg rename to public/app/features/transformers/images/dark/partitionByValues.svg diff --git a/public/img/transformations/dark/prepareTimeSeries.svg b/public/app/features/transformers/images/dark/prepareTimeSeries.svg similarity index 100% rename from public/img/transformations/dark/prepareTimeSeries.svg rename to public/app/features/transformers/images/dark/prepareTimeSeries.svg diff --git a/public/img/transformations/dark/reduce.svg b/public/app/features/transformers/images/dark/reduce.svg similarity index 100% rename from public/img/transformations/dark/reduce.svg rename to public/app/features/transformers/images/dark/reduce.svg diff --git a/public/img/transformations/dark/regression.svg b/public/app/features/transformers/images/dark/regression.svg similarity index 100% rename from public/img/transformations/dark/regression.svg rename to public/app/features/transformers/images/dark/regression.svg diff --git a/public/img/transformations/dark/renameByRegex.svg b/public/app/features/transformers/images/dark/renameByRegex.svg similarity index 100% rename from public/img/transformations/dark/renameByRegex.svg rename to public/app/features/transformers/images/dark/renameByRegex.svg diff --git a/public/img/transformations/dark/rowsToFields.svg b/public/app/features/transformers/images/dark/rowsToFields.svg similarity index 100% rename from public/img/transformations/dark/rowsToFields.svg rename to public/app/features/transformers/images/dark/rowsToFields.svg diff --git a/public/img/transformations/dark/seriesToRows.svg b/public/app/features/transformers/images/dark/seriesToRows.svg similarity index 100% rename from public/img/transformations/dark/seriesToRows.svg rename to public/app/features/transformers/images/dark/seriesToRows.svg diff --git a/public/img/transformations/dark/sortBy.svg b/public/app/features/transformers/images/dark/sortBy.svg similarity index 100% rename from public/img/transformations/dark/sortBy.svg rename to public/app/features/transformers/images/dark/sortBy.svg diff --git a/public/img/transformations/dark/spatial.svg b/public/app/features/transformers/images/dark/spatial.svg similarity index 100% rename from public/img/transformations/dark/spatial.svg rename to public/app/features/transformers/images/dark/spatial.svg diff --git a/public/img/transformations/dark/timeSeriesTable.svg b/public/app/features/transformers/images/dark/timeSeriesTable.svg similarity index 100% rename from public/img/transformations/dark/timeSeriesTable.svg rename to public/app/features/transformers/images/dark/timeSeriesTable.svg diff --git a/public/img/transformations/dark/transpose.svg b/public/app/features/transformers/images/dark/transpose.svg similarity index 100% rename from public/img/transformations/dark/transpose.svg rename to public/app/features/transformers/images/dark/transpose.svg diff --git a/public/img/transformations/disabled/formatTime.svg b/public/app/features/transformers/images/disabled/formatTime.svg similarity index 100% rename from public/img/transformations/disabled/formatTime.svg rename to public/app/features/transformers/images/disabled/formatTime.svg diff --git a/public/img/transformations/disabled/groupBy.svg b/public/app/features/transformers/images/disabled/groupBy.svg similarity index 100% rename from public/img/transformations/disabled/groupBy.svg rename to public/app/features/transformers/images/disabled/groupBy.svg diff --git a/public/img/transformations/disabled/groupToNestedTable.svg b/public/app/features/transformers/images/disabled/groupToNestedTable.svg similarity index 100% rename from public/img/transformations/disabled/groupToNestedTable.svg rename to public/app/features/transformers/images/disabled/groupToNestedTable.svg diff --git a/public/img/transformations/disabled/groupingToMatrix.svg b/public/app/features/transformers/images/disabled/groupingToMatrix.svg similarity index 100% rename from public/img/transformations/disabled/groupingToMatrix.svg rename to public/app/features/transformers/images/disabled/groupingToMatrix.svg diff --git a/public/img/transformations/disabled/heatmap.svg b/public/app/features/transformers/images/disabled/heatmap.svg similarity index 100% rename from public/img/transformations/disabled/heatmap.svg rename to public/app/features/transformers/images/disabled/heatmap.svg diff --git a/public/img/transformations/disabled/merge.svg b/public/app/features/transformers/images/disabled/merge.svg similarity index 100% rename from public/img/transformations/disabled/merge.svg rename to public/app/features/transformers/images/disabled/merge.svg diff --git a/public/img/transformations/disabled/organize.svg b/public/app/features/transformers/images/disabled/organize.svg similarity index 100% rename from public/img/transformations/disabled/organize.svg rename to public/app/features/transformers/images/disabled/organize.svg diff --git a/public/img/transformations/disabled/timeSeriesTable.svg b/public/app/features/transformers/images/disabled/timeSeriesTable.svg similarity index 100% rename from public/img/transformations/disabled/timeSeriesTable.svg rename to public/app/features/transformers/images/disabled/timeSeriesTable.svg diff --git a/public/img/transformations/light/calculateField.svg b/public/app/features/transformers/images/light/calculateField.svg similarity index 100% rename from public/img/transformations/light/calculateField.svg rename to public/app/features/transformers/images/light/calculateField.svg diff --git a/public/img/transformations/light/concatenate.svg b/public/app/features/transformers/images/light/concatenate.svg similarity index 100% rename from public/img/transformations/light/concatenate.svg rename to public/app/features/transformers/images/light/concatenate.svg diff --git a/public/img/transformations/light/configFromData.svg b/public/app/features/transformers/images/light/configFromData.svg similarity index 100% rename from public/img/transformations/light/configFromData.svg rename to public/app/features/transformers/images/light/configFromData.svg diff --git a/public/img/transformations/light/convertFieldType.svg b/public/app/features/transformers/images/light/convertFieldType.svg similarity index 100% rename from public/img/transformations/light/convertFieldType.svg rename to public/app/features/transformers/images/light/convertFieldType.svg diff --git a/public/img/transformations/light/extractFields.svg b/public/app/features/transformers/images/light/extractFields.svg similarity index 100% rename from public/img/transformations/light/extractFields.svg rename to public/app/features/transformers/images/light/extractFields.svg diff --git a/public/img/transformations/light/fieldLookup.svg b/public/app/features/transformers/images/light/fieldLookup.svg similarity index 100% rename from public/img/transformations/light/fieldLookup.svg rename to public/app/features/transformers/images/light/fieldLookup.svg diff --git a/public/img/transformations/light/filterByRefId.svg b/public/app/features/transformers/images/light/filterByRefId.svg similarity index 100% rename from public/img/transformations/light/filterByRefId.svg rename to public/app/features/transformers/images/light/filterByRefId.svg diff --git a/public/img/transformations/light/filterByValue.svg b/public/app/features/transformers/images/light/filterByValue.svg similarity index 100% rename from public/img/transformations/light/filterByValue.svg rename to public/app/features/transformers/images/light/filterByValue.svg diff --git a/public/img/transformations/light/filterFieldsByName.svg b/public/app/features/transformers/images/light/filterFieldsByName.svg similarity index 100% rename from public/img/transformations/light/filterFieldsByName.svg rename to public/app/features/transformers/images/light/filterFieldsByName.svg diff --git a/public/img/transformations/light/formatString.svg b/public/app/features/transformers/images/light/formatString.svg similarity index 100% rename from public/img/transformations/light/formatString.svg rename to public/app/features/transformers/images/light/formatString.svg diff --git a/public/img/transformations/light/formatTime.svg b/public/app/features/transformers/images/light/formatTime.svg similarity index 100% rename from public/img/transformations/light/formatTime.svg rename to public/app/features/transformers/images/light/formatTime.svg diff --git a/public/img/transformations/light/groupBy.svg b/public/app/features/transformers/images/light/groupBy.svg similarity index 100% rename from public/img/transformations/light/groupBy.svg rename to public/app/features/transformers/images/light/groupBy.svg diff --git a/public/img/transformations/light/groupByCalculation.svg b/public/app/features/transformers/images/light/groupByCalculation.svg similarity index 100% rename from public/img/transformations/light/groupByCalculation.svg rename to public/app/features/transformers/images/light/groupByCalculation.svg diff --git a/public/img/transformations/light/groupToNestedTable.svg b/public/app/features/transformers/images/light/groupToNestedTable.svg similarity index 100% rename from public/img/transformations/light/groupToNestedTable.svg rename to public/app/features/transformers/images/light/groupToNestedTable.svg diff --git a/public/img/transformations/light/groupingToMatrix.svg b/public/app/features/transformers/images/light/groupingToMatrix.svg similarity index 100% rename from public/img/transformations/light/groupingToMatrix.svg rename to public/app/features/transformers/images/light/groupingToMatrix.svg diff --git a/public/img/transformations/light/heatmap.svg b/public/app/features/transformers/images/light/heatmap.svg similarity index 100% rename from public/img/transformations/light/heatmap.svg rename to public/app/features/transformers/images/light/heatmap.svg diff --git a/public/img/transformations/light/histogram.svg b/public/app/features/transformers/images/light/histogram.svg similarity index 100% rename from public/img/transformations/light/histogram.svg rename to public/app/features/transformers/images/light/histogram.svg diff --git a/public/img/transformations/light/joinByField.svg b/public/app/features/transformers/images/light/joinByField.svg similarity index 100% rename from public/img/transformations/light/joinByField.svg rename to public/app/features/transformers/images/light/joinByField.svg diff --git a/public/img/transformations/light/joinByLabels.svg b/public/app/features/transformers/images/light/joinByLabels.svg similarity index 100% rename from public/img/transformations/light/joinByLabels.svg rename to public/app/features/transformers/images/light/joinByLabels.svg diff --git a/public/img/transformations/light/labelsToFields.svg b/public/app/features/transformers/images/light/labelsToFields.svg similarity index 100% rename from public/img/transformations/light/labelsToFields.svg rename to public/app/features/transformers/images/light/labelsToFields.svg diff --git a/public/img/transformations/light/limit.svg b/public/app/features/transformers/images/light/limit.svg similarity index 100% rename from public/img/transformations/light/limit.svg rename to public/app/features/transformers/images/light/limit.svg diff --git a/public/img/transformations/light/merge.svg b/public/app/features/transformers/images/light/merge.svg similarity index 100% rename from public/img/transformations/light/merge.svg rename to public/app/features/transformers/images/light/merge.svg diff --git a/public/img/transformations/light/organize.svg b/public/app/features/transformers/images/light/organize.svg similarity index 100% rename from public/img/transformations/light/organize.svg rename to public/app/features/transformers/images/light/organize.svg diff --git a/public/img/transformations/light/partitionByValues.svg b/public/app/features/transformers/images/light/partitionByValues.svg similarity index 100% rename from public/img/transformations/light/partitionByValues.svg rename to public/app/features/transformers/images/light/partitionByValues.svg diff --git a/public/img/transformations/light/prepareTimeSeries.svg b/public/app/features/transformers/images/light/prepareTimeSeries.svg similarity index 100% rename from public/img/transformations/light/prepareTimeSeries.svg rename to public/app/features/transformers/images/light/prepareTimeSeries.svg diff --git a/public/img/transformations/light/reduce.svg b/public/app/features/transformers/images/light/reduce.svg similarity index 100% rename from public/img/transformations/light/reduce.svg rename to public/app/features/transformers/images/light/reduce.svg diff --git a/public/img/transformations/light/regression.svg b/public/app/features/transformers/images/light/regression.svg similarity index 100% rename from public/img/transformations/light/regression.svg rename to public/app/features/transformers/images/light/regression.svg diff --git a/public/img/transformations/light/renameByRegex.svg b/public/app/features/transformers/images/light/renameByRegex.svg similarity index 100% rename from public/img/transformations/light/renameByRegex.svg rename to public/app/features/transformers/images/light/renameByRegex.svg diff --git a/public/img/transformations/light/rowsToFields.svg b/public/app/features/transformers/images/light/rowsToFields.svg similarity index 100% rename from public/img/transformations/light/rowsToFields.svg rename to public/app/features/transformers/images/light/rowsToFields.svg diff --git a/public/img/transformations/light/seriesToRows.svg b/public/app/features/transformers/images/light/seriesToRows.svg similarity index 100% rename from public/img/transformations/light/seriesToRows.svg rename to public/app/features/transformers/images/light/seriesToRows.svg diff --git a/public/img/transformations/light/sortBy.svg b/public/app/features/transformers/images/light/sortBy.svg similarity index 100% rename from public/img/transformations/light/sortBy.svg rename to public/app/features/transformers/images/light/sortBy.svg diff --git a/public/img/transformations/light/spatial.svg b/public/app/features/transformers/images/light/spatial.svg similarity index 100% rename from public/img/transformations/light/spatial.svg rename to public/app/features/transformers/images/light/spatial.svg diff --git a/public/img/transformations/light/timeSeriesTable.svg b/public/app/features/transformers/images/light/timeSeriesTable.svg similarity index 100% rename from public/img/transformations/light/timeSeriesTable.svg rename to public/app/features/transformers/images/light/timeSeriesTable.svg diff --git a/public/img/transformations/light/transpose.svg b/public/app/features/transformers/images/light/transpose.svg similarity index 100% rename from public/img/transformations/light/transpose.svg rename to public/app/features/transformers/images/light/transpose.svg diff --git a/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx b/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx index 144f09552fb..d7ea6b75503 100644 --- a/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx +++ b/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx @@ -12,9 +12,11 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, HorizontalGroup, InlineField, InlineFieldRow, Select, ValuePicker } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/joinByLabels.svg'; +import lightImage from '../images/light/joinByLabels.svg'; import { getDistinctLabels } from '../utils'; -import { joinByLabelsTransformer, JoinByLabelsTransformOptions } from './joinByLabels'; +import { getJoinByLabelsTransformer, JoinByLabelsTransformOptions } from './joinByLabels'; export interface Props extends TransformerUIProps<JoinByLabelsTransformOptions> {} @@ -108,7 +110,7 @@ export function JoinByLabelsTransformerEditor({ input, options, onChange }: Prop <InlineFieldRow> <InlineField - error="required" + error={t('transformers.join-by-labels-transformer-editor.error-required', 'Required')} invalid={!Boolean(options.value?.length)} label={t('transformers.join-by-labels-transformer-editor.label-value', 'Value')} labelWidth={labelWidth} @@ -178,13 +180,18 @@ export function JoinByLabelsTransformerEditor({ input, options, onChange }: Prop ); } -export const joinByLabelsTransformRegistryItem: TransformerRegistryItem<JoinByLabelsTransformOptions> = { - id: joinByLabelsTransformer.id, - editor: JoinByLabelsTransformerEditor, - transformation: joinByLabelsTransformer, - name: joinByLabelsTransformer.name, - description: joinByLabelsTransformer.description, - state: PluginState.beta, - categories: new Set([TransformerCategory.Combine]), - help: getTransformationContent(joinByLabelsTransformer.id).helperDocs, +export const getJoinByLabelsTransformRegistryItem: () => TransformerRegistryItem<JoinByLabelsTransformOptions> = () => { + const joinByLabelsTransformer = getJoinByLabelsTransformer(); + return { + id: joinByLabelsTransformer.id, + editor: JoinByLabelsTransformerEditor, + transformation: joinByLabelsTransformer, + name: joinByLabelsTransformer.name, + description: joinByLabelsTransformer.description, + state: PluginState.beta, + categories: new Set([TransformerCategory.Combine]), + help: getTransformationContent(joinByLabelsTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; }; diff --git a/public/app/features/transformers/joinByLabels/joinByLabels.ts b/public/app/features/transformers/joinByLabels/joinByLabels.ts index 4133dae20a6..9d6d8c99772 100644 --- a/public/app/features/transformers/joinByLabels/joinByLabels.ts +++ b/public/app/features/transformers/joinByLabels/joinByLabels.ts @@ -1,6 +1,7 @@ import { map } from 'rxjs/operators'; import { DataFrame, DataTransformerID, Field, FieldType, SynchronousDataTransformerInfo } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { getDistinctLabels } from '../utils'; @@ -9,14 +10,17 @@ export interface JoinByLabelsTransformOptions { join?: string[]; } -export const joinByLabelsTransformer: SynchronousDataTransformerInfo<JoinByLabelsTransformOptions> = { +export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo<JoinByLabelsTransformOptions> = () => ({ id: DataTransformerID.joinByLabels, - name: 'Join by labels', - description: 'Flatten labeled results into a table joined by labels.', + name: t('transformers.get-join-by-labels-transformer.name.join-by-labels', 'Join by labels'), + description: t( + 'transformers.get-join-by-labels-transformer.description.flatten-labeled-results-table-joined-labels', + 'Flatten labeled results into a table joined by labels.' + ), defaultOptions: {}, operator: (options, ctx) => (source) => - source.pipe(map((data) => joinByLabelsTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getJoinByLabelsTransformer().transformer(options, ctx)(data))), transformer: (options: JoinByLabelsTransformOptions) => { return (data: DataFrame[]) => { @@ -26,7 +30,7 @@ export const joinByLabelsTransformer: SynchronousDataTransformerInfo<JoinByLabel return [joinByLabels(options, data)]; }; }, -}; +}); interface JoinValues { keys: string[]; diff --git a/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx b/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx index 054bb6874e1..d4829275781 100644 --- a/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx +++ b/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx @@ -16,26 +16,34 @@ import { FieldNamePicker } from '@grafana/ui/internal'; import { GazetteerPathEditor, GazetteerPathEditorConfigSettings } from 'app/features/geo/editor/GazetteerPathEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/fieldLookup.svg'; +import lightImage from '../images/light/fieldLookup.svg'; import { FieldLookupOptions, fieldLookupTransformer } from './fieldLookup'; -const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { - settings: { - width: 30, - filter: (f) => f.type === FieldType.string, - placeholderText: 'Select text field', - noFieldsMessage: 'No text fields found', - }, - name: '', - id: '', - editor: () => null, -}; - const fieldLookupSettings = { settings: {}, } as StandardEditorsRegistryItem<string, GazetteerPathEditorConfigSettings>; export const FieldLookupTransformerEditor = ({ input, options, onChange }: TransformerUIProps<FieldLookupOptions>) => { + const fieldNamePickerSettings: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = { + settings: { + width: 30, + filter: (f) => f.type === FieldType.string, + placeholderText: t( + 'transformers.field-lookup-transformer-editor.field-name-picker-settings.placeholderText.select-text-field', + 'Select text field' + ), + noFieldsMessage: t( + 'transformers.field-lookup-transformer-editor.field-name-picker-settings.noFieldsMessage.no-text-fields-found', + 'No text fields found' + ), + }, + name: '', + id: '', + editor: () => null, + }; + const onPickLookupField = useCallback( (value: string | undefined) => { onChange({ @@ -81,13 +89,21 @@ export const FieldLookupTransformerEditor = ({ input, options, onChange }: Trans ); }; -export const fieldLookupTransformRegistryItem: TransformerRegistryItem<FieldLookupOptions> = { +export const getFieldLookupTransformRegistryItem: () => TransformerRegistryItem<FieldLookupOptions> = () => ({ id: DataTransformerID.fieldLookup, editor: FieldLookupTransformerEditor, transformation: fieldLookupTransformer, - name: fieldLookupTransformer.name, - description: `Use a field value to lookup additional fields from an external source. This currently supports spatial data, but will eventually support more formats.`, + name: t( + 'transformers.field-lookup-transformer-editor.name.lookup-fields-from-resource', + 'Lookup fields from resource' + ), + description: t( + 'transformers.field-lookup-transformer-editor.description.lookup-additional-fields-external-source', + 'Use a field value to lookup countries, states, or airports.' + ), state: PluginState.alpha, categories: new Set([TransformerCategory.PerformSpatialOperations]), help: getTransformationContent(DataTransformerID.fieldLookup).helperDocs, -}; + imageDark: darkImage, + imageLight: lightImage, +}); diff --git a/public/app/features/transformers/lookupGazetteer/fieldLookup.ts b/public/app/features/transformers/lookupGazetteer/fieldLookup.ts index 871d06edf05..33aae7ca869 100644 --- a/public/app/features/transformers/lookupGazetteer/fieldLookup.ts +++ b/public/app/features/transformers/lookupGazetteer/fieldLookup.ts @@ -19,7 +19,7 @@ export interface FieldLookupOptions { export const fieldLookupTransformer: DataTransformerInfo<FieldLookupOptions> = { id: DataTransformerID.fieldLookup, name: 'Lookup fields from resource', - description: 'Retrieve matching data based on specified field', + description: 'Use a field value to lookup countries, states, or airports.', defaultOptions: {}, operator: (options) => (source) => source.pipe(mergeMap((data) => from(doGazetteerXform(data, options)))), diff --git a/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx b/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx index 8c552199900..e217384f051 100644 --- a/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx +++ b/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx @@ -21,8 +21,10 @@ import { import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/partitionByValues.svg'; +import lightImage from '../images/light/partitionByValues.svg'; -import { partitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; +import { getPartitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; export function PartitionByValuesEditor({ input, @@ -178,13 +180,19 @@ export function PartitionByValuesEditor({ ); } -export const partitionByValuesTransformRegistryItem: TransformerRegistryItem<PartitionByValuesTransformerOptions> = { - id: DataTransformerID.partitionByValues, - editor: PartitionByValuesEditor, - transformation: partitionByValuesTransformer, - name: partitionByValuesTransformer.name, - description: partitionByValuesTransformer.description, - state: PluginState.alpha, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.partitionByValues).helperDocs, -}; +export const getPartitionByValuesTransformRegistryItem: () => TransformerRegistryItem<PartitionByValuesTransformerOptions> = + () => { + const partitionByValuesTransformer = getPartitionByValuesTransformer(); + return { + id: DataTransformerID.partitionByValues, + editor: PartitionByValuesEditor, + transformation: partitionByValuesTransformer, + name: partitionByValuesTransformer.name, + description: partitionByValuesTransformer.description, + state: PluginState.alpha, + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.partitionByValues).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; + }; diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts index 3741ff8ea94..337efd10215 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts @@ -1,12 +1,14 @@ import { toDataFrame, FieldType } from '@grafana/data'; -import { partitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; +import { getPartitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; const ctx = { interpolate: (v: string) => v, }; describe('Partition by values transformer', () => { + const partitionByValuesTransformer = getPartitionByValuesTransformer(); + it('should partition by one field', () => { const source = [ toDataFrame({ diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.ts b/public/app/features/transformers/partitionByValues/partitionByValues.ts index 4ab8d360cbe..e0da0630cc0 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.ts @@ -9,6 +9,7 @@ import { FieldMatcher, } from '@grafana/data'; import { getMatcherConfig, noopTransformer } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { partition } from './partition'; @@ -62,35 +63,39 @@ function buildFieldLabels(names: string[], values: unknown[]) { return labels; } -export const partitionByValuesTransformer: SynchronousDataTransformerInfo<PartitionByValuesTransformerOptions> = { - id: DataTransformerID.partitionByValues, - name: 'Partition by values', - description: `Splits a one-frame dataset into multiple series discriminated by unique/enum values in one or more fields.`, - defaultOptions: { - keepFields: false, - }, +export const getPartitionByValuesTransformer: () => SynchronousDataTransformerInfo<PartitionByValuesTransformerOptions> = + () => ({ + id: DataTransformerID.partitionByValues, + name: t('transformers.get-partition-by-values-transformer.name.partition-by-values', 'Partition by values'), + description: t( + 'transformers.get-partition-by-values-transformer.description.split-oneframe-dataset-multiple-series', + 'Split a one-frame dataset into multiple series.' + ), + defaultOptions: { + keepFields: false, + }, - operator: (options, ctx) => (source) => - source.pipe(map((data) => partitionByValuesTransformer.transformer(options, ctx)(data))), + operator: (options, ctx) => (source) => + source.pipe(map((data) => getPartitionByValuesTransformer().transformer(options, ctx)(data))), - transformer: (options: PartitionByValuesTransformerOptions, ctx: DataTransformContext) => { - const matcherConfig = getMatcherConfig({ names: options.fields }); + transformer: (options: PartitionByValuesTransformerOptions, ctx: DataTransformContext) => { + const matcherConfig = getMatcherConfig({ names: options.fields }); - if (!matcherConfig) { - return noopTransformer.transformer({}, ctx); - } - - const matcher = getFieldMatcher(matcherConfig); - - return (data: DataFrame[]) => { - if (!data.length) { - return data; + if (!matcherConfig) { + return noopTransformer.transformer({}, ctx); } - // error if > 1 frame? - return partitionByValues(data[0], matcher, options); - }; - }, -}; + + const matcher = getFieldMatcher(matcherConfig); + + return (data: DataFrame[]) => { + if (!data.length) { + return data; + } + // error if > 1 frame? + return partitionByValues(data[0], matcher, options); + }; + }, + }); // Split a single frame dataset into multiple frames based on values in a set of fields export function partitionByValues( diff --git a/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx b/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx index 19399e3e595..c9e6b09f299 100644 --- a/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx +++ b/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx @@ -13,97 +13,113 @@ import { Trans, t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/prepareTimeSeries.svg'; +import lightImage from '../images/light/prepareTimeSeries.svg'; -import { prepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries'; - -const wideInfo = { - label: 'Wide time series', - value: timeSeriesFormat.TimeSeriesWide, - description: 'Creates a single frame joined by time', - info: ( - <ul> - <li> - <Trans i18nKey="transformers.wide-info.single-frame">Single frame</Trans> - </li> - <li> - <Trans i18nKey="transformers.wide-info.st-field-is-shared-time">1st field is shared time field</Trans> - </li> - <li> - <Trans i18nKey="transformers.wide-info.time-in-ascending-order">Time in ascending order</Trans> - </li> - <li> - <Trans i18nKey="transformers.wide-info.multiple-value-fields-of-any-type"> - Multiple value fields of any type - </Trans> - </li> - </ul> - ), -}; - -const multiInfo = { - label: 'Multi-frame time series', - value: timeSeriesFormat.TimeSeriesMulti, - description: 'Creates a new frame for each time/number pair', - info: ( - <ul> - <li> - <Trans i18nKey="transformers.multi-info.multiple-frames">Multiple frames</Trans> - </li> - <li> - <Trans i18nKey="transformers.multi-info.frame-fields-value" values={{ field1: 'time', field2: 'value' }}> - Each frame has two fields: {'{{field1}}'}, {'{{field2}}'} - </Trans> - </li> - <li> - <Trans i18nKey="transformers.multi-info.time-in-ascending-order">Time in ascending order</Trans> - </li> - <li> - <Trans i18nKey="transformers.multi-info.string-values-are-represented-as-labels"> - String values are represented as labels - </Trans> - </li> - <li> - <Trans i18nKey="transformers.multi-info.all-values-are-numeric">All values are numeric</Trans> - </li> - </ul> - ), -}; - -const longInfo = { - label: 'Long time series', - value: timeSeriesFormat.TimeSeriesLong, - description: 'Convert each frame to long format', - info: ( - <ul> - <li> - <Trans i18nKey="transformers.long-info.single-frame">Single frame</Trans> - </li> - <li> - <Trans i18nKey="transformers.long-info.st-field-is-time">1st field is time field</Trans> - </li> - <li> - <Trans i18nKey="transformers.long-info.ascending-order-duplicates"> - Time in ascending order, but may have duplicates - </Trans> - </li> - <li> - <Trans i18nKey="transformers.long-info.string-values-separate"> - String values are represented as separate fields rather than as labels - </Trans> - </li> - <li> - <Trans i18nKey="transformers.long-info.multiple-value-fields-may-exist">Multiple value fields may exist</Trans> - </li> - </ul> - ), -}; - -const formats: Array<SelectableValue<timeSeriesFormat>> = [wideInfo, multiInfo, longInfo]; +import { PrepareTimeSeriesOptions, timeSeriesFormat, getPrepareTimeSeriesTransformer } from './prepareTimeSeries'; export function PrepareTimeSeriesEditor(props: TransformerUIProps<PrepareTimeSeriesOptions>): React.ReactElement { const { options, onChange } = props; const styles = useStyles2(getStyles); + const wideInfo = { + label: t('transformers.prepare-time-series-editor.wide-info.label.wide-time-series', 'Wide time series'), + value: timeSeriesFormat.TimeSeriesWide, + description: t( + 'transformers.prepare-time-series-editor.wide-info.description.creates-single-frame-joined', + 'Creates a single frame joined by time' + ), + info: ( + <ul> + <li> + <Trans i18nKey="transformers.wide-info.single-frame">Single frame</Trans> + </li> + <li> + <Trans i18nKey="transformers.wide-info.st-field-is-shared-time">1st field is shared time field</Trans> + </li> + <li> + <Trans i18nKey="transformers.wide-info.time-in-ascending-order">Time in ascending order</Trans> + </li> + <li> + <Trans i18nKey="transformers.wide-info.multiple-value-fields-of-any-type"> + Multiple value fields of any type + </Trans> + </li> + </ul> + ), + }; + + const multiInfo = { + label: t( + 'transformers.prepare-time-series-editor.multi-info.label.multiframe-time-series', + 'Multi-frame time series' + ), + value: timeSeriesFormat.TimeSeriesMulti, + description: t( + 'transformers.prepare-time-series-editor.multi-info.description.creates-frame-timenumber', + 'Creates a new frame for each time/number pair' + ), + info: ( + <ul> + <li> + <Trans i18nKey="transformers.multi-info.multiple-frames">Multiple frames</Trans> + </li> + <li> + <Trans i18nKey="transformers.multi-info.frame-fields-value" values={{ field1: 'time', field2: 'value' }}> + Each frame has two fields: {'{{field1}}'}, {'{{field2}}'} + </Trans> + </li> + <li> + <Trans i18nKey="transformers.multi-info.time-in-ascending-order">Time in ascending order</Trans> + </li> + <li> + <Trans i18nKey="transformers.multi-info.string-values-are-represented-as-labels"> + String values are represented as labels + </Trans> + </li> + <li> + <Trans i18nKey="transformers.multi-info.all-values-are-numeric">All values are numeric</Trans> + </li> + </ul> + ), + }; + + const longInfo = { + label: t('transformers.prepare-time-series-editor.long-info.label.long-time-series', 'Long time series'), + value: timeSeriesFormat.TimeSeriesLong, + description: t( + 'transformers.prepare-time-series-editor.long-info.description.convert-each-frame-to-long-format', + 'Convert each frame to long format' + ), + info: ( + <ul> + <li> + <Trans i18nKey="transformers.long-info.single-frame">Single frame</Trans> + </li> + <li> + <Trans i18nKey="transformers.long-info.st-field-is-time">1st field is time field</Trans> + </li> + <li> + <Trans i18nKey="transformers.long-info.ascending-order-duplicates"> + Time in ascending order, but may have duplicates + </Trans> + </li> + <li> + <Trans i18nKey="transformers.long-info.string-values-separate"> + String values are represented as separate fields rather than as labels + </Trans> + </li> + <li> + <Trans i18nKey="transformers.long-info.multiple-value-fields-may-exist"> + Multiple value fields may exist + </Trans> + </li> + </ul> + ), + }; + + const formats: Array<SelectableValue<timeSeriesFormat>> = [wideInfo, multiInfo, longInfo]; + const onSelectFormat = useCallback( (value: SelectableValue<timeSeriesFormat>) => { onChange({ @@ -154,12 +170,18 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), }); -export const prepareTimeseriesTransformerRegistryItem: TransformerRegistryItem<PrepareTimeSeriesOptions> = { - id: prepareTimeSeriesTransformer.id, - editor: PrepareTimeSeriesEditor, - transformation: prepareTimeSeriesTransformer, - name: prepareTimeSeriesTransformer.name, - description: prepareTimeSeriesTransformer.description, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(prepareTimeSeriesTransformer.id).helperDocs, -}; +export const getPrepareTimeseriesTransformerRegistryItem: () => TransformerRegistryItem<PrepareTimeSeriesOptions> = + () => { + const prepareTimeSeriesTransformer = getPrepareTimeSeriesTransformer(); + return { + id: prepareTimeSeriesTransformer.id, + editor: PrepareTimeSeriesEditor, + transformation: prepareTimeSeriesTransformer, + name: prepareTimeSeriesTransformer.name, + description: prepareTimeSeriesTransformer.description, + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(prepareTimeSeriesTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; + }; diff --git a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts index 7184e16ff91..f96dc4946a4 100644 --- a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts +++ b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts @@ -8,13 +8,15 @@ import { getFrameDisplayName, } from '@grafana/data'; -import { prepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries'; +import { getPrepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries'; const ctx = { interpolate: (v: string) => v, }; describe('Prepare time series transformer', () => { + const prepareTimeSeriesTransformer = getPrepareTimeSeriesTransformer(); + it('should transform wide to multi', () => { const source = [ toDataFrame({ diff --git a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts index fc94e308ec7..a5924958bbc 100644 --- a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts +++ b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts @@ -12,6 +12,7 @@ import { Field, MutableDataFrame, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { Labels } from 'app/types/unified-alerting-dto'; import { partitionByValues } from '../partitionByValues/partitionByValues'; @@ -299,14 +300,17 @@ export function longToMultiTimeSeries(frame: DataFrame): DataFrame[] { }); } -export const prepareTimeSeriesTransformer: SynchronousDataTransformerInfo<PrepareTimeSeriesOptions> = { +export const getPrepareTimeSeriesTransformer: () => SynchronousDataTransformerInfo<PrepareTimeSeriesOptions> = () => ({ id: DataTransformerID.prepareTimeSeries, - name: 'Prepare time series', - description: `Will stretch data frames from the wide format into the long format. This is really helpful to be able to keep backwards compatibility for panels not supporting the new wide format.`, + name: t('transformers.prepare-time-series.name.prepare-time-series', 'Prepare time series'), + description: t( + 'transformers.prepare-time-series.description.stretch-data-frames', + 'Stretch data frames from the wide format into the long format.' + ), defaultOptions: {}, operator: (options, ctx) => (source) => - source.pipe(map((data) => prepareTimeSeriesTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getPrepareTimeSeriesTransformer().transformer(options, ctx)(data))), transformer: (options: PrepareTimeSeriesOptions) => { const format = options?.format ?? timeSeriesFormat.TimeSeriesWide; @@ -349,4 +353,4 @@ export const prepareTimeSeriesTransformer: SynchronousDataTransformerInfo<Prepar return []; }; }, -}; +}); diff --git a/public/app/features/transformers/regression/regression.test.ts b/public/app/features/transformers/regression/regression.test.ts index ec82cfca2d5..82aa0952e72 100644 --- a/public/app/features/transformers/regression/regression.test.ts +++ b/public/app/features/transformers/regression/regression.test.ts @@ -8,9 +8,11 @@ import { toDataFrameDTO, } from '@grafana/data'; -import { ModelType, RegressionTransformer, RegressionTransformerOptions } from './regression'; +import { ModelType, getRegressionTransformer, RegressionTransformerOptions } from './regression'; describe('Regression transformation', () => { + const RegressionTransformer = getRegressionTransformer(); + it('it should predict a linear regression to exactly fit the data when the data is f(x) = x', () => { const source = [ toDataFrame({ diff --git a/public/app/features/transformers/regression/regression.ts b/public/app/features/transformers/regression/regression.ts index b7d532d381c..cda498b5e9b 100644 --- a/public/app/features/transformers/regression/regression.ts +++ b/public/app/features/transformers/regression/regression.ts @@ -35,12 +35,15 @@ export const DEGREES = [ { label: () => t('transformers.regression-transformer-editor.label.quintic', 'Quintic'), value: 5 }, ]; -export const RegressionTransformer: SynchronousDataTransformerInfo<RegressionTransformerOptions> = { +export const getRegressionTransformer: () => SynchronousDataTransformerInfo<RegressionTransformerOptions> = () => ({ id: DataTransformerID.regression, - name: 'Regression analysis', - description: 'Create a new data frame containing values predicted by a statistical model.', + name: t('transformers.regression.name.regression-analysis', 'Regression analysis'), + description: t( + 'transformers.regression.description.create-new-data-frame', + 'Create a new data frame containing values predicted by a statistical model.' + ), operator: (options, ctx) => (source) => - source.pipe(map((data) => RegressionTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getRegressionTransformer().transformer(options, ctx)(data))), transformer: (options, ctx) => { return (frames: DataFrame[]) => { const { predictionCount, modelType, degree } = { ...DEFAULTS, ...options }; @@ -137,4 +140,4 @@ export const RegressionTransformer: SynchronousDataTransformerInfo<RegressionTra return [...frames, newFrame]; }; }, -}; +}); diff --git a/public/app/features/transformers/regression/regressionEditor.tsx b/public/app/features/transformers/regression/regressionEditor.tsx index 2b340daef2e..abf9b862b19 100644 --- a/public/app/features/transformers/regression/regressionEditor.tsx +++ b/public/app/features/transformers/regression/regressionEditor.tsx @@ -15,8 +15,10 @@ import { FieldNamePicker } from '@grafana/ui/internal'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/regression.svg'; +import lightImage from '../images/light/regression.svg'; -import { DEFAULTS, DEGREES, ModelType, RegressionTransformer, RegressionTransformerOptions } from './regression'; +import { DEFAULTS, DEGREES, ModelType, getRegressionTransformer, RegressionTransformerOptions } from './regression'; const fieldNamePickerSettings = { editor: FieldNamePicker, @@ -157,12 +159,17 @@ export const RegressionTransformerEditor = ({ ); }; -export const regressionTransformerRegistryItem: TransformerRegistryItem<RegressionTransformerOptions> = { - id: DataTransformerID.regression, - editor: RegressionTransformerEditor, - transformation: RegressionTransformer, - name: RegressionTransformer.name, - description: RegressionTransformer.description, - categories: new Set([TransformerCategory.CalculateNewFields]), - help: getTransformationContent(DataTransformerID.regression).helperDocs, +export const getRegressionTransformerRegistryItem: () => TransformerRegistryItem<RegressionTransformerOptions> = () => { + const regressionTransformer = getRegressionTransformer(); + return { + id: DataTransformerID.regression, + editor: RegressionTransformerEditor, + transformation: regressionTransformer, + name: regressionTransformer.name, + description: regressionTransformer.description, + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(DataTransformerID.regression).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; }; diff --git a/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx b/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx index 8e4a0b5faad..ed111c06b03 100644 --- a/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx @@ -2,8 +2,10 @@ import { PluginState, TransformerRegistryItem, TransformerUIProps, TransformerCa import { getTransformationContent } from '../docs/getTransformationContent'; import { FieldToConfigMappingEditor } from '../fieldToConfigMapping/FieldToConfigMappingEditor'; +import darkImage from '../images/dark/rowsToFields.svg'; +import lightImage from '../images/light/rowsToFields.svg'; -import { rowsToFieldsTransformer, RowToFieldsTransformOptions } from './rowsToFields'; +import { getRowsToFieldsTransformer, RowToFieldsTransformOptions } from './rowsToFields'; export interface Props extends TransformerUIProps<RowToFieldsTransformOptions> {} @@ -24,13 +26,18 @@ export function RowsToFieldsTransformerEditor({ input, options, onChange }: Prop ); } -export const rowsToFieldsTransformRegistryItem: TransformerRegistryItem<RowToFieldsTransformOptions> = { - id: rowsToFieldsTransformer.id, - editor: RowsToFieldsTransformerEditor, - transformation: rowsToFieldsTransformer, - name: rowsToFieldsTransformer.name, - description: rowsToFieldsTransformer.description, - state: PluginState.beta, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(rowsToFieldsTransformer.id).helperDocs, +export const getRowsToFieldsTransformRegistryItem: () => TransformerRegistryItem<RowToFieldsTransformOptions> = () => { + const rowsToFieldsTransformer = getRowsToFieldsTransformer(); + return { + id: rowsToFieldsTransformer.id, + editor: RowsToFieldsTransformerEditor, + transformation: rowsToFieldsTransformer, + name: rowsToFieldsTransformer.name, + description: rowsToFieldsTransformer.description, + state: PluginState.beta, + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(rowsToFieldsTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; }; diff --git a/public/app/features/transformers/rowsToFields/rowsToFields.ts b/public/app/features/transformers/rowsToFields/rowsToFields.ts index 7bdece8e98b..d7f5173273b 100644 --- a/public/app/features/transformers/rowsToFields/rowsToFields.ts +++ b/public/app/features/transformers/rowsToFields/rowsToFields.ts @@ -1,6 +1,7 @@ import { map } from 'rxjs/operators'; import { DataFrame, DataTransformerID, DataTransformerInfo, Field, getFieldDisplayName, Labels } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { EvaluatedMappingResult, @@ -16,10 +17,13 @@ export interface RowToFieldsTransformOptions { mappings?: FieldToConfigMapping[]; } -export const rowsToFieldsTransformer: DataTransformerInfo<RowToFieldsTransformOptions> = { +export const getRowsToFieldsTransformer: () => DataTransformerInfo<RowToFieldsTransformOptions> = () => ({ id: DataTransformerID.rowsToFields, - name: 'Rows to fields', - description: 'Convert each row into a field with dynamic config.', + name: t('transformers.get-rows-to-fields-transformer.name.rows-to-fields', 'Rows to fields'), + description: t( + 'transformers.get-rows-to-fields-transformer.description.convert-field-dynamic-config', + 'Convert each row into a field with dynamic config.' + ), defaultOptions: {}, /** @@ -32,7 +36,7 @@ export const rowsToFieldsTransformer: DataTransformerInfo<RowToFieldsTransformOp return data.map((frame) => rowsToFields(options, frame)); }) ), -}; +}); export function rowsToFields(options: RowToFieldsTransformOptions, data: DataFrame): DataFrame { const mappingResult = evaluateFieldMappings(data, options.mappings ?? [], true); diff --git a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx index 71c8d6d7287..8d63ed60746 100644 --- a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx +++ b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx @@ -17,10 +17,12 @@ import { useTheme2 } from '@grafana/ui'; import { addLocationFields } from 'app/features/geo/editor/locationEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/spatial.svg'; +import lightImage from '../images/light/spatial.svg'; import { SpatialCalculation, SpatialOperation, SpatialAction, SpatialTransformOptions } from './models.gen'; import { getDefaultOptions, getTransformerOptionPane } from './optionsHelper'; -import { isLineBuilderOption, spatialTransformer } from './spatialTransformer'; +import { isLineBuilderOption, getSpatialTransformer } from './spatialTransformer'; // Nothing defined in state const supplier = ( @@ -173,13 +175,18 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export const spatialTransformRegistryItem: TransformerRegistryItem<SpatialTransformOptions> = { - id: DataTransformerID.spatial, - editor: SetGeometryTransformerEditor, - transformation: spatialTransformer, - name: spatialTransformer.name, - description: spatialTransformer.description, - state: PluginState.alpha, - categories: new Set([TransformerCategory.PerformSpatialOperations]), - help: getTransformationContent(DataTransformerID.spatial).helperDocs, +export const getSpatialTransformRegistryItem: () => TransformerRegistryItem<SpatialTransformOptions> = () => { + const spatialTransformer = getSpatialTransformer(); + return { + id: DataTransformerID.spatial, + editor: SetGeometryTransformerEditor, + transformation: spatialTransformer, + name: spatialTransformer.name, + description: spatialTransformer.description, + state: PluginState.alpha, + categories: new Set([TransformerCategory.PerformSpatialOperations]), + help: getTransformationContent(DataTransformerID.spatial).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; }; diff --git a/public/app/features/transformers/spatial/spatialTransformer.ts b/public/app/features/transformers/spatial/spatialTransformer.ts index 7bf8c2adf3f..19e0f90d50b 100644 --- a/public/app/features/transformers/spatial/spatialTransformer.ts +++ b/public/app/features/transformers/spatial/spatialTransformer.ts @@ -1,20 +1,24 @@ import { mergeMap, from } from 'rxjs'; import { DataFrame, DataTransformerID, DataTransformerInfo, FieldType } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { createGeometryCollection, createLineBetween } from 'app/features/geo/format/utils'; import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location'; import { SpatialOperation, SpatialAction, SpatialTransformOptions } from './models.gen'; import { doGeomeryCalculation, toLineString } from './utils'; -export const spatialTransformer: DataTransformerInfo<SpatialTransformOptions> = { +export const getSpatialTransformer: () => DataTransformerInfo<SpatialTransformOptions> = () => ({ id: DataTransformerID.spatial, - name: 'Spatial operations', - description: 'Apply spatial operations to query results.', + name: t('transformers.get-spatial-transformer.name.spatial-operations', 'Spatial operations'), + description: t( + 'transformers.get-spatial-transformer.description.apply-spatial-operations-to-query-results', + 'Apply spatial operations to query results.' + ), defaultOptions: {}, operator: (options) => (source) => source.pipe(mergeMap((data) => from(doSetGeometry(data, options)))), -}; +}); export function isLineBuilderOption(options: SpatialTransformOptions): boolean { return options.action === SpatialAction.Modify && options.modify?.op === SpatialOperation.LineBuilder; diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index 4f8a5598c75..3fdd629a6e4 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -1,74 +1,74 @@ import { TransformerRegistryItem } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { filterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor'; -import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor'; -import { configFromQueryTransformRegistryItem } from './configFromQuery/ConfigFromQueryTransformerEditor'; -import { calculateFieldTransformRegistryItem } from './editors/CalculateFieldTransformerEditor'; -import { concatenateTransformRegistryItem } from './editors/ConcatenateTransformerEditor'; -import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor'; -import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor'; -import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor'; -import { formatStringTransformerRegistryItem } from './editors/FormatStringTransformerEditor'; -import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor'; -import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor'; -import { groupToNestedTableTransformRegistryItem } from './editors/GroupToNestedTableTransformerEditor'; -import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor'; -import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor'; -import { joinByFieldTransformerRegistryItem } from './editors/JoinByFieldTransformerEditor'; -import { labelsToFieldsTransformerRegistryItem } from './editors/LabelsToFieldsTransformerEditor'; -import { limitTransformRegistryItem } from './editors/LimitTransformerEditor'; -import { mergeTransformerRegistryItem } from './editors/MergeTransformerEditor'; -import { organizeFieldsTransformRegistryItem } from './editors/OrganizeFieldsTransformerEditor'; -import { reduceTransformRegistryItem } from './editors/ReduceTransformerEditor'; -import { renameByRegexTransformRegistryItem } from './editors/RenameByRegexTransformer'; -import { seriesToRowsTransformerRegistryItem } from './editors/SeriesToRowsTransformerEditor'; -import { sortByTransformRegistryItem } from './editors/SortByTransformerEditor'; -import { transposeTransformerRegistryItem } from './editors/TransposeTransformerEditor'; -import { extractFieldsTransformRegistryItem } from './extractFields/ExtractFieldsTransformerEditor'; -import { joinByLabelsTransformRegistryItem } from './joinByLabels/JoinByLabelsTransformerEditor'; -import { fieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupTransformerEditor'; -import { partitionByValuesTransformRegistryItem } from './partitionByValues/PartitionByValuesEditor'; -import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; -import { regressionTransformerRegistryItem } from './regression/regressionEditor'; -import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; -import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; -import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; +import { getFilterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor'; +import { getHeatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor'; +import { getConfigFromQueryTransformRegistryItem } from './configFromQuery/ConfigFromQueryTransformerEditor'; +import { getCalculateFieldTransformRegistryItem } from './editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor'; +import { getConcatenateTransformRegistryItem } from './editors/ConcatenateTransformerEditor'; +import { getConvertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor'; +import { getFilterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor'; +import { getFilterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor'; +import { getFormatStringTransformerRegistryItem } from './editors/FormatStringTransformerEditor'; +import { getFormatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor'; +import { getGroupByTransformRegistryItem } from './editors/GroupByTransformerEditor'; +import { getGroupToNestedTableTransformRegistryItem } from './editors/GroupToNestedTableTransformerEditor'; +import { getGroupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor'; +import { getHistogramTransformRegistryItem } from './editors/HistogramTransformerEditor'; +import { getJoinByFieldTransformerRegistryItem } from './editors/JoinByFieldTransformerEditor'; +import { getLabelsToFieldsTransformerRegistryItem } from './editors/LabelsToFieldsTransformerEditor'; +import { getLimitTransformRegistryItem } from './editors/LimitTransformerEditor'; +import { getMergeTransformerRegistryItem } from './editors/MergeTransformerEditor'; +import { getOrganizeFieldsTransformRegistryItem } from './editors/OrganizeFieldsTransformerEditor'; +import { getReduceTransformRegistryItem } from './editors/ReduceTransformerEditor'; +import { getRenameByRegexTransformRegistryItem } from './editors/RenameByRegexTransformer'; +import { getSeriesToRowsTransformerRegistryItem } from './editors/SeriesToRowsTransformerEditor'; +import { getSortByTransformRegistryItem } from './editors/SortByTransformerEditor'; +import { getTransposeTransformerRegistryItem } from './editors/TransposeTransformerEditor'; +import { getExtractFieldsTransformRegistryItem } from './extractFields/ExtractFieldsTransformerEditor'; +import { getJoinByLabelsTransformRegistryItem } from './joinByLabels/JoinByLabelsTransformerEditor'; +import { getFieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupTransformerEditor'; +import { getPartitionByValuesTransformRegistryItem } from './partitionByValues/PartitionByValuesEditor'; +import { getPrepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; +import { getRegressionTransformerRegistryItem } from './regression/regressionEditor'; +import { getRowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; +import { getSpatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; +import { getTimeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; export const getStandardTransformers = (): TransformerRegistryItem[] => { return [ - reduceTransformRegistryItem, - filterFieldsByNameTransformRegistryItem, - renameByRegexTransformRegistryItem, - filterFramesByRefIdTransformRegistryItem, - filterByValueTransformRegistryItem, - organizeFieldsTransformRegistryItem, - joinByFieldTransformerRegistryItem, - seriesToRowsTransformerRegistryItem, - concatenateTransformRegistryItem, - calculateFieldTransformRegistryItem, - labelsToFieldsTransformerRegistryItem, - groupByTransformRegistryItem, - sortByTransformRegistryItem, - mergeTransformerRegistryItem, - histogramTransformRegistryItem, - rowsToFieldsTransformRegistryItem, - configFromQueryTransformRegistryItem, - prepareTimeseriesTransformerRegistryItem, - convertFieldTypeTransformRegistryItem, - spatialTransformRegistryItem, - fieldLookupTransformRegistryItem, - extractFieldsTransformRegistryItem, - heatmapTransformRegistryItem, - groupingToMatrixTransformRegistryItem, - limitTransformRegistryItem, - joinByLabelsTransformRegistryItem, - regressionTransformerRegistryItem, - partitionByValuesTransformRegistryItem, - ...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []), - ...(config.featureToggles.groupToNestedTableTransformation ? [groupToNestedTableTransformRegistryItem] : []), - formatTimeTransformerRegistryItem, - timeSeriesTableTransformRegistryItem, - transposeTransformerRegistryItem, + getReduceTransformRegistryItem(), + getFilterFieldsByNameTransformRegistryItem(), + getRenameByRegexTransformRegistryItem(), + getFilterFramesByRefIdTransformRegistryItem(), + getFilterByValueTransformRegistryItem(), + getOrganizeFieldsTransformRegistryItem(), + getJoinByFieldTransformerRegistryItem(), + getSeriesToRowsTransformerRegistryItem(), + getConcatenateTransformRegistryItem(), + getCalculateFieldTransformRegistryItem(), + getLabelsToFieldsTransformerRegistryItem(), + getGroupByTransformRegistryItem(), + getSortByTransformRegistryItem(), + getMergeTransformerRegistryItem(), + getHistogramTransformRegistryItem(), + getRowsToFieldsTransformRegistryItem(), + getConfigFromQueryTransformRegistryItem(), + getPrepareTimeseriesTransformerRegistryItem(), + getConvertFieldTypeTransformRegistryItem(), + getSpatialTransformRegistryItem(), + getFieldLookupTransformRegistryItem(), + getExtractFieldsTransformRegistryItem(), + getHeatmapTransformRegistryItem(), + getGroupingToMatrixTransformRegistryItem(), + getLimitTransformRegistryItem(), + getJoinByLabelsTransformRegistryItem(), + getRegressionTransformerRegistryItem(), + getPartitionByValuesTransformRegistryItem(), + ...(config.featureToggles.formatString ? [getFormatStringTransformerRegistryItem()] : []), + ...(config.featureToggles.groupToNestedTableTransformation ? [getGroupToNestedTableTransformRegistryItem()] : []), + getFormatTimeTransformerRegistryItem(), + getTimeSeriesTableTransformRegistryItem(), + getTransposeTransformerRegistryItem(), ]; }; diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx index bc47f4eb04c..2cf438efdb7 100644 --- a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -15,9 +15,11 @@ import { t } from '@grafana/i18n'; import { InlineFieldRow, InlineField, StatsPicker, Select, InlineLabel } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/timeSeriesTable.svg'; +import lightImage from '../images/light/timeSeriesTable.svg'; import { - timeSeriesTableTransformer, + getTimeSeriesTableTransformer, TimeSeriesTableTransformerOptions, getRefData, } from './timeSeriesTableTransformer'; @@ -123,12 +125,18 @@ export function TimeSeriesTableTransformEditor({ return <>{configRows}</>; } -export const timeSeriesTableTransformRegistryItem: TransformerRegistryItem<TimeSeriesTableTransformerOptions> = { - id: timeSeriesTableTransformer.id, - editor: TimeSeriesTableTransformEditor, - transformation: timeSeriesTableTransformer, - name: timeSeriesTableTransformer.name, - description: timeSeriesTableTransformer.description, - state: PluginState.beta, - help: getTransformationContent(timeSeriesTableTransformer.id).helperDocs, -}; +export const getTimeSeriesTableTransformRegistryItem: () => TransformerRegistryItem<TimeSeriesTableTransformerOptions> = + () => { + const timeSeriesTableTransformer = getTimeSeriesTableTransformer(); + return { + id: timeSeriesTableTransformer.id, + editor: TimeSeriesTableTransformEditor, + transformation: timeSeriesTableTransformer, + name: timeSeriesTableTransformer.name, + description: timeSeriesTableTransformer.description, + state: PluginState.beta, + help: getTransformationContent(timeSeriesTableTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, + }; + }; diff --git a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts index fc20a059fde..d3e4073022b 100644 --- a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts +++ b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts @@ -14,6 +14,7 @@ import { TransformationApplicabilityLevels, isTimeSeriesField, } from '@grafana/data'; +import { t } from '@grafana/i18n'; /** * Maps a refId to a Field which can contain @@ -81,10 +82,13 @@ export interface RefIdTransformerOptions { inlineStat?: boolean; } -export const timeSeriesTableTransformer: DataTransformerInfo<TimeSeriesTableTransformerOptions> = { +export const getTimeSeriesTableTransformer: () => DataTransformerInfo<TimeSeriesTableTransformerOptions> = () => ({ id: DataTransformerID.timeSeriesTable, - name: 'Time series to table', - description: 'Convert time series data to table rows so that they can be viewed in tabular or sparkline format.', + name: t('transformers.time-series-table.name.time-series-to-table', 'Time series to table'), + description: t( + 'transformers.time-series-table.description.convert-to-table-rows', + 'Convert time series data to table rows so that they can be viewed in tabular or sparkline format.' + ), defaultOptions: {}, isApplicable: (data) => { for (const frame of data) { @@ -95,15 +99,17 @@ export const timeSeriesTableTransformer: DataTransformerInfo<TimeSeriesTableTran return TransformationApplicabilityLevels.NotApplicable; }, - isApplicableDescription: - 'The Time series to table transformation requires at least one time series frame to function. You currently have none.', + isApplicableDescription: t( + 'transformers.time-series-table.is-applicable-description.requires-time-series-frame', + 'The Time series to table transformation requires at least one time series frame to function. You currently have none.' + ), operator: (options) => (source) => source.pipe( map((data) => { return timeSeriesToTableTransform(options, data); }) ), -}; +}); /** * Converts time series frames to table frames for use with sparkline chart type. diff --git a/public/app/features/transformers/utils.ts b/public/app/features/transformers/utils.ts index fcb18493e90..7cbf4cd1241 100644 --- a/public/app/features/transformers/utils.ts +++ b/public/app/features/transformers/utils.ts @@ -62,15 +62,21 @@ export function getDistinctLabels(input: DataFrame[]): Set<string> { return distinct; } -export const categoriesLabels: { [K in TransformerCategory]: string } = { - combine: 'Combine', - calculateNewFields: 'Calculate new fields', - createNewVisualization: 'Create new visualization', - filter: 'Filter', - performSpatialOperations: 'Perform spatial operations', - reformat: 'Reformat', - reorderAndRename: 'Reorder and rename', -}; +export const getCategoriesLabels: () => { [K in TransformerCategory]: string } = () => ({ + combine: t('transformers.utils.get-categories-labels.combine', 'Combine'), + calculateNewFields: t('transformers.utils.get-categories-labels.calculate-new-fields', 'Calculate new fields'), + createNewVisualization: t( + 'transformers.utils.get-categories-labels.create-new-visualization', + 'Create new visualization' + ), + filter: t('transformers.utils.get-categories-labels.filter', 'Filter'), + performSpatialOperations: t( + 'transformers.utils.get-categories-labels.perform-spatial-operations', + 'Perform spatial operations' + ), + reformat: t('transformers.utils.get-categories-labels.reformat', 'Reformat'), + reorderAndRename: t('transformers.utils.get-categories-labels.reorder-and-rename', 'Reorder and rename'), +}); export const numberOrVariableValidator = (value: string | number) => { if (typeof value === 'number') { diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 79659a9e54f..18ab669a9f0 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -5,7 +5,8 @@ import { reportInteraction } from '@grafana/runtime'; import { RadioButtonGroup, LinkButton, FilterInput, InlineField } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; -import { AccessControlAction, StoreState } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { StoreState } from 'app/types/store'; import { selectTotal } from '../invites/state/selectors'; diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx index 4f8f88c3c4a..91dc6767767 100644 --- a/public/app/features/users/UsersListPage.test.tsx +++ b/public/app/features/users/UsersListPage.test.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { configureStore } from 'app/store/configureStore'; -import { Invitee, OrgUser } from 'app/types'; +import { Invitee, OrgUser } from 'app/types/user'; import { Props, UsersListPageUnconnected } from './UsersListPage'; import { pageChanged, sortChanged } from './state/reducers'; diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index e1992a7f46a..b362bf81621 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { renderMarkdown } from '@grafana/data'; +import { OrgRole, renderMarkdown } from '@grafana/data'; import { Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { OrgUser, OrgRole, StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; +import { OrgUser } from 'app/types/user'; import { OrgUsersTable } from '../admin/Users/OrgUsersTable'; import InviteesTable from '../invites/InviteesTable'; diff --git a/public/app/features/users/mocks/userMocks.ts b/public/app/features/users/mocks/userMocks.ts index 68ce9a4bfba..6c0895f1887 100644 --- a/public/app/features/users/mocks/userMocks.ts +++ b/public/app/features/users/mocks/userMocks.ts @@ -1,4 +1,5 @@ -import { OrgRole, OrgUser } from 'app/types'; +import { OrgRole } from '@grafana/data'; +import { OrgUser } from 'app/types/user'; import { UsersFetchResult, initialState } from '../state/reducers'; diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts index 9dbc7672f63..b13bf27e223 100644 --- a/public/app/features/users/state/actions.ts +++ b/public/app/features/users/state/actions.ts @@ -4,9 +4,9 @@ import { getBackendSrv } from '@grafana/runtime'; import { FetchDataArgs } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { OrgUser } from 'app/types'; - -import { AccessControlAction, ThunkResult } from '../../../types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { ThunkResult } from 'app/types/store'; +import { OrgUser } from 'app/types/user'; import { usersLoaded, diff --git a/public/app/features/users/state/reducers.test.ts b/public/app/features/users/state/reducers.test.ts index 5c76c88ba5e..abfc484300b 100644 --- a/public/app/features/users/state/reducers.test.ts +++ b/public/app/features/users/state/reducers.test.ts @@ -1,5 +1,6 @@ +import { UsersState } from 'app/types/user'; + import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { UsersState } from '../../../types'; import { getMockUsers, getFetchUsersMock } from '../mocks/userMocks'; import { initialState, searchQueryChanged, usersLoaded, usersReducer } from './reducers'; diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts index f74fea6ad1d..bb7cc4c8ea0 100644 --- a/public/app/features/users/state/reducers.ts +++ b/public/app/features/users/state/reducers.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import config from 'app/core/config'; -import { OrgUser, UsersState } from 'app/types'; +import { UsersState, OrgUser } from 'app/types/user'; export const initialState: UsersState = { users: [], diff --git a/public/app/features/users/state/selectors.ts b/public/app/features/users/state/selectors.ts index 2d80444a47d..a2ba378aaf4 100644 --- a/public/app/features/users/state/selectors.ts +++ b/public/app/features/users/state/selectors.ts @@ -1,4 +1,4 @@ -import { UsersState } from 'app/types'; +import { UsersState } from 'app/types/user'; export const getUsers = (state: UsersState) => { const regex = new RegExp(state.searchQuery, 'i'); diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx index 90e99866827..65745e8337f 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { AdHocVariableModel, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; import { AdHocVariableForm } from 'app/features/dashboard-scene/settings/variables/components/AdHocVariableForm'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { initialVariableEditorState } from '../editor/reducer'; import { getAdhocVariableEditorState } from '../editor/selectors'; diff --git a/public/app/features/variables/adhoc/actions.ts b/public/app/features/variables/adhoc/actions.ts index 4263bd09d3f..7e57e0d6344 100644 --- a/public/app/features/variables/adhoc/actions.ts +++ b/public/app/features/variables/adhoc/actions.ts @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'; import { AdHocVariableFilter, AdHocVariableModel, DataSourceRef } from '@grafana/data'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { StoreState, ThunkResult } from 'app/types'; +import { StoreState, ThunkResult } from 'app/types/store'; import { changeVariableEditorExtended } from '../editor/reducer'; import { isAdHoc } from '../guard'; diff --git a/public/app/features/variables/constant/actions.ts b/public/app/features/variables/constant/actions.ts index 87f78229983..17ba4e9699b 100644 --- a/public/app/features/variables/constant/actions.ts +++ b/public/app/features/variables/constant/actions.ts @@ -1,4 +1,4 @@ -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { validateVariableSelectionState } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; diff --git a/public/app/features/variables/custom/CustomVariableEditor.tsx b/public/app/features/variables/custom/CustomVariableEditor.tsx index 7488326cec7..82b783542e2 100644 --- a/public/app/features/variables/custom/CustomVariableEditor.tsx +++ b/public/app/features/variables/custom/CustomVariableEditor.tsx @@ -4,7 +4,7 @@ import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { CustomVariableModel, VariableWithMultiSupport } from '@grafana/data'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { CustomVariableForm } from 'app/features/dashboard-scene/settings/variables/components/CustomVariableForm'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; import { changeVariableMultiValue } from '../state/actions'; diff --git a/public/app/features/variables/custom/actions.ts b/public/app/features/variables/custom/actions.ts index d8751dd56ed..73eff38f5f5 100644 --- a/public/app/features/variables/custom/actions.ts +++ b/public/app/features/variables/custom/actions.ts @@ -1,5 +1,5 @@ import { getTemplateSrv } from '@grafana/runtime'; -import { ThunkResult } from 'app/types'; +import { ThunkResult } from 'app/types/store'; import { validateVariableSelectionState } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; diff --git a/public/app/features/variables/datasource/DataSourceVariableEditor.tsx b/public/app/features/variables/datasource/DataSourceVariableEditor.tsx index 2f9116d4d7b..256c5ff29fd 100644 --- a/public/app/features/variables/datasource/DataSourceVariableEditor.tsx +++ b/public/app/features/variables/datasource/DataSourceVariableEditor.tsx @@ -3,8 +3,8 @@ import { connect, ConnectedProps } from 'react-redux'; import { DataSourceVariableModel, SelectableValue, VariableWithMultiSupport } from '@grafana/data'; import { DataSourceVariableForm } from 'app/features/dashboard-scene/settings/variables/components/DataSourceVariableForm'; +import { StoreState } from 'app/types/store'; -import { StoreState } from '../../../types'; import { initialVariableEditorState } from '../editor/reducer'; import { getDatasourceVariableEditorState } from '../editor/selectors'; import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; diff --git a/public/app/features/variables/datasource/actions.ts b/public/app/features/variables/datasource/actions.ts index d2786309f44..8daf15ec8bc 100644 --- a/public/app/features/variables/datasource/actions.ts +++ b/public/app/features/variables/datasource/actions.ts @@ -2,8 +2,8 @@ import { chain } from 'lodash'; import { stringToJsRegex } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { changeVariableEditorExtended } from '../editor/reducer'; import { validateVariableSelectionState } from '../state/actions'; diff --git a/public/app/features/variables/editor/ConfirmDeleteModal.tsx b/public/app/features/variables/editor/ConfirmDeleteModal.tsx index 45f7da8c1b0..dbe8a48ea14 100644 --- a/public/app/features/variables/editor/ConfirmDeleteModal.tsx +++ b/public/app/features/variables/editor/ConfirmDeleteModal.tsx @@ -17,11 +17,13 @@ export function ConfirmDeleteModal({ varName, isOpen = false, onConfirm, onDismi isOpen={isOpen} onConfirm={onConfirm} onDismiss={onDismiss} - body={` - Are you sure you want to delete variable "${varName}"? - `} + body={t( + 'variables.confirm-delete-modal.body-delete-variable', + 'Are you sure you want to delete variable "{{variableToDelete}}"?', + { variableToDelete: varName } + )} modalClass={styles.modal} - confirmText="Delete" + confirmText={t('variables.confirm-delete-modal.confirmText-delete', 'Delete')} /> ); } diff --git a/public/app/features/variables/editor/VariableEditorContainer.tsx b/public/app/features/variables/editor/VariableEditorContainer.tsx index f810d4bfc88..07398d88f0b 100644 --- a/public/app/features/variables/editor/VariableEditorContainer.tsx +++ b/public/app/features/variables/editor/VariableEditorContainer.tsx @@ -5,8 +5,8 @@ import { bindActionCreators } from 'redux'; import { locationService } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; import { SettingsPageProps } from 'app/features/dashboard/components/DashboardSettings/types'; +import { StoreState, ThunkDispatch } from 'app/types/store'; -import { StoreState, ThunkDispatch } from '../../../types'; import { VariablesUnknownTable } from '../inspect/VariablesUnknownTable'; import { toKeyedAction } from '../state/keyedVariablesReducer'; import { getEditorVariables, getVariablesState } from '../state/selectors'; diff --git a/public/app/features/variables/editor/VariableEditorEditor.tsx b/public/app/features/variables/editor/VariableEditorEditor.tsx index a145ab0c72b..3a8e4b88c62 100644 --- a/public/app/features/variables/editor/VariableEditorEditor.tsx +++ b/public/app/features/variables/editor/VariableEditorEditor.tsx @@ -8,8 +8,8 @@ import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Button, HorizontalGroup, Icon, Themeable2, withTheme2 } from '@grafana/ui'; +import { StoreState, ThunkDispatch } from 'app/types/store'; -import { StoreState, ThunkDispatch } from '../../../types'; import { VariableHideSelect } from '../../dashboard-scene/settings/variables/components/VariableHideSelect'; import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend'; import { VariableTextAreaField } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField'; @@ -174,7 +174,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State> <VariableTextField value={this.props.editor.name} onChange={this.onNameChange} - name="Name" + name={t('variables.variable-editor-editor-un-connected.name-name', 'Name')} placeholder={t('variables.variable-editor-editor-un-connected.placeholder-variable-name', 'Variable name')} description={t( 'variables.variable-editor-editor-un-connected.description-template-variable-characters', @@ -188,7 +188,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State> /> <VariableTextField - name="Label" + name={t('variables.variable-editor-editor-un-connected.name-label', 'Label')} description={t( 'variables.variable-editor-editor-un-connected.description-optional-display-name', 'Optional display name' @@ -199,7 +199,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State> testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2} /> <VariableTextAreaField - name="Description" + name={t('variables.variable-editor-un-connected.name-description', 'Description')} value={variable.description ?? ''} placeholder={t( 'variables.variable-editor-editor-un-connected.placeholder-descriptive-text', diff --git a/public/app/features/variables/editor/VariableTypeSelect.tsx b/public/app/features/variables/editor/VariableTypeSelect.tsx index bb3b643cc32..2580017bf1c 100644 --- a/public/app/features/variables/editor/VariableTypeSelect.tsx +++ b/public/app/features/variables/editor/VariableTypeSelect.tsx @@ -2,6 +2,7 @@ import { PropsWithChildren, useMemo } from 'react'; import { SelectableValue, VariableType } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { t } from '@grafana/i18n'; import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField'; import { getVariableTypes } from '../utils'; @@ -17,7 +18,7 @@ export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>) return ( <VariableSelectField - name="Select variable type" + name={t('variables.variable-type-select.name-select-variable-type', 'Select variable type')} value={value} options={options} onChange={onChange} diff --git a/public/app/features/variables/editor/actions.ts b/public/app/features/variables/editor/actions.ts index 9f99d411b8a..827a951d56a 100644 --- a/public/app/features/variables/editor/actions.ts +++ b/public/app/features/variables/editor/actions.ts @@ -2,8 +2,8 @@ import { cloneDeep } from 'lodash'; import { TypedVariableModel, VariableType } from '@grafana/data'; import { locationService } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { variableAdapters } from '../adapters'; import { initInspect } from '../inspect/reducer'; import { createUsagesNetwork, transformUsagesToNetwork } from '../inspect/utils'; diff --git a/public/app/features/variables/editor/getVariableQueryEditor.tsx b/public/app/features/variables/editor/getVariableQueryEditor.tsx index 2e4177ef10d..ad96e78cca1 100644 --- a/public/app/features/variables/editor/getVariableQueryEditor.tsx +++ b/public/app/features/variables/editor/getVariableQueryEditor.tsx @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { DataQuery, DataSourceApi, DataSourceJsonData, QueryEditorProps, StandardVariableQuery } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; -import { importDataSourcePlugin } from '../../plugins/plugin_loader'; +import { importDataSourcePlugin } from '../../plugins/pluginLoader'; import { hasCustomVariableSupport, hasDatasourceVariableSupport, diff --git a/public/app/features/variables/inspect/NetworkGraph.tsx b/public/app/features/variables/inspect/NetworkGraph.tsx index 78b6977359c..cbcb413ea4d 100644 --- a/public/app/features/variables/inspect/NetworkGraph.tsx +++ b/public/app/features/variables/inspect/NetworkGraph.tsx @@ -1,4 +1,6 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; +import type { DataSet } from 'vis-data'; +import type { Network, Options, Data, Edge, Node } from 'vis-network'; import { GraphEdge, GraphNode } from './utils'; @@ -6,7 +8,6 @@ interface OwnProps { nodes: GraphNode[]; edges: GraphEdge[]; direction?: 'UD' | 'DU' | 'LR' | 'RL'; - onDoubleClick?: (node: string) => void; width?: string; height?: string; } @@ -17,28 +18,19 @@ interface DispatchProps {} export type Props = OwnProps & ConnectedProps & DispatchProps; -export const NetworkGraph = ({ nodes, edges, direction, width, height, onDoubleClick }: Props) => { - const network = useRef<any>(null); - const ref = useRef(null); - - const onNodeDoubleClick = useCallback( - (params: { nodes: string[] }) => { - if (onDoubleClick) { - onDoubleClick(params.nodes[0]); - } - }, - [onDoubleClick] - ); +export const NetworkGraph = ({ nodes, edges, direction, width, height }: Props) => { + const network = useRef<Network | null>(null); + const ref = useRef<HTMLDivElement>(null); useEffect(() => { const createNetwork = async () => { - // @ts-ignore no types yet for visjs-network - const visJs = await import(/* webpackChunkName: "visjs-network" */ 'visjs-network'); - const data = { - nodes: toVisNetworkNodes(visJs, nodes), - edges: toVisNetworkEdges(visJs, edges), + const visJs = await import(/* webpackChunkName: "vis-network" */ 'vis-network'); + const visData = await import(/* webpackChunkName: "vis-data" */ 'vis-data'); + const data: Data = { + nodes: toVisNetworkNodes(visData, nodes), + edges: toVisNetworkEdges(visData, edges), }; - const options = { + const options: Options = { width: '100%', height: '100%', autoResize: true, @@ -55,20 +47,13 @@ export const NetworkGraph = ({ nodes, edges, direction, width, height, onDoubleC dragNodes: false, }, }; - - network.current = new visJs.Network(ref.current, data, options); - network.current?.on('doubleClick', onNodeDoubleClick); + if (ref.current) { + network.current = new visJs.Network(ref.current, data, options); + } }; createNetwork(); - - return () => { - // unsubscribe event handlers - if (network.current) { - network.current.off('doubleClick'); - } - }; - }, [direction, edges, nodes, onNodeDoubleClick]); + }, [direction, edges, nodes]); return ( <div> @@ -77,15 +62,15 @@ export const NetworkGraph = ({ nodes, edges, direction, width, height, onDoubleC ); }; -function toVisNetworkNodes(visJs: any, nodes: GraphNode[]): any[] { +function toVisNetworkNodes(visData: any, nodes: GraphNode[]): DataSet<Node> { const nodesWithStyle = nodes.map((node) => ({ ...node, shape: 'box', })); - return new visJs.DataSet(nodesWithStyle); + return new visData.DataSet(nodesWithStyle); } -function toVisNetworkEdges(visJs: any, edges: GraphEdge[]): any[] { +function toVisNetworkEdges(visData: any, edges: GraphEdge[]): DataSet<Edge> { const edgesWithStyle = edges.map((edge) => ({ ...edge, arrows: 'to', dashes: true })); - return new visJs.DataSet(edgesWithStyle); + return new visData.DataSet(edgesWithStyle); } diff --git a/public/app/features/variables/interval/actions.ts b/public/app/features/variables/interval/actions.ts index f583eb7df57..3043da16e0d 100644 --- a/public/app/features/variables/interval/actions.ts +++ b/public/app/features/variables/interval/actions.ts @@ -1,6 +1,6 @@ import { rangeUtil } from '@grafana/data'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv'; import { validateVariableSelectionState } from '../state/actions'; diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx index 7a52df41e12..df8736b4935 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx @@ -6,7 +6,7 @@ import { bindActionCreators } from 'redux'; import { LoadingState, VariableOption, VariableWithMultiSupport, VariableWithOptions } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { ClickOutsideWrapper } from '@grafana/ui'; -import { StoreState, ThunkDispatch } from 'app/types'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { VARIABLE_PREFIX } from '../../constants'; import { isMulti } from '../../guard'; diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.ts b/public/app/features/variables/pickers/OptionsPicker/actions.ts index 43ef1fdec6e..e6049b37187 100644 --- a/public/app/features/variables/pickers/OptionsPicker/actions.ts +++ b/public/app/features/variables/pickers/OptionsPicker/actions.ts @@ -1,7 +1,7 @@ import { debounce, trim } from 'lodash'; import { isEmptyObject, containsSearchFilter, VariableWithOptions, VariableOption } from '@grafana/data'; -import { StoreState, ThunkDispatch, ThunkResult } from 'app/types'; +import { StoreState, ThunkDispatch, ThunkResult } from 'app/types/store'; import { variableAdapters } from '../../adapters'; import { hasOptions } from '../../guard'; diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index a10700237bd..5d6a56bdb5a 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -10,8 +10,8 @@ import { VariableSort, } from '@grafana/data'; import { QueryVariableEditorForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableForm'; +import { StoreState } from 'app/types/store'; -import { StoreState } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { initialVariableEditorState } from '../editor/reducer'; import { getQueryVariableEditorState } from '../editor/selectors'; diff --git a/public/app/features/variables/query/QueryVariableSortSelect.tsx b/public/app/features/variables/query/QueryVariableSortSelect.tsx index eb82c020bbd..be71f17e209 100644 --- a/public/app/features/variables/query/QueryVariableSortSelect.tsx +++ b/public/app/features/variables/query/QueryVariableSortSelect.tsx @@ -11,24 +11,60 @@ interface Props { testId?: string; } -const SORT_OPTIONS = [ - { label: 'Disabled', value: VariableSort.disabled }, - { label: 'Alphabetical (asc)', value: VariableSort.alphabeticalAsc }, - { label: 'Alphabetical (desc)', value: VariableSort.alphabeticalDesc }, - { label: 'Numerical (asc)', value: VariableSort.numericalAsc }, - { label: 'Numerical (desc)', value: VariableSort.numericalDesc }, - { label: 'Alphabetical (case-insensitive, asc)', value: VariableSort.alphabeticalCaseInsensitiveAsc }, - { label: 'Alphabetical (case-insensitive, desc)', value: VariableSort.alphabeticalCaseInsensitiveDesc }, - { label: 'Natural (asc)', value: VariableSort.naturalAsc }, - { label: 'Natural (desc)', value: VariableSort.naturalDesc }, -]; - export function QueryVariableSortSelect({ onChange, sort, testId }: PropsWithChildren<Props>) { - const value = useMemo(() => SORT_OPTIONS.find((o) => o.value === sort) ?? SORT_OPTIONS[0], [sort]); + const SORT_OPTIONS = useMemo( + () => [ + { + label: t('variables.query-variable-sort-select.sort_options.label.disabled', 'Disabled'), + value: VariableSort.disabled, + }, + { + label: t('variables.query-variable-sort-select.sort_options.label.alphabetical-asc', 'Alphabetical (asc)'), + value: VariableSort.alphabeticalAsc, + }, + { + label: t('variables.query-variable-sort-select.sort_options.label.alphabetical-desc', 'Alphabetical (desc)'), + value: VariableSort.alphabeticalDesc, + }, + { + label: t('variables.query-variable-sort-select.sort_options.label.numerical-asc', 'Numerical (asc)'), + value: VariableSort.numericalAsc, + }, + { + label: t('variables.query-variable-sort-select.sort_options.label.numerical-desc', 'Numerical (desc)'), + value: VariableSort.numericalDesc, + }, + { + label: t( + 'variables.query-variable-sort-select.sort_options.label.alphabetical-caseinsensitive-asc', + 'Alphabetical (case-insensitive, asc)' + ), + value: VariableSort.alphabeticalCaseInsensitiveAsc, + }, + { + label: t( + 'variables.query-variable-sort-select.sort_options.label.alphabetical-caseinsensitive-desc', + 'Alphabetical (case-insensitive, desc)' + ), + value: VariableSort.alphabeticalCaseInsensitiveDesc, + }, + { + label: t('variables.query-variable-sort-select.sort_options.label.natural-asc', 'Natural (asc)'), + value: VariableSort.naturalAsc, + }, + { + label: t('variables.query-variable-sort-select.sort_options.label.natural-desc', 'Natural (desc)'), + value: VariableSort.naturalDesc, + }, + ], + [] + ); + + const value = useMemo(() => SORT_OPTIONS.find((o) => o.value === sort) ?? SORT_OPTIONS[0], [sort, SORT_OPTIONS]); return ( <VariableSelectField - name="Sort" + name={t('variables.query-variable-sort-select.name-sort', 'Sort')} description={t( 'variables.query-variable-sort-select.description-values-variable', 'How to sort the values of this variable' diff --git a/public/app/features/variables/query/QueryVariableStaticOptions.tsx b/public/app/features/variables/query/QueryVariableStaticOptions.tsx new file mode 100644 index 00000000000..8e7abbb97a3 --- /dev/null +++ b/public/app/features/variables/query/QueryVariableStaticOptions.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { t, Trans } from '@grafana/i18n'; +import { QueryVariable } from '@grafana/scenes'; +import { Field, Stack, Switch } from '@grafana/ui'; +import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend'; +import { VariableOptionsInput } from 'app/features/dashboard-scene/settings/variables/components/VariableOptionsInput'; +import { VariableSelectField } from 'app/features/dashboard-scene/settings/variables/components/VariableSelectField'; + +export type StaticOptionsType = QueryVariable['state']['staticOptions']; +export type StaticOptionsOrderType = QueryVariable['state']['staticOptionsOrder']; + +interface QueryVariableStaticOptionsProps { + staticOptions: StaticOptionsType; + staticOptionsOrder: StaticOptionsOrderType; + onStaticOptionsChange: (staticOptions: StaticOptionsType) => void; + onStaticOptionsOrderChange: (staticOptionsOrder: StaticOptionsOrderType) => void; +} + +const SORT_OPTIONS = [ + { label: 'Before query values', value: 'before' }, + { label: 'After query values', value: 'after' }, + { label: 'Sorted with query values', value: 'sorted' }, +]; + +export function QueryVariableStaticOptions(props: QueryVariableStaticOptionsProps) { + const { staticOptions, onStaticOptionsChange, staticOptionsOrder, onStaticOptionsOrderChange } = props; + + const value = SORT_OPTIONS.find((o) => o.value === staticOptionsOrder) ?? SORT_OPTIONS[0]; + + const [areStaticOptionsEnabled, setAreStaticOptionsEnabled] = useState(!!staticOptions?.length); + + return ( + <> + <VariableLegend> + <Trans i18nKey="dashboard-scene.query-variable-editor-form.static-options-legend">Static options</Trans> + </VariableLegend> + <Stack direction="column" gap={2}> + <Field + noMargin + label={t('dashboard-scene.query-variable-editor-form.label-use-static-options', 'Use static options')} + description={t( + 'variables.query-variable-static-options.description', + 'Add custom options in addition to query results' + )} + > + <> + <Stack direction="column" gap={2}> + <Switch + data-testid={ + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle + } + value={areStaticOptionsEnabled} + onChange={(e) => { + if (e.currentTarget.checked) { + setAreStaticOptionsEnabled(true); + } else { + setAreStaticOptionsEnabled(false); + if (!!staticOptions?.length) { + onStaticOptionsChange(undefined); + } + } + }} + /> + + {areStaticOptionsEnabled && ( + <VariableOptionsInput width={60} options={staticOptions ?? []} onChange={onStaticOptionsChange} /> + )} + </Stack> + </> + </Field> + + {areStaticOptionsEnabled && ( + <VariableSelectField + name={t('dashboard-scene.query-variable-editor-form.label-static-options-sort', 'Static options sort')} + description={t( + 'variables.query-variable-static-options-sort-select.description-values-variable', + 'How to sort static options with query results' + )} + value={value} + options={SORT_OPTIONS} + onChange={(opt) => onStaticOptionsOrderChange(opt.value)} + testId={ + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown + } + width={25} + /> + )} + </Stack> + </> + ); +} diff --git a/public/app/features/variables/query/VariableQueryRunner.ts b/public/app/features/variables/query/VariableQueryRunner.ts index 2a21c3c1226..aae4819e01a 100644 --- a/public/app/features/variables/query/VariableQueryRunner.ts +++ b/public/app/features/variables/query/VariableQueryRunner.ts @@ -12,9 +12,9 @@ import { QueryVariableModel, ScopedVars, } from '@grafana/data'; +import { StoreState, ThunkDispatch } from 'app/types/store'; import { dispatch, getState } from '../../../store/store'; -import { StoreState, ThunkDispatch } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { runRequest } from '../../query/state/runRequest'; import { getLastKey, getVariable } from '../state/selectors'; diff --git a/public/app/features/variables/query/actions.test.tsx b/public/app/features/variables/query/actions.test.tsx index 72cfc067092..7a203bd4b77 100644 --- a/public/app/features/variables/query/actions.test.tsx +++ b/public/app/features/variables/query/actions.test.tsx @@ -69,7 +69,7 @@ const mocks: Record<string, any> = { setDataSourceSrv(mocks.dataSourceSrv as DataSourceSrv); -jest.mock('../../plugins/plugin_loader', () => ({ +jest.mock('../../plugins/pluginLoader', () => ({ importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(), })); diff --git a/public/app/features/variables/query/actions.ts b/public/app/features/variables/query/actions.ts index fe494c17560..90fff7f6a46 100644 --- a/public/app/features/variables/query/actions.ts +++ b/public/app/features/variables/query/actions.ts @@ -2,8 +2,8 @@ import { Subscription } from 'rxjs'; import { DataSourceRef } from '@grafana/data'; import { getDataSourceSrv, toDataQueryError } from '@grafana/runtime'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { getVariableQueryEditor } from '../editor/getVariableQueryEditor'; import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer'; import { getQueryVariableEditorState } from '../editor/selectors'; diff --git a/public/app/features/variables/query/operators.ts b/public/app/features/variables/query/operators.ts index c4bb25960bb..a119b4ebd97 100644 --- a/public/app/features/variables/query/operators.ts +++ b/public/app/features/variables/query/operators.ts @@ -10,8 +10,8 @@ import { PanelData, QueryVariableModel, } from '@grafana/data'; +import { ThunkDispatch } from 'app/types/store'; -import { ThunkDispatch } from '../../../types'; import { validateVariableSelectionState } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; import { getTemplatedRegex, toKeyedVariableIdentifier, toVariablePayload } from '../utils'; diff --git a/public/app/features/variables/state/actions.ts b/public/app/features/variables/state/actions.ts index fdbc54bbbba..ffc45b6b939 100644 --- a/public/app/features/variables/state/actions.ts +++ b/public/app/features/variables/state/actions.ts @@ -26,12 +26,13 @@ import { contextSrv } from 'app/core/services/context_srv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { store } from 'app/store/store'; +import { AppNotification } from 'app/types/appNotifications'; +import { ThunkResult, StoreState } from 'app/types/store'; import { createErrorNotification } from '../../../core/copy/appNotification'; import { appEvents } from '../../../core/core'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { Graph, Node } from '../../../core/utils/dag'; -import { AppNotification, StoreState, ThunkResult } from '../../../types'; import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv'; import { variableAdapters } from '../adapters'; diff --git a/public/app/features/variables/state/helpers.ts b/public/app/features/variables/state/helpers.ts index 9752d6ecf5a..163c6d7acf9 100644 --- a/public/app/features/variables/state/helpers.ts +++ b/public/app/features/variables/state/helpers.ts @@ -9,8 +9,9 @@ import { } from '@grafana/data'; import { VariableRefresh } from '@grafana/schema'; import { dashboardReducer } from 'app/features/dashboard/state/reducers'; +import { DashboardState } from 'app/types/dashboard'; +import { StoreState } from 'app/types/store'; -import { DashboardState, StoreState } from '../../../types'; import { VariableAdapter } from '../adapters'; import { NEW_VARIABLE_ID } from '../constants'; import { initialVariableModelState } from '../types'; diff --git a/public/app/features/variables/state/onTimeRangeUpdated.test.ts b/public/app/features/variables/state/onTimeRangeUpdated.test.ts index 528db0045ec..efed3f1b5ab 100644 --- a/public/app/features/variables/state/onTimeRangeUpdated.test.ts +++ b/public/app/features/variables/state/onTimeRangeUpdated.test.ts @@ -1,12 +1,12 @@ import { dateTime, TimeRange, VariableRefresh } from '@grafana/data'; import { config, DataSourceSrv } from '@grafana/runtime'; import * as runtime from '@grafana/runtime'; +import { DashboardState } from 'app/types/dashboard'; import { reduxTester } from '../../../../test/core/redux/reduxTester'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; import { appEvents } from '../../../core/core'; import { notifyApp } from '../../../core/reducers/appNotification'; -import { DashboardState } from '../../../types'; import { DashboardModel } from '../../dashboard/state/DashboardModel'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { TemplateSrv } from '../../templating/template_srv'; diff --git a/public/app/features/variables/state/selectors.ts b/public/app/features/variables/state/selectors.ts index d9334fd17c0..31e14318d87 100644 --- a/public/app/features/variables/state/selectors.ts +++ b/public/app/features/variables/state/selectors.ts @@ -1,9 +1,9 @@ import memoizeOne from 'memoize-one'; import { TypedVariableModel } from '@grafana/data'; +import { StoreState } from 'app/types/store'; import { getState } from '../../../store/store'; -import { StoreState } from '../../../types'; import { toStateKey } from '../utils'; import { getInitialTemplatingState, TemplatingState } from './reducers'; diff --git a/public/app/features/variables/state/templateVarsChangedInUrl.test.ts b/public/app/features/variables/state/templateVarsChangedInUrl.test.ts index 8e6f6432599..5d6828877c7 100644 --- a/public/app/features/variables/state/templateVarsChangedInUrl.test.ts +++ b/public/app/features/variables/state/templateVarsChangedInUrl.test.ts @@ -1,6 +1,7 @@ import { TypedVariableModel } from '@grafana/data'; +import { DashboardState } from 'app/types/dashboard'; +import { StoreState } from 'app/types/store'; -import { DashboardState, StoreState } from '../../../types'; import { PanelModel } from '../../dashboard/state/PanelModel'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { initialState } from '../../dashboard/state/reducers'; diff --git a/public/app/features/variables/textbox/TextBoxVariableEditor.tsx b/public/app/features/variables/textbox/TextBoxVariableEditor.tsx index 0ae6544764a..637056e03e0 100644 --- a/public/app/features/variables/textbox/TextBoxVariableEditor.tsx +++ b/public/app/features/variables/textbox/TextBoxVariableEditor.tsx @@ -30,7 +30,7 @@ export function TextBoxVariableEditor({ onPropChange, variable: { query } }: Pro </VariableLegend> <VariableTextField value={query} - name="Default value" + name={t('variables.text-box-variable-editor.name-default-value', 'Default value')} placeholder={t('variables.text-box-variable-editor.placeholder-default-value-if-any', 'default value, if any')} onChange={onChange} onBlur={onBlur} diff --git a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx index 260a57d972e..143cc6f128d 100644 --- a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx +++ b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx @@ -3,7 +3,7 @@ import { ChangeEvent, FocusEvent, KeyboardEvent, ReactElement, useCallback, useE import { TextBoxVariableModel, isEmptyObject } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Input } from '@grafana/ui'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { variableAdapters } from '../adapters'; import { VARIABLE_PREFIX } from '../constants'; diff --git a/public/app/features/variables/textbox/actions.ts b/public/app/features/variables/textbox/actions.ts index c80cc3dd271..0e3c249614f 100644 --- a/public/app/features/variables/textbox/actions.ts +++ b/public/app/features/variables/textbox/actions.ts @@ -1,6 +1,6 @@ import { UrlQueryValue } from '@grafana/data'; +import { ThunkResult } from 'app/types/store'; -import { ThunkResult } from '../../../types'; import { variableAdapters } from '../adapters'; import { setOptionFromUrl } from '../state/actions'; import { toKeyedAction } from '../state/keyedVariablesReducer'; diff --git a/public/app/features/variables/utils.ts b/public/app/features/variables/utils.ts index 4ab42c8ec5c..6908bb9ce57 100644 --- a/public/app/features/variables/utils.ts +++ b/public/app/features/variables/utils.ts @@ -12,9 +12,9 @@ import { } from '@grafana/data'; import { getTemplateSrv, locationService } from '@grafana/runtime'; import { safeStringifyValue } from 'app/core/utils/explore'; +import { StoreState } from 'app/types/store'; import { getState } from '../../store/store'; -import { StoreState } from '../../types'; import { TimeSrv } from '../dashboard/services/TimeSrv'; import { variableAdapters } from './adapters'; diff --git a/public/app/plugins/datasource/azuremonitor/azureMetadata/index.ts b/public/app/plugins/datasource/azuremonitor/azureMetadata/index.ts deleted file mode 100644 index 7514607b889..00000000000 --- a/public/app/plugins/datasource/azuremonitor/azureMetadata/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './resourceTypes'; -export * from './logsResourceTypes'; diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts index 2c59c4a38bb..88043e66dc2 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -3,7 +3,7 @@ import { CustomVariableModel } from '@grafana/data'; import { Context, createContext } from '../mocks/datasource'; import createMockQuery from '../mocks/query'; import { singleVariable } from '../mocks/variables'; -import { AzureLogsQuery, AzureMonitorQuery, AzureQueryType, AzureTracesQuery } from '../types'; +import { AzureLogsQuery, AzureMonitorQuery, AzureQueryType, AzureTracesQuery } from '../types/query'; import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource'; import FakeSchemaData from './mocks/schema'; diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts index cb08deeafcc..77733e2dfa3 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts @@ -6,17 +6,16 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import ResponseParser from '../azure_monitor/response_parser'; import { getCredentials } from '../credentials'; +import { AzureMonitorQuery, AzureQueryType } from '../types/query'; import { + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceInstanceSettings, AzureAPIResponse, AzureLogsVariable, - AzureMonitorDataSourceInstanceSettings, - AzureMonitorDataSourceJsonData, - AzureMonitorQuery, - AzureQueryType, + Workspace, DatasourceValidationResult, Subscription, - Workspace, -} from '../types'; +} from '../types/types'; import { interpolateVariable, routeNames } from '../utils/common'; import { transformMetadataToKustoSchema } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/mocks/schema.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/mocks/schema.ts index 40257cee20b..b7f3cc12aa5 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/mocks/schema.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/mocks/schema.ts @@ -1,4 +1,5 @@ -import { AzureLogAnalyticsMetadataTable, EngineSchema } from '../../types'; +import { AzureLogAnalyticsMetadataTable } from '../../types/logAnalyticsMetadata'; +import { EngineSchema } from '../../types/types'; export default class FakeSchemaData { static getLogAnalyticsFakeSchema() { diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts index 7f577f53d4f..8124207b449 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/utils.ts @@ -1,7 +1,7 @@ import { TypedVariableModel } from '@grafana/data'; -import { EngineSchema } from '../types'; import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata'; +import { EngineSchema } from '../types/types'; // matches (name):(type) = (defaultValue) // e.g. fromRangeStart:datetime = datetime(null) diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts index c5561e53be1..0b96116b304 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts @@ -13,7 +13,7 @@ import { Location, RawAzureResourceGroupItem, RawAzureResourceItem, -} from '../types'; +} from '../types/types'; // We want replace to just return the value as is in general/ // We declare this as a function so that we can overwrite it in each test diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts index cd8a21476c7..a4bb34dc5f9 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts @@ -6,16 +6,14 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import { getCredentials } from '../credentials'; import TimegrainConverter from '../time_grain_converter'; +import { AzureMetricQuery, AzureMonitorQuery, AzureQueryType } from '../types/query'; import { AzureAPIResponse, - AzureMetricQuery, AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceJsonData, AzureMonitorLocations, AzureMonitorMetricsMetadataResponse, AzureMonitorProvidersResponse, - AzureMonitorQuery, - AzureQueryType, DatasourceValidationResult, GetLogAnalyticsTableResponse, GetMetricMetadataQuery, @@ -27,7 +25,7 @@ import { MetricNamespace, Subscription, TablePlan, -} from '../types'; +} from '../types/types'; import { replaceTemplateVariables, routeNames } from '../utils/common'; import migrateQuery from '../utils/migrateQuery'; diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts index 42dacec1042..c4a00c1db8c 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts @@ -4,15 +4,16 @@ import { FetchResponse } from '@grafana/runtime'; import TimeGrainConverter from '../time_grain_converter'; import { + AzureAPIResponse, AzureMonitorLocalizedValue, AzureMonitorLocations, AzureMonitorMetricAvailabilityMetadata, AzureMonitorMetricsMetadataResponse, AzureMonitorOption, - AzureAPIResponse, Location, Subscription, -} from '../types'; +} from '../types/types'; + export default class ResponseParser { static parseResponseValues<T>( result: AzureAPIResponse<T>, diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts index 148a8a94baf..545c1648315 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts @@ -1,6 +1,7 @@ import { TemplateSrv } from '@grafana/runtime'; -import { AzureMonitorResource, GetMetricNamespacesQuery, GetMetricNamesQuery } from '../types'; +import { AzureMonitorResource } from '../types/query'; +import { GetMetricNamespacesQuery, GetMetricNamesQuery } from '../types/types'; export default class UrlBuilder { static buildResourceUri(templateSrv: TemplateSrv, resource: AzureMonitorResource, multipleResources?: boolean) { diff --git a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts index fc58c626fbf..c6c920f062d 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts @@ -7,7 +7,7 @@ import { createMockInstanceSetttings } from '../mocks/instanceSettings'; import createMockQuery from '../mocks/query'; import { createTemplateVariables } from '../mocks/utils'; import { multiVariable, singleVariable, subscriptionsVariable } from '../mocks/variables'; -import { AzureQueryType } from '../types'; +import { AzureQueryType } from '../types/query'; import AzureResourceGraphDatasource from './azure_resource_graph_datasource'; diff --git a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts index ac4f32e1986..67a6532deeb 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts @@ -3,20 +3,19 @@ import { filter, find, includes, startsWith } from 'lodash'; import { ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { resourceTypes } from '../azureMetadata'; +import { resourceTypes } from '../azureMetadata/resourceTypes'; import { ARGScope } from '../dataquery.gen'; +import { AzureMonitorQuery, AzureQueryType } from '../types/query'; import { AzureGetResourceNamesQuery, AzureGraphResponse, AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceJsonData, - AzureMonitorQuery, - AzureQueryType, AzureResourceGraphOptions, RawAzureResourceGroupItem, RawAzureResourceItem, RawAzureSubscriptionItem, -} from '../types'; +} from '../types/types'; import { interpolateVariable, replaceTemplateVariables, routeNames } from '../utils/common'; export default class AzureResourceGraphDatasource extends DataSourceWithBackend< diff --git a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx index 43b2a82ee43..b15705db73b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/ArgQueryEditor.tsx @@ -9,7 +9,8 @@ import { Combobox } from '@grafana/ui'; import { ARGScope } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; +import { AzureMonitorQuery } from '../../types/query'; +import { AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import { Field } from '../shared/Field'; import QueryField from './QueryField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/QueryField.tsx b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/QueryField.tsx index d6a69f3f3b4..fd06a327e1e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/QueryField.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { CodeEditor } from '@grafana/ui'; -import { AzureQueryEditorFieldProps } from '../../types'; +import { AzureQueryEditorFieldProps } from '../../types/types'; const QueryField = ({ query, onQueryChange }: AzureQueryEditorFieldProps) => { const onChange = useCallback( diff --git a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx index 16ceab1e26d..dfe75072933 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ArgQueryEditor/SubscriptionField.tsx @@ -5,7 +5,8 @@ import { Trans, t } from '@grafana/i18n'; import { FieldValidationMessage, MultiSelect } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorQuery, AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { AzureMonitorQuery } from '../../types/query'; +import { AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types/types'; import { findOptions } from '../../utils/common'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx index fc5966ccf5d..094780de576 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { Trans, t } from '@grafana/i18n'; import { Field, Switch, TextLink, useTheme2 } from '@grafana/ui'; -import { AzureMonitorDataSourceJsonData } from '../../types'; +import { AzureMonitorDataSourceJsonData } from '../../types/types'; export interface Props { options: AzureMonitorDataSourceJsonData; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx index 212b02739a9..6fc1c94ce81 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx @@ -13,7 +13,7 @@ import { AzureMonitorDataSourceSecureJsonData, AzureMonitorDataSourceSettings, Subscription, -} from '../../types'; +} from '../../types/types'; import { routeNames } from '../../utils/common'; import { MonitorConfig } from './MonitorConfig'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx index e6386e608fb..0bd33039bda 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx @@ -6,7 +6,7 @@ import { Trans, t } from '@grafana/i18n'; import { Select, Button, Field } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorDataSourceJsonData } from '../../types'; +import { AzureMonitorDataSourceJsonData } from '../../types/types'; export interface Props { options: AzureMonitorDataSourceJsonData; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx index a989a01056d..e8b3d77b52a 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx @@ -6,7 +6,7 @@ import { SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; import { getCredentials, updateCredentials } from '../../credentials'; -import { AzureMonitorDataSourceSettings } from '../../types'; +import { AzureMonitorDataSourceSettings } from '../../types/types'; import { AzureCredentialsForm, getAzureCloudOptions } from './AzureCredentialsForm'; import { BasicLogsToggle } from './BasicLogsToggle'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregationSection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregationSection.tsx index 2647546f0ef..4f3a1050a14 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregationSection.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/AggregationSection.tsx @@ -5,7 +5,8 @@ import { t } from '@grafana/i18n'; import { EditorField, EditorFieldGroup, EditorList, EditorRow } from '@grafana/plugin-ui'; import { BuilderQueryEditorReduceExpression } from '../../dataquery.gen'; -import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; +import { AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; import AggregateItem from './AggregateItem'; import { BuildAndUpdateOptions } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FilterSection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FilterSection.tsx index 1864cc14e5b..c3deae532a3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FilterSection.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FilterSection.tsx @@ -15,7 +15,8 @@ import { BuilderQueryEditorWhereExpressionItems, } from '../../dataquery.gen'; import Datasource from '../../datasource'; -import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; +import { AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; import { FilterItem } from './FilterItem'; import { BuildAndUpdateOptions } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FuzzySearch.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FuzzySearch.tsx index bceacb94a8c..6f000c1a923 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FuzzySearch.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/FuzzySearch.tsx @@ -10,7 +10,8 @@ import { BuilderQueryEditorWhereExpression, BuilderQueryEditorPropertyType, } from '../../dataquery.gen'; -import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; +import { AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; import { BuildAndUpdateOptions, removeExtraQuotes } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx index 629a5b8093e..5a3b1bca4fd 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/GroupBySection.tsx @@ -10,7 +10,8 @@ import { BuilderQueryEditorGroupByExpression, BuilderQueryEditorPropertyType, } from '../../dataquery.gen'; -import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; +import { AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; import { GroupByItem } from './GroupByItem'; import { BuildAndUpdateOptions } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx index 459f0cebae8..3015fa4179b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx @@ -17,12 +17,9 @@ import { } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { - AzureLogAnalyticsMetadataTable, - AzureLogAnalyticsMetadataColumn, - AzureMonitorQuery, - EngineSchema, -} from '../../types'; +import { AzureLogAnalyticsMetadataTable, AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; +import { EngineSchema } from '../../types/types'; import { AggregateSection } from './AggregationSection'; import { AzureMonitorKustoQueryBuilder } from './AzureMonitorKustoQueryBuilder'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx index f98a94cde13..5d88282f289 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx @@ -11,7 +11,8 @@ import { BuilderQueryEditorOrderByOptions, BuilderQueryEditorPropertyType, } from '../../dataquery.gen'; -import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; +import { AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; import { BuildAndUpdateOptions, inputFieldSize } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/TableSection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/TableSection.tsx index f01475f78e0..1b5a7e35aa4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/TableSection.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/TableSection.tsx @@ -6,12 +6,9 @@ import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/p import { Button, Select } from '@grafana/ui'; import { BuilderQueryEditorExpressionType, BuilderQueryEditorPropertyType } from '../../dataquery.gen'; -import { - AzureMonitorQuery, - AzureLogAnalyticsMetadataColumn, - AzureLogAnalyticsMetadataTable, - TablePlan, -} from '../../types'; +import { AzureLogAnalyticsMetadataColumn, AzureLogAnalyticsMetadataTable } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; +import { TablePlan } from '../../types/types'; import { BuildAndUpdateOptions, inputFieldSize } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/expressions.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/expressions.ts index bada9ae81b2..8a0dc1a0b37 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/expressions.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/expressions.ts @@ -1,4 +1,4 @@ -import { QueryEditorProperty, QueryEditorPropertyType } from '../../types'; +import { QueryEditorProperty, QueryEditorPropertyType } from '../../types/types'; export enum QueryEditorExpressionType { Property = 'property', diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts index 3c72a880040..062dac286f9 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts @@ -12,7 +12,8 @@ import { BuilderQueryEditorWhereExpression, BuilderQueryExpression, } from '../../dataquery.gen'; -import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; +import { AzureLogAnalyticsMetadataColumn } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery } from '../../types/query'; const DYNAMIC_TYPE_ARRAY_DELIMITER = '["`indexer`"]'; export const inputFieldSize = 20; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx index c68a707e5f5..204bc43d984 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/AzureCheatSheet.tsx @@ -17,14 +17,8 @@ import { } from '@grafana/ui'; import AzureLogAnalyticsDatasource from '../../azure_log_analytics/azure_log_analytics_datasource'; -import { - AzureMonitorQuery, - AzureQueryType, - Category, - CheatsheetQueries, - CheatsheetQuery, - DropdownCategories, -} from '../../types'; +import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { Category, CheatsheetQueries, CheatsheetQuery, DropdownCategories } from '../../types/types'; import { RawQuery } from './RawQuery'; import tokenizer from './syntax'; @@ -161,7 +155,10 @@ const AzureCheatSheet = (props: AzureCheatSheetProps) => { backspaceRemovesValue={true} placeholder={t('components.azure-cheat-sheet.placeholder-all-categories', 'All categories')} isClearable={true} - noOptionsMessage="Unable to list all categories" + noOptionsMessage={t( + 'components.azure-cheat-sheet.noOptionsMessage-unable-to-list-categories', + 'Unable to list all categories' + )} formatCreateLabel={(input: string) => `Category: ${input}`} isSearchable={true} isMulti={true} diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx index b0ba3eab206..a54949c3187 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsManagement.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { t } from '@grafana/i18n'; import { ConfirmModal, InlineField, RadioButtonGroup } from '@grafana/ui'; -import { AzureQueryEditorFieldProps } from '../../types'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import { setBasicLogsQuery, setDashboardTime, setKustoQuery } from './setQueryValue'; @@ -15,12 +15,12 @@ export function LogsManagement({ query, onQueryChange: onChange }: AzureQueryEdi <ConfirmModal isOpen={basicLogsAckOpen} title={t('components.logs-management.title-basic-logs-queries', 'Basic Logs Queries')} - body="Are you sure you want to switch to Basic Logs?" + body={t('components.logs-management.body-basic-logs-queries', 'Are you sure you want to switch to Basic Logs?')} description={t( 'components.logs-management.description-basic-logs-queries', 'Basic Logs queries incur cost based on the amount of data scanned.' )} - confirmText="Confirm" + confirmText={t('components.logs-management.confirmText-confirm', 'Confirm')} onConfirm={() => { setBasicLogsAckOpen(false); let updatedBasicLogsQuery = setBasicLogsQuery(query, true); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 1f2ad8a8f6f..2c7fbced3be 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -9,14 +9,9 @@ import { Alert, LinkButton, Space, Text, TextLink } from '@grafana/ui'; import { LogsEditorMode } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { - AzureMonitorErrorish, - AzureMonitorOption, - AzureMonitorQuery, - ResultFormat, - EngineSchema, - AzureLogAnalyticsMetadataTable, -} from '../../types'; +import { AzureLogAnalyticsMetadataTable } from '../../types/logAnalyticsMetadata'; +import { AzureMonitorQuery, ResultFormat } from '../../types/query'; +import { AzureMonitorErrorish, AzureMonitorOption, EngineSchema } from '../../types/types'; import { LogsQueryBuilder } from '../LogsQueryBuilder/LogsQueryBuilder'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx index e540de2bf21..679b2dad27f 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; -import { AzureQueryEditorFieldProps } from '../../types'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import { setKustoQuery } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx index 534f91c2fcb..f66e54ce88b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/TimeManagement.tsx @@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { InlineField, RadioButtonGroup, Select } from '@grafana/ui'; -import { AzureQueryEditorFieldProps } from '../../types'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import { setDashboardTime, setTimeColumn } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts index 31c7933c9ad..9662afd52af 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/setQueryValue.ts @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; -import { AzureMonitorQuery, ResultFormat } from '../../types'; +import { AzureMonitorQuery, ResultFormat } from '../../types/query'; export function setKustoQuery(query: AzureMonitorQuery, kustoQuery: string): AzureMonitorQuery { return { diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/useMigrations.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/useMigrations.ts index 9c5beb75421..32024ee2b7b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/useMigrations.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/useMigrations.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import Datasource from '../../datasource'; -import { AzureMonitorQuery } from '../../types'; +import { AzureMonitorQuery } from '../../types/query'; import { isGUIDish } from '../ResourcePicker/utils'; async function migrateWorkspaceQueryToResourceQuery( diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx index dfeb9b59867..abf4d91de90 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AdvancedResourcePicker.tsx @@ -7,7 +7,7 @@ import { AccessoryButton } from '@grafana/plugin-ui'; import { Input, Label, InlineField, Button, useStyles2 } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorResource } from '../../types'; +import { AzureMonitorResource } from '../../types/query'; export interface ResourcePickerProps<T> { resources: T[]; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.test.tsx index 35063802773..26724c82f09 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorOption } from '../../types'; +import { AzureMonitorOption } from '../../types/types'; import AggregationField from './AggregationField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx index 659718a0c15..e3eae750d76 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/AggregationField.tsx @@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types/types'; import { addValueToOptions } from '../../utils/common'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx index e4756276aab..6a7d3c22d37 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.tsx @@ -5,7 +5,8 @@ import { t } from '@grafana/i18n'; import { EditorList, AccessoryButton } from '@grafana/plugin-ui'; import { Select, Stack, MultiSelect } from '@grafana/ui'; -import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; +import { AzureMetricDimension, AzureMonitorQuery } from '../../types/query'; +import { AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types/types'; import { Field } from '../shared/Field'; import { setDimensionFilters } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx index 7852168636d..955e29f3ee8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/LegendFormatField.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { t } from '@grafana/i18n'; import { Input } from '@grafana/ui'; -import { AzureQueryEditorFieldProps } from '../../types'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import { Field } from '../shared/Field'; import { setLegendAlias } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.test.tsx index 06a08b1cdeb..95215db882c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorOption } from '../../types'; +import { AzureMonitorOption } from '../../types/types'; import MetricNameField from './MetricNameField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx index 92a68181347..4d94af7a366 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNameField.tsx @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types/types'; import { addValueToOptions } from '../../utils/common'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.test.tsx index dc1aab8b0bf..4187cfb6a88 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorOption } from '../../types'; +import { AzureMonitorOption } from '../../types/types'; import MetricNamespaceField from './MetricNamespaceField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx index e916cf064a1..53e436c1028 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricNamespaceField.tsx @@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types/types'; import { addValueToOptions } from '../../utils/common'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 6048ebca21e..04b046d31e4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,10 +1,11 @@ import { PanelData } from '@grafana/data'; import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/plugin-ui'; -import { multiResourceCompatibleTypes } from '../../azureMetadata'; +import { multiResourceCompatibleTypes } from '../../azureMetadata/resourceTypes'; import type Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish, AzureMonitorResource } from '../../types'; +import { AzureMonitorQuery, AzureMonitorResource } from '../../types/query'; +import { AzureMonitorOption, AzureMonitorErrorish } from '../../types/types'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; import { parseResourceDetails } from '../ResourcePicker/utils'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.test.tsx index 8722f5eee2c..bf427ff37aa 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorOption } from '../../types'; +import { AzureMonitorOption } from '../../types/types'; import TimeGrainField from './TimeGrainField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx index 22b9350b24c..dda333837e6 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TimeGrainField.tsx @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; import TimegrainConverter from '../../time_grain_converter'; -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types/types'; import { addValueToOptions } from '../../utils/common'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx index 201c378a88e..3037e17868d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/TopField.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { t } from '@grafana/i18n'; import { Input } from '@grafana/ui'; -import { AzureQueryEditorFieldProps } from '../../types'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import { Field } from '../shared/Field'; import { setTop } from './setQueryValue'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts index acf197d2e2c..47a6edf0a19 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.test.ts @@ -2,7 +2,8 @@ import { renderHook, waitFor } from '@testing-library/react'; import Datasource from '../../datasource'; import createMockDatasource from '../../mocks/datasource'; -import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType } from '../../types'; +import { AzureMetricQuery, AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorOption } from '../../types/types'; import { useMetricNames, diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts index f6ec8f2b8f1..82115c289c4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/dataHooks.ts @@ -4,7 +4,8 @@ import { rangeUtil } from '@grafana/data'; import Datasource from '../../datasource'; import TimegrainConverter from '../../time_grain_converter'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, AzureMonitorResource } from '../../types'; +import { AzureMonitorQuery, AzureMonitorResource } from '../../types/query'; +import { AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import { toOption } from '../../utils/common'; import { useAsyncState } from '../../utils/useAsyncState'; diff --git a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts index 6e1416913ce..05b17493ca3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/setQueryValue.ts @@ -1,4 +1,4 @@ -import { AzureMetricDimension, AzureMonitorQuery } from '../../types'; +import { AzureMetricDimension, AzureMonitorQuery } from '../../types/query'; export function setCustomNamespace(query: AzureMonitorQuery, selection: string | undefined): AzureMonitorQuery { if (query.azureMonitor?.customNamespace === selection) { diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx index 5a944e472c3..367c406b0cb 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.test.tsx @@ -8,7 +8,7 @@ import { selectors } from '../../e2e/selectors'; import createMockDatasource from '../../mocks/datasource'; import { invalidNamespaceError } from '../../mocks/errors'; import createMockQuery from '../../mocks/query'; -import { AzureQueryType, ResultFormat } from '../../types'; +import { AzureQueryType, ResultFormat } from '../../types/query'; import { selectOptionInTest } from '../../utils/testUtils'; import { createMockResourcePickerData } from '../LogsQueryEditor/mocks'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index dbf026c7c3e..54c1bf3dea8 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -9,13 +9,8 @@ import { Alert, CodeEditor, Space, TextLink } from '@grafana/ui'; import AzureMonitorDatasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { - AzureMonitorDataSourceJsonData, - AzureMonitorErrorish, - AzureMonitorOption, - AzureMonitorQuery, - AzureQueryType, -} from '../../types'; +import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorDataSourceJsonData, AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import useLastError from '../../utils/useLastError'; import ArgQueryEditor from '../ArgQueryEditor'; import LogsQueryEditor from '../LogsQueryEditor'; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx index 3a91a5d9998..035a918fc8d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryHeader.tsx @@ -8,7 +8,7 @@ import { Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui'; import { LogsEditorMode } from '../../dataquery.gen'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorQuery, AzureQueryType } from '../../types'; +import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; interface QueryTypeFieldProps { query: AzureMonitorQuery; @@ -19,11 +19,6 @@ interface QueryTypeFieldProps { app: CoreApp | undefined; } -const EDITOR_MODES = [ - { label: 'Builder', value: LogsEditorMode.Builder }, - { label: 'KQL', value: LogsEditorMode.Raw }, -]; - export const QueryHeader = ({ query, onQueryChange, @@ -37,6 +32,11 @@ export const QueryHeader = ({ const [showModeSwitchWarning, setShowModeSwitchWarning] = useState(false); const [pendingModeChange, setPendingModeChange] = useState<LogsEditorMode | null>(null); + const EDITOR_MODES = [ + { label: t('components.query-header.editor-modes.label-builder', 'Builder'), value: LogsEditorMode.Builder }, + { label: t('components.query-header.editor-modes.label-kql', 'KQL'), value: LogsEditorMode.Raw }, + ]; + const currentMode = query.azureLogAnalytics?.mode; const queryTypes: Array<{ value: AzureQueryType; label: string }> = [ @@ -113,10 +113,18 @@ export const QueryHeader = ({ title={t('components.query-header.title-switch-mode', 'Switch editor mode?')} body={ pendingModeChange === LogsEditorMode.Builder - ? 'Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?' - : 'Switching to KQL will discard your current builder settings. Are you sure?' + ? t( + 'components.query-header.body-switching-to-builder', + 'Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?' + ) + : t( + 'components.query-header.body-switching-to-kql', + 'Switching to KQL will discard your current builder settings. Are you sure?' + ) } - confirmText={`Switch to ${pendingModeChange === LogsEditorMode.Builder ? 'Builder' : 'KQL'}`} + confirmText={t('components.query-header.confirmText-switch-to', 'Switch to {{newMode}}', { + newMode: pendingModeChange === LogsEditorMode.Builder ? 'Builder' : 'KQL', + })} onConfirm={() => { if (pendingModeChange) { applyModeChange(pendingModeChange); diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts index 8b12487cd39..c2da635fcd0 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/usePreparedQuery.ts @@ -2,7 +2,7 @@ import deepEqual from 'fast-deep-equal'; import { defaults } from 'lodash'; import { useEffect, useMemo } from 'react'; -import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types'; +import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types/query'; import migrateQuery from '../../utils/migrateQuery'; const DEFAULT_QUERY = { diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx index 723603e968c..ddf88888f07 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourceField/ResourceField.tsx @@ -8,7 +8,8 @@ import { Button, Icon, Modal, useStyles2, IconName } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; -import { AzureQueryEditorFieldProps, AzureMonitorResource } from '../../types'; +import { AzureMonitorResource } from '../../types/query'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import ResourcePicker from '../ResourcePicker'; import getStyles from '../ResourcePicker/styles'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx index dffe37fb6ba..d16cfff82e9 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/AdvancedMulti.tsx @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n'; import { Collapse, Space } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorResource } from '../../types'; +import { AzureMonitorResource } from '../../types/query'; export interface ResourcePickerProps<T> { resources: T[]; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx index 52053c3702d..0f47899d98d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.tsx @@ -8,7 +8,7 @@ import { Alert, Button, LoadingPlaceholder, Modal, useStyles2, Space } from '@gr import { selectors } from '../../e2e/selectors'; import ResourcePickerData, { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; -import { AzureMonitorResource } from '../../types'; +import { AzureMonitorResource } from '../../types/query'; import messageFromError from '../../utils/messageFromError'; import AdvancedMulti from './AdvancedMulti'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts index c226f80366a..bcc9c9d2d03 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/utils.ts @@ -4,7 +4,7 @@ import { getTemplateSrv } from '@grafana/runtime'; import UrlBuilder from '../../azure_monitor/url_builder'; import { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData'; -import { AzureMonitorResource, AzureMonitorQuery } from '../../types'; +import { AzureMonitorQuery, AzureMonitorResource } from '../../types/query'; import { ResourceRow, ResourceRowGroup } from './types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx index c6b03769683..21f8dad755e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx @@ -11,7 +11,7 @@ import { Select, ButtonSelect, AsyncMultiSelect, getSelectStyles, useTheme2, Che import { AzureMonitorQuery, AzureQueryType, AzureTracesFilter } from '../../dataquery.gen'; import Datasource from '../../datasource'; -import { VariableOptionGroup } from '../../types'; +import { VariableOptionGroup } from '../../types/types'; import { addValueToOptions } from '../../utils/common'; export interface FilterProps { diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx index 9eef48a3a4d..61c571f6ca1 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx @@ -9,7 +9,7 @@ import { AzureQueryType } from '../../dataquery.gen'; import Datasource from '../../datasource'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorQuery } from '../../types'; +import { AzureMonitorQuery } from '../../types/query'; import { selectOptionInTest } from '../../utils/testUtils'; import Filters from './Filters'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx index 96d443343ac..785c9fd4e12 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.tsx @@ -6,7 +6,8 @@ import { t } from '@grafana/i18n'; import { EditorList } from '@grafana/plugin-ui'; import { Field } from '@grafana/ui'; -import { AzureQueryEditorFieldProps, AzureTracesFilter } from '../../types'; +import { AzureTracesFilter } from '../../types/query'; +import { AzureQueryEditorFieldProps } from '../../types/types'; import { makeRenderItem } from './Filter'; import { tablesSchema } from './consts'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx index b1067b3170e..a9ecc848a0b 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TraceTypeField.tsx @@ -5,7 +5,7 @@ import { t } from '@grafana/i18n'; import { MultiSelect } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types/types'; import { findOptions } from '../../utils/common'; import { Field } from '../shared/Field'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx index 686f8b9475c..405674a1665 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/TracesQueryEditor.tsx @@ -9,7 +9,8 @@ import { Input } from '@grafana/ui'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types'; +import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../../types/query'; +import { AzureMonitorErrorish, AzureMonitorOption } from '../../types/types'; import AdvancedResourcePicker from '../LogsQueryEditor/AdvancedResourcePicker'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts index ca71c5dd010..b901c7b800d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/setQueryValue.ts @@ -1,6 +1,6 @@ import { SelectableValue } from '@grafana/data'; -import { AzureMonitorQuery, AzureQueryType, AzureTracesFilter, ResultFormat } from '../../types'; +import { AzureMonitorQuery, AzureQueryType, AzureTracesFilter, ResultFormat } from '../../types/query'; // Used when switching from a traces exemplar query to a standard Azure Traces query export function setDefaultTracesQuery(query: AzureMonitorQuery): AzureMonitorQuery { diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx index b84c05f8641..1727cac59a7 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/GrafanaTemplateVariableFn.tsx @@ -5,7 +5,7 @@ import { InlineField, Input } from '@grafana/ui'; import DataSource from '../../datasource'; import { migrateStringQueriesToObjectQueries } from '../../grafanaTemplateVariableFns'; -import { AzureMonitorQuery, AzureQueryType } from '../../types'; +import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; const GrafanaTemplateVariableFnInput = ({ query, diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx index 9f6c8ac8e75..f14bb609a37 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx @@ -6,7 +6,7 @@ import { select, openMenu } from 'react-select-event'; import * as ui from '@grafana/ui'; import createMockDatasource from '../../mocks/datasource'; -import { AzureMonitorQuery, AzureQueryType } from '../../types'; +import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; import VariableEditor from './VariableEditor'; diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx index 850b87ee06f..ce722187c7d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx @@ -11,7 +11,8 @@ import UrlBuilder from '../../azure_monitor/url_builder'; import DataSource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { migrateQuery } from '../../grafanaTemplateVariableFns'; -import { AzureMonitorOption, AzureMonitorQuery, AzureQueryType } from '../../types'; +import { AzureMonitorQuery, AzureQueryType } from '../../types/query'; +import { AzureMonitorOption } from '../../types/types'; import useLastError from '../../utils/useLastError'; import ArgQueryEditor from '../ArgQueryEditor'; import LogsQueryEditor from '../LogsQueryEditor'; diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx index fe06dc7cee1..ed02f1b9ce3 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.test.tsx @@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data'; import createMockDatasource from '../../mocks/datasource'; import createMockQuery from '../../mocks/query'; -import { AzureMonitorQuery, ResultFormat } from '../../types'; +import { AzureMonitorQuery, ResultFormat } from '../../types/query'; import FormatAsField from './FormatAsField'; diff --git a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx index 34318ec1e74..240db475305 100644 --- a/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/shared/FormatAsField.tsx @@ -6,7 +6,8 @@ import { t } from '@grafana/i18n'; import { Select } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { FormatAsFieldProps, ResultFormat } from '../../types'; +import { ResultFormat } from '../../types/query'; +import { FormatAsFieldProps } from '../../types/types'; import { Field } from './Field'; diff --git a/public/app/plugins/datasource/azuremonitor/credentials.ts b/public/app/plugins/datasource/azuremonitor/credentials.ts index 44aeb3af365..7557ca5c2b4 100644 --- a/public/app/plugins/datasource/azuremonitor/credentials.ts +++ b/public/app/plugins/datasource/azuremonitor/credentials.ts @@ -8,7 +8,7 @@ import { } from '@grafana/azure-sdk'; import { config } from '@grafana/runtime'; -import { AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceSettings } from './types'; +import { AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceSettings } from './types/types'; export function getCredentials( options: AzureMonitorDataSourceSettings | AzureMonitorDataSourceInstanceSettings diff --git a/public/app/plugins/datasource/azuremonitor/datasource.test.ts b/public/app/plugins/datasource/azuremonitor/datasource.test.ts index 709e7c836d8..be090fa6254 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.test.ts @@ -2,7 +2,7 @@ import Datasource from './datasource'; import { createMockInstanceSetttings } from './mocks/instanceSettings'; import createMockQuery from './mocks/query'; import { singleVariable } from './mocks/variables'; -import { AzureQueryType } from './types'; +import { AzureQueryType } from './types/query'; jest.mock('@grafana/runtime', () => { return { diff --git a/public/app/plugins/datasource/azuremonitor/datasource.ts b/public/app/plugins/datasource/azuremonitor/datasource.ts index 917426a0169..db8acd9a103 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.ts @@ -18,7 +18,8 @@ import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analyti import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource'; import ResourcePickerData from './resourcePicker/resourcePickerData'; -import { AzureMonitorDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from './types'; +import { AzureMonitorQuery, AzureQueryType } from './types/query'; +import { AzureMonitorDataSourceJsonData } from './types/types'; import migrateAnnotation from './utils/migrateAnnotation'; import migrateQuery from './utils/migrateQuery'; import { VariableSupport } from './variables'; diff --git a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts index b4859079099..3e3945da4f2 100644 --- a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts +++ b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariableFns.ts @@ -1,6 +1,6 @@ import { isGUIDish } from './components/ResourcePicker/utils'; import DataSource from './datasource'; -import { AzureMonitorQuery, AzureQueryType } from './types'; +import { AzureMonitorQuery, AzureQueryType } from './types/query'; import { AppInsightsGroupByQuery, AppInsightsMetricNameQuery, diff --git a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts index 68743a6942e..d5db96929e2 100644 --- a/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts +++ b/public/app/plugins/datasource/azuremonitor/grafanaTemplateVariables.test.ts @@ -1,6 +1,6 @@ import { migrateQuery, migrateStringQueriesToObjectQueries } from './grafanaTemplateVariableFns'; import createMockDatasource from './mocks/datasource'; -import { AzureMonitorQuery, AzureQueryType } from './types'; +import { AzureMonitorQuery, AzureQueryType } from './types/query'; describe('migrateStringQueriesToObjectQueries', () => { const expectedMigrations: Array<{ input: string; output: AzureMonitorQuery }> = [ diff --git a/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json index d00dc5b60de..b1f951a843c 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/cs-CZ/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Použít tento dotaz", "label-categories": "Kategorie", "label-query-results": "Výsledky dotazu: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Všechny kategorie", "placeholder-search-logs": "Hledat dotazy protokolů", "text-loading": "Načítání…" @@ -161,6 +162,8 @@ "tooltip-limit": "Omezte počet vrácených řádků (výchozí hodnota je 1 000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Dotazy na základní protokoly jsou zpoplatněny na základě množství naskenovaných dat.", "label-logs": "Protokoly", "title-basic-logs-queries": "Dotazy na základní protokoly", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Tlačítko Spustit dotaz protokolů Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Spustit dotaz", "button-run-query": "Spustit dotaz", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Služba", "placeholder-service": "Služba…", "title-switch-mode": "Přepnout režim editoru?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json index 0f33925d236..7b5adf4401a 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/de-DE/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Diese Abfrage nutzen", "label-categories": "Kategorien", "label-query-results": "Abfrageergebnisse: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Alle Kategorien", "placeholder-search-logs": "Logs-Abfragen durchsuchen", "text-loading": "Wird geladen ..." @@ -161,6 +162,8 @@ "tooltip-limit": "Beschränken Sie die Zahl der zurückgegebenen Zeilen (Standard ist 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Für Abfragen von Basis-Logs fallen Kosten an, je nach der Menge der gescannten Daten.", "label-logs": "Logs", "title-basic-logs-queries": "Abfragen von Basis-Logs", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure-Logs starten Ihre Abfrage-Schaltfläche", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Starten Sie Ihre Abfrage", "button-run-query": "Abfrage ausführen", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Dienst", "placeholder-service": "Dienst …", "title-switch-mode": "Editor-Modus wechseln?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json index 1031611467c..933c052938c 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/en-US/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Use this query", "label-categories": "Categories", "label-query-results": "Query results: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "Unable to list all categories", "placeholder-all-categories": "All categories", "placeholder-search-logs": "Search Logs queries", "text-loading": "Loading..." @@ -161,6 +162,8 @@ "tooltip-limit": "Restrict the number of rows returned (default is 1000)." }, "logs-management": { + "body-basic-logs-queries": "Are you sure you want to switch to Basic Logs?", + "confirmText-confirm": "Confirm", "description-basic-logs-queries": "Basic Logs queries incur cost based on the amount of data scanned.", "label-logs": "Logs", "title-basic-logs-queries": "Basic Logs Queries", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure logs kick start your query button", + "body-switching-to-builder": "Switching to Builder will discard your current KQL query and clear the KQL editor. Are you sure?", + "body-switching-to-kql": "Switching to KQL will discard your current builder settings. Are you sure?", "button-kick-start-your-query": "Kick start your query", "button-run-query": "Run query", + "confirmText-switch-to": "Switch to {{newMode}}", + "editor-modes": { + "label-builder": "Builder", + "label-kql": "KQL" + }, "label-service": "Service", "placeholder-service": "Service...", "title-switch-mode": "Switch editor mode?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json index 2d2d1144cd1..385fa114747 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/es-ES/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Utilizar esta consulta", "label-categories": "Categorías", "label-query-results": "Resultados de la consulta: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Todas las categorías", "placeholder-search-logs": "Buscar consultas de logs", "text-loading": "Cargando..." @@ -161,6 +162,8 @@ "tooltip-limit": "Restrinja el número de filas devueltas (el valor predeterminado es 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Las consultas de logs básicos incurren en un coste basado en la cantidad de datos escaneados.", "label-logs": "Logs", "title-basic-logs-queries": "Consultas de logs básicos", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Los logs de Azure inician su botón de consulta", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Inicie su consulta", "button-run-query": "Ejecutar consulta", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Servicio", "placeholder-service": "Servicio...", "title-switch-mode": "¿Cambiar al modo de edición?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json index 431a645aede..9ba907b9072 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/fr-FR/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Utiliser cette requête", "label-categories": "Catégories", "label-query-results": "Résultats de la requête : {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Toutes les catégories", "placeholder-search-logs": "Rechercher des requêtes de journaux", "text-loading": "Chargement en cours..." @@ -161,6 +162,8 @@ "tooltip-limit": "Limitez le nombre de lignes renvoyées (la valeur par défaut est 1 000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Les requêtes de journaux de base entraînent des coûts en fonction de la quantité de données analysées.", "label-logs": "Journaux", "title-basic-logs-queries": "Requêtes de journaux de base", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Bouton Lancer votre requête des journaux Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Lancer votre requête", "button-run-query": "Exécuter la requête", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Service", "placeholder-service": "Service...", "title-switch-mode": "Passer au mode d’édition ?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json index bc16508e61e..4ccef7bbbfe 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/hu-HU/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Használja ezt a lekérdezést", "label-categories": "Kategóriák", "label-query-results": "Lekérdezés eredményei: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Összes kategória", "placeholder-search-logs": "Naplólekérdezések keresése", "text-loading": "Betöltés…" @@ -161,6 +162,8 @@ "tooltip-limit": "A visszaadott sorok számának korlátozása (az alapértelmezett érték az 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Az Alapvető naplók lekérdezései a beolvasott adatok mennyiségétől függően költséget eredményeznek.", "label-logs": "Naplók", "title-basic-logs-queries": "Alapvető naplók lekérdezései", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure-naplók előbeállításos lekérdezése gomb", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Előbeállításos lekérdezés", "button-run-query": "Lekérdezés futtatása", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Szolgáltatás", "placeholder-service": "Szolgáltatás…", "title-switch-mode": "Váltás szerkesztési módba?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json index 1c2e4c4eefb..3ab6b7e9853 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/id-ID/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Gunakan kueri ini", "label-categories": "Kategori", "label-query-results": "Hasil kueri: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Semua kategori", "placeholder-search-logs": "Pencarian kueri Log", "text-loading": "Memuat..." @@ -161,6 +162,8 @@ "tooltip-limit": "Batasi jumlah baris yang dikembalikan (default-nya adalah 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Kueri Log Dasar dikenakan biaya berdasarkan jumlah data yang dipindai.", "label-logs": "Log", "title-basic-logs-queries": "Kueri Log Dasar", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Tombol kueri untuk memulai menggunakan log Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Mulai kueri Anda", "button-run-query": "Jalankan kueri", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Layanan", "placeholder-service": "Melayani...", "title-switch-mode": "Beralih ke mode editor?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json index 129013718ef..ff698e97ada 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/it-IT/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Utilizza questa query", "label-categories": "Categorie", "label-query-results": "Risultati della query: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Tutte le categorie", "placeholder-search-logs": "Cerca query dei registri", "text-loading": "Caricamento in corso..." @@ -161,6 +162,8 @@ "tooltip-limit": "Limita il numero di righe restituite (il valore predefinito è 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Le query dei registri di base comportano un costo in base alla quantità di dati analizzati.", "label-logs": "Registri", "title-basic-logs-queries": "Query sui registri di base", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "I registri di Azure avviano il pulsante della query", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Avvia la query", "button-run-query": "Esegui query", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Servizio", "placeholder-service": "Servizio...", "title-switch-mode": "Cambiare modalità editor?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json index 672349f7891..165a50a63bc 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/ja-JP/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "このクエリを使用", "label-categories": "カテゴリ-", "label-query-results": "クエリ結果:{{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "すべてのカテゴリー", "placeholder-search-logs": "ログクエリを検索", "text-loading": "読み込み中..." @@ -161,6 +162,8 @@ "tooltip-limit": "返される行数を制限します(デフォルトは1000)。" }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "基本ログクエリを使用すると、スキャンされたデータ量に基づいてコストが発生します。", "label-logs": "ログ", "title-basic-logs-queries": "基本ログクエリ", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azureログのクエリ開始ボタン", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "クエリを開始", "button-run-query": "クエリの実行", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "サービス", "placeholder-service": "サービス...", "title-switch-mode": "エディターモードを切り替えますか?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json index 627ccff83a0..a5c0286ae6e 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/ko-KR/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "이 쿼리 사용", "label-categories": "범주", "label-query-results": "쿼리 결과: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "모든 범주", "placeholder-search-logs": "로그 쿼리 검색", "text-loading": "로딩 중..." @@ -161,6 +162,8 @@ "tooltip-limit": "반환되는 행 수를 제한합니다(기본값은 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "기본 로그 쿼리는 스캔된 데이터 양에 따라 비용이 발생합니다.", "label-logs": "로그", "title-basic-logs-queries": "기본 로그 쿼리", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure 로그 쿼리 시작 버튼", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "쿼리 시작하기", "button-run-query": "쿼리 실행", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "서비스", "placeholder-service": "서비스...", "title-switch-mode": "편집기 모드를 전환하시겠어요?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json index 7f99a7ed5c6..92b8e41d8ee 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/nl-NL/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Gebruik deze query", "label-categories": "Categorieën", "label-query-results": "Resultaten van query: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Alle categorieën", "placeholder-search-logs": "Querylogboeken zoeken", "text-loading": "Laden..." @@ -161,6 +162,8 @@ "tooltip-limit": "Beperk het aantal geretourneerde rijen (standaard is 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Basislogquery's brengen kosten met zich mee op basis van de hoeveelheid gescande gegevens.", "label-logs": "Logs", "title-basic-logs-queries": "Basislogquery's", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Knop Start query met Azure-logboeken", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Start je query", "button-run-query": "Query uitvoeren", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Service", "placeholder-service": "Service...", "title-switch-mode": "Veranderen van bewerkersmodus?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json index 1cbab6280a4..9eb4415ea63 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/pl-PL/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Użyj tego zapytania", "label-categories": "Kategorie", "label-query-results": "Wyniki zapytania: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Wszystkie kategorie", "placeholder-search-logs": "Wyszukaj zapytania dotyczące dzienników", "text-loading": "Ładowanie…" @@ -161,6 +162,8 @@ "tooltip-limit": "Ogranicz liczbę zwracanych wierszy (domyślnie 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Zapytania dotyczące podstawowych dzienników wiążą się z kosztami w zależności od ilości przeskanowanych danych.", "label-logs": "Logi", "title-basic-logs-queries": "Zapytania dotyczące podstawowych dzienników", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Przycisk Uruchom zapytanie dotyczący dzienników Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Uruchom zapytanie", "button-run-query": "Uruchom zapytanie", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Usługa", "placeholder-service": "Usługa…", "title-switch-mode": "Przełączyć tryb edytora?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json index 0c41247797d..ab362edc573 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/pt-BR/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Usar esta consulta", "label-categories": "Categorias", "label-query-results": "Resultados da consulta: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Todas as categorias", "placeholder-search-logs": "Fazer buscas nas consultas de logs", "text-loading": "Carregando..." @@ -161,6 +162,8 @@ "tooltip-limit": "Restrinja a quantidade de linhas retornadas (o valor padrão é 1.000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "As consultas de logs básicos incorrem em custos com base na quantidade de dados verificados.", "label-logs": "Logs", "title-basic-logs-queries": "Consultas de logs básicos", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Botão de iniciar consulta de logs do Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Iniciar sua consulta", "button-run-query": "Executar consulta", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Serviço", "placeholder-service": "Serviço...", "title-switch-mode": "Deseja trocar para o modo de edição?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json index b92e3e7489f..42424f61c2e 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/pt-PT/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Utilizar esta consulta", "label-categories": "Categorias", "label-query-results": "Resultados da consulta: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Todas as categorias", "placeholder-search-logs": "Pesquisar consultas de registos", "text-loading": "A carregar..." @@ -161,6 +162,8 @@ "tooltip-limit": "Restrinja o número de linhas devolvidas (a predefinição é 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "As consultas de registos básicos incorrem em custos com base na quantidade de dados lidos.", "label-logs": "Registos", "title-basic-logs-queries": "Consultas de registos básicos", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Botão de início de consulta de registos do Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Dê início à sua consulta", "button-run-query": "Executar consulta", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Serviço", "placeholder-service": "Serviço...", "title-switch-mode": "Mudar o modo de editor?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json index bb7916e57c5..fc75819703a 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/ru-RU/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Использовать запрос", "label-categories": "Категории", "label-query-results": "Результатов запроса: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Все категории", "placeholder-search-logs": "Поиск запросов журналов", "text-loading": "Загрузка…" @@ -161,6 +162,8 @@ "tooltip-limit": "Установите ограничение по количеству возвращаемых строк (по умолчанию — 1000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Запросы базовых журналов являются платными. Затраты зависят от объема сканируемых данных.", "label-logs": "Журналы", "title-basic-logs-queries": "Запросы базовых журналов", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Кнопка запуска запроса журналов Azure", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Запустить запрос", "button-run-query": "Выполнить запрос", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Служба", "placeholder-service": "Служба...", "title-switch-mode": "Переключить режим редактора?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json index 3c7f8f697d1..c190317bfb2 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/sv-SE/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Använd denna fråga", "label-categories": "Kategorier", "label-query-results": "Resultat av fråga: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Alla kategorier", "placeholder-search-logs": "Sök loggfrågor", "text-loading": "Laddar …" @@ -161,6 +162,8 @@ "tooltip-limit": "Begränsa antalet rader som returneras (standard är 1 000)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Grundläggande loggfrågor medför kostnader baserat på mängden data som skannas.", "label-logs": "Loggar", "title-basic-logs-queries": "Grundläggande loggfrågor", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Knapp för att kickstarta Azure-loggfrågor", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Kickstarta din fråga", "button-run-query": "Kör fråga", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Service", "placeholder-service": "Service …", "title-switch-mode": "Växla redigeringsläge?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json index b4f242b9bf4..81e42162930 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/tr-TR/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "Bu sorguyu kullanın", "label-categories": "Kategoriler", "label-query-results": "Sorgu sonuçları: {{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "Tüm kategoriler", "placeholder-search-logs": "Günlük sorgusu ara", "text-loading": "Yükleniyor..." @@ -161,6 +162,8 @@ "tooltip-limit": "Döndürülen satır sayısını kısıtlayın (varsayılan 1000'dir)." }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "Temel Günlük sorguları, taranan veri miktarına bağlı olarak maliyet doğurur.", "label-logs": "Günlük kayıtları", "title-basic-logs-queries": "Temel Günlük Sorguları", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure günlükleri sorgu hızlı başlatma düğmesi", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "Sorgunuzu hızlı başlatın", "button-run-query": "Sorgu çalıştır", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "Hizmet", "placeholder-service": "Hizmet...", "title-switch-mode": "Düzenleyici moduna geçilsin mi?" diff --git a/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json index ae72a995b28..0d010087fb3 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/zh-Hans/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "使用此查询", "label-categories": "类别", "label-query-results": "查询结果:{{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "所有类别", "placeholder-search-logs": "在日志查询中搜索", "text-loading": "加载中..." @@ -161,6 +162,8 @@ "tooltip-limit": "限制返回的行数(默认为 1000)。" }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "基本日志查询根据扫描的数据量产生费用。", "label-logs": "日志", "title-basic-logs-queries": "基本日志查询", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure 日志启动查询按钮", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "启动您的查询", "button-run-query": "运行查询", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "服务", "placeholder-service": "服务...", "title-switch-mode": "切换编辑器模式?" @@ -223,7 +233,7 @@ "title-error-occurred": "从 Azure Monitor 请求资源时发生错误" }, "scope-selector": { - "label": "" + "label": "范围" }, "search": { "aria-label-resource-search": "资源搜索", diff --git a/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json b/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json index d265ab34f07..b24ba7c624c 100644 --- a/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json +++ b/public/app/plugins/datasource/azuremonitor/locales/zh-Hant/grafana-azure-monitor-datasource.json @@ -52,6 +52,7 @@ "button-use-query": "使用此查詢", "label-categories": "分類", "label-query-results": "查詢結果:{{numResults}}", + "noOptionsMessage-unable-to-list-categories": "", "placeholder-all-categories": "所有分類", "placeholder-search-logs": "搜尋「紀錄查詢」", "text-loading": "正在載入…" @@ -161,6 +162,8 @@ "tooltip-limit": "限制傳回的列數(預設為 1000)。" }, "logs-management": { + "body-basic-logs-queries": "", + "confirmText-confirm": "", "description-basic-logs-queries": "基本紀錄查詢會根據掃描的資料量產生費用。", "label-logs": "紀錄", "title-basic-logs-queries": "基本紀錄查詢", @@ -194,8 +197,15 @@ }, "query-header": { "aria-label-kick-start": "Azure 紀錄會啟動您的查詢按鈕", + "body-switching-to-builder": "", + "body-switching-to-kql": "", "button-kick-start-your-query": "啟動您的查詢", "button-run-query": "執行查詢", + "confirmText-switch-to": "", + "editor-modes": { + "label-builder": "", + "label-kql": "" + }, "label-service": "服務", "placeholder-service": "服務…", "title-switch-mode": "要切換編輯器模式嗎?" @@ -223,7 +233,7 @@ "title-error-occurred": "從 Azure Monitor 請求資源時發生錯誤" }, "scope-selector": { - "label": "" + "label": "範圍" }, "search": { "aria-label-resource-search": "資源搜尋", diff --git a/public/app/plugins/datasource/azuremonitor/mocks/argResourcePickerResponse.ts b/public/app/plugins/datasource/azuremonitor/mocks/argResourcePickerResponse.ts index da06ff8236d..875736cfb69 100644 --- a/public/app/plugins/datasource/azuremonitor/mocks/argResourcePickerResponse.ts +++ b/public/app/plugins/datasource/azuremonitor/mocks/argResourcePickerResponse.ts @@ -3,7 +3,7 @@ import { RawAzureResourceGroupItem, RawAzureResourceItem, RawAzureSubscriptionItem, -} from '../types'; +} from '../types/types'; export const createMockARGSubscriptionResponse = (): AzureGraphResponse<RawAzureSubscriptionItem[]> => ({ data: [ diff --git a/public/app/plugins/datasource/azuremonitor/mocks/datasource.ts b/public/app/plugins/datasource/azuremonitor/mocks/datasource.ts index 893e1123490..e8cdd539fcb 100644 --- a/public/app/plugins/datasource/azuremonitor/mocks/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/mocks/datasource.ts @@ -1,7 +1,7 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import Datasource from '../datasource'; -import { AzureMonitorDataSourceInstanceSettings } from '../types'; +import { AzureMonitorDataSourceInstanceSettings } from '../types/types'; import { createMockInstanceSetttings } from './instanceSettings'; import { DeepPartial } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/mocks/datasourceSettings.ts b/public/app/plugins/datasource/azuremonitor/mocks/datasourceSettings.ts index ec558625438..9b2b5a6daac 100644 --- a/public/app/plugins/datasource/azuremonitor/mocks/datasourceSettings.ts +++ b/public/app/plugins/datasource/azuremonitor/mocks/datasourceSettings.ts @@ -1,6 +1,6 @@ import { KeyValue } from '@grafana/data'; -import { AzureMonitorDataSourceSettings } from '../types'; +import { AzureMonitorDataSourceSettings } from '../types/types'; import { DeepPartial } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/mocks/instanceSettings.ts b/public/app/plugins/datasource/azuremonitor/mocks/instanceSettings.ts index 9c984c870a9..fd0acf35979 100644 --- a/public/app/plugins/datasource/azuremonitor/mocks/instanceSettings.ts +++ b/public/app/plugins/datasource/azuremonitor/mocks/instanceSettings.ts @@ -1,6 +1,6 @@ import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; -import { AzureMonitorDataSourceInstanceSettings } from '../types'; +import { AzureMonitorDataSourceInstanceSettings } from '../types/types'; import { DeepPartial, mapPartialArrayObject } from './utils'; diff --git a/public/app/plugins/datasource/azuremonitor/mocks/query.ts b/public/app/plugins/datasource/azuremonitor/mocks/query.ts index fab83f3fdcd..6fbfb66b365 100644 --- a/public/app/plugins/datasource/azuremonitor/mocks/query.ts +++ b/public/app/plugins/datasource/azuremonitor/mocks/query.ts @@ -1,4 +1,4 @@ -import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../types'; +import { AzureMonitorQuery, AzureQueryType, ResultFormat } from '../types/query'; export default function createMockQuery(overrides?: Partial<AzureMonitorQuery>): AzureMonitorQuery { return { diff --git a/public/app/plugins/datasource/azuremonitor/module.test.ts b/public/app/plugins/datasource/azuremonitor/module.test.ts index 06a55850647..117ded762ce 100644 --- a/public/app/plugins/datasource/azuremonitor/module.test.ts +++ b/public/app/plugins/datasource/azuremonitor/module.test.ts @@ -2,7 +2,7 @@ import { DashboardLoadedEvent } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import './module'; -import { AzureMonitorQuery } from './types'; +import { AzureMonitorQuery } from './types/query'; jest.mock('@grafana/runtime', () => { return { diff --git a/public/app/plugins/datasource/azuremonitor/module.ts b/public/app/plugins/datasource/azuremonitor/module.ts index 654ac1ff5a4..b723a0bd50c 100644 --- a/public/app/plugins/datasource/azuremonitor/module.ts +++ b/public/app/plugins/datasource/azuremonitor/module.ts @@ -7,9 +7,14 @@ import AzureMonitorQueryEditor from './components/QueryEditor'; import Datasource from './datasource'; import pluginJson from './plugin.json'; import { trackAzureMonitorDashboardLoaded } from './tracking'; -import { AzureMonitorQuery, AzureMonitorDataSourceJsonData, AzureQueryType, ResultFormat } from './types'; +import { AzureMonitorQuery, AzureQueryType, ResultFormat } from './types/query'; +import { AzureMonitorDataSourceJsonData } from './types/types'; -initPluginTranslations(pluginJson.id); +// don't load plugin translations in test environments +// we don't use them anyway, and top-level await won't work currently in jest +if (process.env.NODE_ENV !== 'test') { + await initPluginTranslations(pluginJson.id); +} export const plugin = new DataSourcePlugin<Datasource, AzureMonitorQuery, AzureMonitorDataSourceJsonData>(Datasource) .setConfigEditor(ConfigEditor) diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 7ff99e186da..70fc99f5487 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -13,14 +13,14 @@ "@grafana/ui": "12.1.0-pre", "@kusto/monaco-kusto": "^10.0.0", "fast-deep-equal": "^3.1.3", - "i18next": "^24.0.0", + "i18next": "^25.0.0", "immer": "10.1.1", "lodash": "4.17.21", "monaco-editor": "0.34.1", "prismjs": "1.30.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" @@ -33,7 +33,7 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", @@ -41,16 +41,16 @@ "i18next-parser": "9.3.0", "react-select-event": "5.5.1", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "i18n-extract": "i18next --config locales/i18next-parser.config.cjs" }, "packageManager": "yarn@4.9.2" diff --git a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts index 0243f717d52..9bdf757a5c3 100644 --- a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts +++ b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts @@ -1,4 +1,4 @@ -import { resourceTypes } from '../azureMetadata'; +import { resourceTypes } from '../azureMetadata/resourceTypes'; import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource'; import { ResourceRowType } from '../components/ResourcePicker/types'; import { @@ -8,7 +8,7 @@ import { } from '../mocks/argResourcePickerResponse'; import createMockDatasource from '../mocks/datasource'; import { createMockInstanceSetttings } from '../mocks/instanceSettings'; -import { AzureGraphResponse } from '../types'; +import { AzureGraphResponse } from '../types/types'; import ResourcePickerData from './resourcePickerData'; diff --git a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts index b3cd9e7a498..35bf3933f1b 100644 --- a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts @@ -1,6 +1,7 @@ import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime'; -import { logsResourceTypes, resourceTypeDisplayNames, resourceTypes } from '../azureMetadata'; +import { logsResourceTypes } from '../azureMetadata/logsResourceTypes'; +import { resourceTypeDisplayNames, resourceTypes } from '../azureMetadata/resourceTypes'; import AzureMonitorDatasource from '../azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types'; @@ -12,14 +13,13 @@ import { parseResourceURI, resourceToString, } from '../components/ResourcePicker/utils'; +import { AzureMonitorQuery, AzureMonitorResource } from '../types/query'; import { AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceJsonData, - AzureMonitorResource, - AzureMonitorQuery, AzureResourceSummaryItem, RawAzureResourceItem, -} from '../types'; +} from '../types/types'; const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).join(','); diff --git a/public/app/plugins/datasource/azuremonitor/types/index.ts b/public/app/plugins/datasource/azuremonitor/types/index.ts deleted file mode 100644 index 06ced0cd13a..00000000000 --- a/public/app/plugins/datasource/azuremonitor/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './logAnalyticsMetadata'; -export * from './query'; -export * from './types'; diff --git a/public/app/plugins/datasource/azuremonitor/utils/common.ts b/public/app/plugins/datasource/azuremonitor/utils/common.ts index 3d47b161f5c..00472a6cf94 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/common.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/common.ts @@ -3,7 +3,7 @@ import { map } from 'lodash'; import { ScopedVars, SelectableValue, VariableWithMultiSupport } from '@grafana/data'; import { TemplateSrv, VariableInterpolation } from '@grafana/runtime'; -import { AzureMonitorOption, VariableOptionGroup } from '../types'; +import { AzureMonitorOption, VariableOptionGroup } from '../types/types'; export const hasOption = (options: AzureMonitorOption[], value: string): boolean => options.some((v) => (v.options ? hasOption(v.options, value) : v.value === value)); diff --git a/public/app/plugins/datasource/azuremonitor/utils/messageFromError.ts b/public/app/plugins/datasource/azuremonitor/utils/messageFromError.ts index 2a7236df003..3fc3c36f735 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/messageFromError.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/messageFromError.ts @@ -1,6 +1,6 @@ import { isValidElement } from 'react'; -import { AzureMonitorErrorish } from '../types'; +import { AzureMonitorErrorish } from '../types/types'; export function messageFromElement(error: AzureMonitorErrorish): AzureMonitorErrorish | undefined { if (isValidElement(error)) { diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts index bfd74252441..b1642f250e2 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.test.ts @@ -1,6 +1,6 @@ import { AnnotationQuery } from '@grafana/data'; -import { AzureMonitorQuery, AzureQueryType } from '../types'; +import { AzureMonitorQuery, AzureQueryType } from '../types/query'; import migrateAnnotation from './migrateAnnotation'; diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts index 31247ec7a78..ce37019e15f 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateAnnotation.ts @@ -1,6 +1,6 @@ import { AnnotationQuery } from '@grafana/data'; -import { AzureMonitorQuery, AzureQueryType } from '../types'; +import { AzureMonitorQuery, AzureQueryType } from '../types/query'; // The old Angular annotations editor put some properties (rawQuery, workspace, subscription) // on the root annotation object, rather than down in the 'targets' query value diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts index e46be675f8f..7e5e1769660 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.test.ts @@ -1,4 +1,4 @@ -import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType, ResultFormat } from '../types'; +import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType, ResultFormat } from '../types/query'; import migrateQuery from './migrateQuery'; diff --git a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts index feb22d424e3..6ebf853d8fa 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/migrateQuery.ts @@ -5,7 +5,7 @@ import { } from '../components/MetricsQueryEditor/setQueryValue'; import { parseResourceDetails } from '../components/ResourcePicker/utils'; import TimegrainConverter from '../time_grain_converter'; -import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types'; +import { AzureMetricDimension, AzureMonitorQuery, AzureQueryType } from '../types/query'; const OLD_DEFAULT_DROPDOWN_VALUE = 'select'; diff --git a/public/app/plugins/datasource/azuremonitor/utils/useLastError.ts b/public/app/plugins/datasource/azuremonitor/utils/useLastError.ts index 1c26117b2b2..00a6f289062 100644 --- a/public/app/plugins/datasource/azuremonitor/utils/useLastError.ts +++ b/public/app/plugins/datasource/azuremonitor/utils/useLastError.ts @@ -1,6 +1,6 @@ import { useState, useCallback, useMemo } from 'react'; -import { AzureMonitorErrorish } from '../types'; +import { AzureMonitorErrorish } from '../types/types'; import { messageFromElement } from './messageFromError'; diff --git a/public/app/plugins/datasource/azuremonitor/variables.test.ts b/public/app/plugins/datasource/azuremonitor/variables.test.ts index dcec7ff8005..55102e3fcfa 100644 --- a/public/app/plugins/datasource/azuremonitor/variables.test.ts +++ b/public/app/plugins/datasource/azuremonitor/variables.test.ts @@ -4,7 +4,7 @@ import { DataQueryRequest, toDataFrame } from '@grafana/data'; import createMockDatasource from './mocks/datasource'; import { invalidSubscriptionError } from './mocks/errors'; -import { AzureMonitorQuery, AzureQueryType } from './types'; +import { AzureMonitorQuery, AzureQueryType } from './types/query'; import { VariableSupport } from './variables'; jest.mock('@grafana/runtime', () => ({ diff --git a/public/app/plugins/datasource/azuremonitor/variables.ts b/public/app/plugins/datasource/azuremonitor/variables.ts index 2d6b3d73cb8..5477def0e2d 100644 --- a/public/app/plugins/datasource/azuremonitor/variables.ts +++ b/public/app/plugins/datasource/azuremonitor/variables.ts @@ -15,8 +15,9 @@ import { parseResourceURI } from './components/ResourcePicker/utils'; import VariableEditor from './components/VariableEditor/VariableEditor'; import DataSource from './datasource'; import { migrateQuery } from './grafanaTemplateVariableFns'; -import { AzureMonitorQuery, AzureQueryType, RawAzureResourceItem } from './types'; +import { AzureMonitorQuery, AzureQueryType } from './types/query'; import { GrafanaTemplateVariableQuery } from './types/templateVariables'; +import { RawAzureResourceItem } from './types/types'; import messageFromError from './utils/messageFromError'; export function parseResourceNamesAsTemplateVariable(resources: RawAzureResourceItem[], metricNamespace?: string) { diff --git a/public/app/plugins/datasource/azuremonitor/webpack.config.ts b/public/app/plugins/datasource/azuremonitor/webpack.config.ts index c831a42f307..875ef0a4893 100644 --- a/public/app/plugins/datasource/azuremonitor/webpack.config.ts +++ b/public/app/plugins/datasource/azuremonitor/webpack.config.ts @@ -1,9 +1,9 @@ import type { Configuration } from 'webpack'; import { merge } from 'webpack-merge'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; -const config = async (env: Record<string, unknown>): Promise<Configuration> => { +const config = async (env: Env): Promise<Configuration> => { const baseConfig = await grafanaConfig(env); return merge(baseConfig, { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx index 6925e724d68..cb754445570 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationQueryEditor.tsx @@ -10,10 +10,9 @@ import CloudMonitoringDatasource from '../datasource'; import { TimeSeriesList, CloudMonitoringQuery, QueryType } from '../types/query'; import { CloudMonitoringOptions } from '../types/types'; +import { AnnotationsHelp } from './AnnotationsHelp'; import { MetricQueryEditor, defaultTimeSeriesList } from './MetricQueryEditor'; -import { AnnotationsHelp } from './'; - export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>; export const defaultQuery: (datasource: CloudMonitoringDatasource) => TimeSeriesList = (datasource) => ({ diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx index 69801b0c976..a3c1d9372b6 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx @@ -4,7 +4,7 @@ import { Stack, Switch } from '@grafana/ui'; import { GRAPH_PERIODS } from '../constants'; -import { PeriodSelect } from './index'; +import { PeriodSelect } from './PeriodSelect'; export interface Props { refId: string; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index 7b701df34da..d0578dea9d4 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -11,12 +11,10 @@ import { selectors } from '../e2e/selectors'; import { CloudMonitoringQuery, PromQLQuery, QueryType, SLOQuery } from '../types/query'; import { CloudMonitoringOptions } from '../types/types'; -import { defaultTimeSeriesList, defaultTimeSeriesQuery } from './MetricQueryEditor'; +import { defaultTimeSeriesList, defaultTimeSeriesQuery, MetricQueryEditor } from './MetricQueryEditor'; import { PromQLQueryEditor } from './PromQLEditor'; import { QueryHeader } from './QueryHeader'; -import { defaultQuery as defaultSLOQuery } from './SLOQueryEditor'; - -import { MetricQueryEditor, SLOQueryEditor } from './'; +import { defaultQuery as defaultSLOQuery, SLOQueryEditor } from './SLOQueryEditor'; export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.tsx index 0b7169ae2fd..6045e61b37d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.tsx @@ -13,7 +13,7 @@ import { VariableQueryData, } from '../types/types'; -import { VariableQueryField } from './'; +import { VariableQueryField } from './Fields'; export type Props = QueryEditorProps< CloudMonitoringDatasource, diff --git a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap index fa53a1bac2d..c2d75c72c2d 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap @@ -67,7 +67,7 @@ exports[`VariableQueryEditor renders correctly 1`] = ` </div> </div> <div - class="css-1h17wob-input-suffix" + class="css-1ms3s8l-input-suffix" > <svg aria-hidden="true" diff --git a/public/app/plugins/datasource/cloud-monitoring/components/index.ts b/public/app/plugins/datasource/cloud-monitoring/components/index.ts deleted file mode 100644 index 734d8c47c22..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { Project } from './Project'; -export { GroupBy } from './GroupBy'; -export { Alignment } from './Alignment'; -export { LabelFilter } from './LabelFilter'; -export { AnnotationsHelp } from './AnnotationsHelp'; -export { AlignmentFunction } from './AlignmentFunction'; -export { AliasBy } from './AliasBy'; -export { Aggregation } from './Aggregation'; -export { MetricQueryEditor } from './MetricQueryEditor'; -export { SLOQueryEditor } from './SLOQueryEditor'; -export { MQLQueryEditor } from './MQLQueryEditor'; -export { VariableQueryField } from './Fields'; -export { VisualMetricQueryEditor } from './VisualMetricQueryEditor'; -export { PeriodSelect } from './PeriodSelect'; -export { Preprocessor } from './Preprocessor'; diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 12a8e878f30..4ebc93b79be 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -6,21 +6,21 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "12.1.0-pre", - "@grafana/google-sdk": "0.3.2", + "@grafana/google-sdk": "0.3.4", "@grafana/plugin-ui": "0.10.7", "@grafana/runtime": "12.1.0-pre", "@grafana/schema": "12.1.0-pre", "@grafana/ui": "12.1.0-pre", "debounce-promise": "3.1.2", "fast-deep-equal": "^3.1.3", - "i18next": "^24.0.0", + "i18next": "^25.0.0", "immer": "10.1.1", "lodash": "4.17.21", "monaco-editor": "0.34.1", "prismjs": "1.30.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" @@ -34,23 +34,23 @@ "@testing-library/user-event": "14.6.1", "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "react-select-event": "5.5.1", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts b/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts +++ b/public/app/plugins/datasource/cloud-monitoring/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx index bb87c2c6d19..ee9255a4fde 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import selectEvent from 'react-select-event'; import { CustomVariableModel, DataSourceInstanceSettings } from '@grafana/data'; @@ -7,7 +7,7 @@ import * as ui from '@grafana/ui'; import { CloudWatchDatasource } from '../../../datasource'; import { setupMockedTemplateService } from '../../../mocks/CloudWatchDataSource'; import { initialVariableModelState } from '../../../mocks/CloudWatchVariables'; -import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../../types'; +import { CloudWatchJsonData, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../../../types'; import { MetricsQueryEditor, Props } from './MetricsQueryEditor'; @@ -18,7 +18,16 @@ jest.mock('@grafana/ui', () => ({ }, })); -const setup = () => { +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual<typeof import('@grafana/runtime')>('@grafana/runtime'), + config: { + featureToggles: { + cloudWatchCrossAccountQuerying: true, + }, + }, +})); + +const setup = (customQuery?: Partial<CloudWatchMetricsQuery>, isMonitoringAccount?: boolean) => { const instanceSettings = { jsonData: { defaultRegion: 'us-east-1' }, } as DataSourceInstanceSettings<CloudWatchJsonData>; @@ -47,7 +56,7 @@ const setup = () => { datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); datasource.resources.getRegions = jest.fn().mockResolvedValue([]); datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(isMonitoringAccount ?? false); const props: Props = { query: { @@ -65,6 +74,7 @@ const setup = () => { matchExact: true, metricQueryType: MetricQueryType.Search, metricEditorMode: MetricEditorMode.Builder, + ...customQuery, }, extraHeaderElementLeft: () => {}, extraHeaderElementRight: () => {}, @@ -121,4 +131,31 @@ describe('QueryEditor', () => { expect(screen.queryByText('Alias')).toBeNull(); expect(screen.getByText("Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}")); }); + + it('should clear accountId field when datasource connects to a non-monitoring account', async () => { + const props = setup({ accountId: '123456789' }); + + render(<MetricsQueryEditor {...props} />); + + expect(props.datasource.resources.isMonitoringAccount).toHaveBeenCalledWith('us-east-1'); + await waitFor(async () => { + expect(props.onChange).toHaveBeenCalledWith({ + ...props.query, + accountId: undefined, + }); + }); + }); + it('should keep accountId field when datasource connects to a monitoring account', async () => { + const props = setup({ accountId: '123456789' }, true); + + render(<MetricsQueryEditor {...props} />); + + expect(props.datasource.resources.isMonitoringAccount).toHaveBeenCalledWith('us-east-1'); + await waitFor(async () => { + expect(props.onChange).not.toHaveBeenCalledWith({ + ...props.query, + accountId: undefined, + }); + }); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx index 2740403e522..bbb7d1272b1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { EditorField, EditorRow, InlineSelect } from '@grafana/plugin-ui'; +import { config } from '@grafana/runtime'; import { ConfirmModal, Input, RadioButtonGroup, Space } from '@grafana/ui'; import { CloudWatchDatasource } from '../../../datasource'; @@ -59,6 +60,17 @@ export const MetricsQueryEditor = (props: Props) => { [setShowConfirm, onChange, codeEditorIsDirty, query] ); + const updateAccounIdOnMount = () => { + if (config.featureToggles.cloudWatchCrossAccountQuerying && query.accountId) { + datasource.resources.isMonitoringAccount(query.region).then((isMonitoring) => { + if (!isMonitoring && query.accountId) { + onChange({ ...query, accountId: undefined }); + } + }); + } + }; + useEffect(updateAccounIdOnMount, [datasource, onChange, query]); + useEffect(() => { extraHeaderElementLeft?.( <InlineSelect diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.test.ts index 8cfcb4c7960..eb1afde02f7 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/statementPosition.test.ts @@ -1,11 +1,9 @@ import { monacoTypes } from '@grafana/ui'; -import { - multiLineFullQuery, - singleLineFullQuery, - singleLineEmptyQuery, - singleLineTwoQueries, -} from '../../../mocks/cloudwatch-sql-test-data'; +import { sqlTestDataMultiLineFullQuery } from '../../../mocks/cloudwatch-sql-test-data/multiLineFullQuery'; +import { sqlTestDataSingleLineEmptyQuery } from '../../../mocks/cloudwatch-sql-test-data/singleLineEmptyQuery'; +import { sqlTestDataSingleLineFullQuery } from '../../../mocks/cloudwatch-sql-test-data/singleLineFullQuery'; +import { sqlTestDataSingleLineTwoQueries } from '../../../mocks/cloudwatch-sql-test-data/singleLineTwoQueries'; import MonacoMock from '../../../mocks/monarch/Monaco'; import TextModel from '../../../mocks/monarch/TextModel'; import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; @@ -29,138 +27,138 @@ describe('statementPosition', () => { expect(statementPosition).toBe(expected); } test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 0 }], - [multiLineFullQuery.query, { lineNumber: 1, column: 0 }], - [singleLineEmptyQuery.query, { lineNumber: 1, column: 0 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 154 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 0 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 1, column: 0 }], + [sqlTestDataSingleLineEmptyQuery.query, { lineNumber: 1, column: 0 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 154 }], ])('should be before select keyword', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.SelectKeyword); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 7 }], - [multiLineFullQuery.query, { lineNumber: 1, column: 7 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 161 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 7 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 1, column: 7 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 161 }], ])('should be after select keyword', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterSelectKeyword); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 12 }], - [multiLineFullQuery.query, { lineNumber: 1, column: 12 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 166 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 12 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 1, column: 12 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 166 }], ])('should be first argument in select statistic function', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterSelectFuncFirstArgument); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 27 }], - [multiLineFullQuery.query, { lineNumber: 2, column: 0 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 181 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 27 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 0 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 181 }], ])('should be before the FROM keyword', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.FromKeyword); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 32 }], - [multiLineFullQuery.query, { lineNumber: 2, column: 5 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 186 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 32 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 5 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 186 }], ])('should after the FROM keyword', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterFromKeyword); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 40 }], - [multiLineFullQuery.query, { lineNumber: 2, column: 13 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 40 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 40 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 13 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 40 }], ])('should be namespace arg in the schema func', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.SchemaFuncFirstArgument); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, { lineNumber: 2, column: 23 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 50 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 50 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 23 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 50 }], ])('should be label key args within the schema func', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.SchemaFuncExtraArgument); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 63 }], - [multiLineFullQuery.query, { lineNumber: 3, column: 0 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 63 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 63 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 3, column: 0 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 63 }], ])('should be after from schema/namespace', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterFrom); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 69 }], - [multiLineFullQuery.query, { lineNumber: 4, column: 6 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 69 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 69 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 6 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 69 }], ])('should after where keyword and before label key', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.WhereKey); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 79 }], - [multiLineFullQuery.query, { lineNumber: 4, column: 17 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 79 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 79 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 17 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 79 }], ])('should be before the comparison operator in a where filter', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.WhereComparisonOperator); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 81 }], - [multiLineFullQuery.query, { lineNumber: 4, column: 19 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 81 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 81 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 19 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 81 }], ])('should be before or in the value in a where filter', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.WhereValue); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 105 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 0 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 105 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 105 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 0 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 105 }], ])('should be after a where value', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterWhereValue); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 115 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 115 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 115 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 10 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 115 }], ])('should be after group by keywords', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterGroupByKeywords); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 123 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 22 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 123 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 123 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 22 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 123 }], ])('should be after group by labels', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterGroupBy); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 132 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 31 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 132 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 132 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 31 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 132 }], ])('should be after order by keywords', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterOrderByKeywords); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 138 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 37 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 138 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 138 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 37 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 138 }], ])('should be after order by function', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterOrderByFunction); }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 143 }], - [multiLineFullQuery.query, { lineNumber: 6, column: 0 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 145 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 143 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 6, column: 0 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 145 }], ])('should be after order by direction', (query: string, position: monacoTypes.IPosition) => { assertPosition(query, position, StatementPosition.AfterOrderByDirection); }); diff --git a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/tokenUtils.test.ts b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/tokenUtils.test.ts index 54e44f44866..58368ebe281 100644 --- a/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/tokenUtils.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/cloudwatch-sql/completion/tokenUtils.test.ts @@ -1,11 +1,9 @@ import { monacoTypes } from '@grafana/ui'; -import { - multiLineFullQuery, - singleLineFullQuery, - singleLineTwoQueries, - multiLineIncompleteQueryWithoutNamespace, -} from '../../../mocks/cloudwatch-sql-test-data'; +import { sqlTestDataMultiLineFullQuery } from '../../../mocks/cloudwatch-sql-test-data/multiLineFullQuery'; +import { sqlTestDataMultiLineIncompleteQueryWithoutNamespace } from '../../../mocks/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace'; +import { sqlTestDataSingleLineFullQuery } from '../../../mocks/cloudwatch-sql-test-data/singleLineFullQuery'; +import { sqlTestDataSingleLineTwoQueries } from '../../../mocks/cloudwatch-sql-test-data/singleLineTwoQueries'; import MonacoMock from '../../../mocks/monarch/Monaco'; import TextModel from '../../../mocks/monarch/TextModel'; import { LinkedToken } from '../../monarch/LinkedToken'; @@ -34,10 +32,10 @@ const getToken = ( describe('tokenUtils', () => { test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 30 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 185 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 50 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 10 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 30 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 185 }], ])('getSelectToken should return the right token', (query: string, position: monacoTypes.IPosition) => { const token = getToken(query, position, getSelectToken); expect(token).not.toBeNull(); @@ -46,10 +44,10 @@ describe('tokenUtils', () => { }); test.each([ - [singleLineFullQuery.query, { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 30 }], - [singleLineTwoQueries.query, { lineNumber: 1, column: 185 }], + [sqlTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 50 }], + [sqlTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 10 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 30 }], + [sqlTestDataSingleLineTwoQueries.query, { lineNumber: 1, column: 185 }], ])('getSelectToken should return the right token', (query: string, position: monacoTypes.IPosition) => { const token = getToken(query, position, getSelectStatisticToken); expect(token).not.toBeNull(); @@ -57,10 +55,10 @@ describe('tokenUtils', () => { }); test.each([ - [singleLineFullQuery.query, 'AVG', { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, 'AVG', { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, 'AVG', { lineNumber: 1, column: 30 }], - [singleLineTwoQueries.query, 'SUM', { lineNumber: 1, column: 185 }], + [sqlTestDataSingleLineFullQuery.query, 'AVG', { lineNumber: 1, column: 50 }], + [sqlTestDataMultiLineFullQuery.query, 'AVG', { lineNumber: 5, column: 10 }], + [sqlTestDataSingleLineTwoQueries.query, 'AVG', { lineNumber: 1, column: 30 }], + [sqlTestDataSingleLineTwoQueries.query, 'SUM', { lineNumber: 1, column: 185 }], ])( 'getSelectStatisticToken should return the right token', (query: string, value: string, position: monacoTypes.IPosition) => { @@ -72,10 +70,10 @@ describe('tokenUtils', () => { ); test.each([ - [singleLineFullQuery.query, 'CPUUtilization', { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, 'CPUUtilization', { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, 'CPUUtilization', { lineNumber: 1, column: 30 }], - [singleLineTwoQueries.query, 'CPUCreditUsage', { lineNumber: 1, column: 185 }], + [sqlTestDataSingleLineFullQuery.query, 'CPUUtilization', { lineNumber: 1, column: 50 }], + [sqlTestDataMultiLineFullQuery.query, 'CPUUtilization', { lineNumber: 5, column: 10 }], + [sqlTestDataSingleLineTwoQueries.query, 'CPUUtilization', { lineNumber: 1, column: 30 }], + [sqlTestDataSingleLineTwoQueries.query, 'CPUCreditUsage', { lineNumber: 1, column: 185 }], ])( 'getMetricNameToken should return the right token', (query: string, value: string, position: monacoTypes.IPosition) => { @@ -87,11 +85,11 @@ describe('tokenUtils', () => { ); test.each([ - [singleLineFullQuery.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 50 }], - [multiLineFullQuery.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 5, column: 10 }], - [singleLineTwoQueries.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 30 }], - [singleLineTwoQueries.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 1, column: 185 }], - [multiLineIncompleteQueryWithoutNamespace.query, undefined, undefined, { lineNumber: 2, column: 5 }], + [sqlTestDataSingleLineFullQuery.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 50 }], + [sqlTestDataMultiLineFullQuery.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 5, column: 10 }], + [sqlTestDataSingleLineTwoQueries.query, '"AWS/EC2"', SQLTokenTypes.Type, { lineNumber: 1, column: 30 }], + [sqlTestDataSingleLineTwoQueries.query, '"AWS/ECS"', SQLTokenTypes.Type, { lineNumber: 1, column: 185 }], + [sqlTestDataMultiLineIncompleteQueryWithoutNamespace.query, undefined, undefined, { lineNumber: 2, column: 5 }], ])( 'getNamespaceToken should return the right token', (query: string, value: string | undefined, tokenType: string | undefined, position: monacoTypes.IPosition) => { diff --git a/public/app/plugins/datasource/cloudwatch/language/dynamic-labels/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/dynamic-labels/CompletionItemProvider.test.ts index c91dad19e38..f488d20d5dc 100644 --- a/public/app/plugins/datasource/cloudwatch/language/dynamic-labels/CompletionItemProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/dynamic-labels/CompletionItemProvider.test.ts @@ -1,7 +1,8 @@ import { CompletionItemPriority } from '@grafana/plugin-ui'; import { Monaco, monacoTypes } from '@grafana/ui'; -import { afterLabelValue, insideLabelValue } from '../../mocks/dynamic-label-test-data'; +import { dynamicLabelTestDataAfterLabelValue } from '../../mocks/dynamic-label-test-data/afterLabelValue'; +import { dynamicLabelTestDataInsideLabelValue } from '../../mocks/dynamic-label-test-data/insideLabelValue'; import MonacoMock from '../../mocks/monarch/Monaco'; import TextModel from '../../mocks/monarch/TextModel'; @@ -23,13 +24,13 @@ const getSuggestions = async (value: string, position: monacoTypes.IPosition) => describe('Dynamic labels: CompletionItemProvider', () => { describe('getSuggestions', () => { it('returns all dynamic labels in case current token is a whitespace', async () => { - const { query, position } = afterLabelValue; + const { query, position } = dynamicLabelTestDataAfterLabelValue; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toEqual(DYNAMIC_LABEL_PATTERNS.length + 1); // + 1 for the dimension suggestions }); it('should return suggestion for dimension label that has high prio', async () => { - const { query, position } = afterLabelValue; + const { query, position } = dynamicLabelTestDataAfterLabelValue; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toBeTruthy(); const highPrioSuggestsions = suggestions.filter((s) => s.sortText === CompletionItemPriority.High); @@ -38,7 +39,7 @@ describe('Dynamic labels: CompletionItemProvider', () => { }); it('doesnt return suggestions if cursor is inside a dynamic label', async () => { - const { query, position } = insideLabelValue; + const { query, position } = dynamicLabelTestDataInsideLabelValue; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toBe(0); }); diff --git a/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts index 3a70d392749..54a0a2cb941 100644 --- a/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/logs/completion/CompletionItemProvider.test.ts @@ -2,7 +2,10 @@ import { CustomVariableModel } from '@grafana/data'; import { Monaco, monacoTypes } from '@grafana/ui'; import { setupMockedTemplateService, logGroupNamesVariable } from '../../../mocks/CloudWatchDataSource'; -import { emptyQuery, filterQuery, newCommandQuery, sortQuery } from '../../../mocks/cloudwatch-logs-test-data'; +import { logsTestDataEmptyQuery } from '../../../mocks/cloudwatch-logs-test-data/empty'; +import { logsTestDataFilterQuery } from '../../../mocks/cloudwatch-logs-test-data/filterQuery'; +import { logsTestDataNewCommandQuery } from '../../../mocks/cloudwatch-logs-test-data/newCommandQuery'; +import { logsTestDataSortQuery } from '../../../mocks/cloudwatch-logs-test-data/sortQuery'; import MonacoMock from '../../../mocks/monarch/Monaco'; import TextModel from '../../../mocks/monarch/TextModel'; import { ResourcesAPI } from '../../../resources/ResourcesAPI'; @@ -45,39 +48,41 @@ const getSuggestions = async ( describe('LogsCompletionItemProvider', () => { describe('getSuggestions', () => { it('returns commands for an empty query', async () => { - const suggestions = await getSuggestions(emptyQuery.query, emptyQuery.position); + const suggestions = await getSuggestions(logsTestDataEmptyQuery.query, logsTestDataEmptyQuery.position); const suggestionLabels = suggestions.map((s) => s.label); expect(suggestionLabels).toEqual(expect.arrayContaining(LOGS_COMMANDS)); }); it('returns commands for a query when a new command is started', async () => { - const suggestions = await getSuggestions(newCommandQuery.query, newCommandQuery.position); + const suggestions = await getSuggestions(logsTestDataNewCommandQuery.query, logsTestDataNewCommandQuery.position); const suggestionLabels = suggestions.map((s) => s.label); expect(suggestionLabels).toEqual(expect.arrayContaining(LOGS_COMMANDS)); }); it('returns sort order directions for the sort keyword', async () => { - const suggestions = await getSuggestions(sortQuery.query, sortQuery.position); + const suggestions = await getSuggestions(logsTestDataSortQuery.query, logsTestDataSortQuery.position); const suggestionLabels = suggestions.map((s) => s.label); expect(suggestionLabels).toEqual(expect.arrayContaining(SORT_DIRECTION_KEYWORDS)); }); it('returns function suggestions after a command', async () => { - const suggestions = await getSuggestions(sortQuery.query, sortQuery.position); + const suggestions = await getSuggestions(logsTestDataSortQuery.query, logsTestDataSortQuery.position); const suggestionLabels = suggestions.map((s) => s.label); expect(suggestionLabels).toEqual(expect.arrayContaining(LOGS_FUNCTION_OPERATORS)); }); it('returns `in []` snippet for the `in` keyword', async () => { - const suggestions = await getSuggestions(filterQuery.query, filterQuery.position); + const suggestions = await getSuggestions(logsTestDataFilterQuery.query, logsTestDataFilterQuery.position); const suggestionLabels = suggestions.map((s) => s.label); expect(suggestionLabels).toEqual(expect.arrayContaining(['in []'])); }); it('returns template variables appended to list of suggestions', async () => { - const suggestions = await getSuggestions(newCommandQuery.query, newCommandQuery.position, [ - logGroupNamesVariable, - ]); + const suggestions = await getSuggestions( + logsTestDataNewCommandQuery.query, + logsTestDataNewCommandQuery.position, + [logGroupNamesVariable] + ); const suggestionLabels = suggestions.map((s) => s.label); const expectedTemplateVariableLabel = `$${logGroupNamesVariable.name}`; const expectedLabels = [...LOGS_COMMANDS, expectedTemplateVariableLabel]; @@ -86,8 +91,8 @@ describe('LogsCompletionItemProvider', () => { it('fetches fields when logGroups are set', async () => { const suggestions = await getSuggestions( - sortQuery.query, - sortQuery.position, + logsTestDataSortQuery.query, + logsTestDataSortQuery.position, [], [{ arn: 'foo', name: 'bar' }], [{ value: { name: '@field' } }] diff --git a/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.test.ts index 0eac4cecc43..2820712f031 100644 --- a/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/logs/completion/statementPosition.test.ts @@ -1,12 +1,10 @@ import { monacoTypes } from '@grafana/ui'; -import { - emptyQuery, - whitespaceOnlyQuery, - commentOnlyQuery, - singleLineFullQuery, - multiLineFullQuery, -} from '../../../mocks/cloudwatch-logs-test-data'; +import { logsTestDataCommentOnlyQuery } from '../../../mocks/cloudwatch-logs-test-data/commentOnlyQuery'; +import { logsTestDataEmptyQuery } from '../../../mocks/cloudwatch-logs-test-data/empty'; +import { logsTestDataMultiLineFullQuery } from '../../../mocks/cloudwatch-logs-test-data/multiLineFullQuery'; +import { logsTestDataSingleLineFullQuery } from '../../../mocks/cloudwatch-logs-test-data/singleLineFullQuery'; +import { logsTestDataWhitespaceOnlyQuery } from '../../../mocks/cloudwatch-logs-test-data/whitespaceQuery'; import MonacoMock from '../../../mocks/monarch/Monaco'; import TextModel from '../../../mocks/monarch/TextModel'; import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; @@ -33,125 +31,125 @@ describe('getStatementPosition', () => { }); it('should return StatementPosition.NewCommand for an empty query', () => { - expect(getStatementPosition(generateToken(emptyQuery.query, { lineNumber: 1, column: 1 }))).toEqual( + expect(getStatementPosition(generateToken(logsTestDataEmptyQuery.query, { lineNumber: 1, column: 1 }))).toEqual( StatementPosition.NewCommand ); }); it('should return StatementPosition.NewCommand for a query that is only whitespace', () => { - expect(getStatementPosition(generateToken(whitespaceOnlyQuery.query, { lineNumber: 1, column: 1 }))).toEqual( - StatementPosition.NewCommand - ); + expect( + getStatementPosition(generateToken(logsTestDataWhitespaceOnlyQuery.query, { lineNumber: 1, column: 1 })) + ).toEqual(StatementPosition.NewCommand); }); it('should return StatementPosition.Comment for a query with only a comment', () => { - expect(getStatementPosition(generateToken(commentOnlyQuery.query, { lineNumber: 1, column: 1 }))).toEqual( - StatementPosition.Comment - ); + expect( + getStatementPosition(generateToken(logsTestDataCommentOnlyQuery.query, { lineNumber: 1, column: 1 })) + ).toEqual(StatementPosition.Comment); }); it('should return StatementPosition.NewCommand after a `|`', () => { - expect(getStatementPosition(generateToken(singleLineFullQuery.query, { lineNumber: 1, column: 30 }))).toEqual( - StatementPosition.NewCommand - ); - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 2 }))).toEqual( - StatementPosition.NewCommand - ); + expect( + getStatementPosition(generateToken(logsTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 30 })) + ).toEqual(StatementPosition.NewCommand); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 2 })) + ).toEqual(StatementPosition.NewCommand); }); it('should return StatementPosition.FieldsKeyword inside the `fields` keyword', () => { - expect(getStatementPosition(generateToken(singleLineFullQuery.query, { lineNumber: 1, column: 6 }))).toEqual( - StatementPosition.FieldsKeyword - ); + expect( + getStatementPosition(generateToken(logsTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 6 })) + ).toEqual(StatementPosition.FieldsKeyword); }); it('should return StatementPosition.AfterFieldsKeyword after the `fields` keyword', () => { - expect(getStatementPosition(generateToken(singleLineFullQuery.query, { lineNumber: 1, column: 7 }))).toEqual( - StatementPosition.AfterFieldsKeyword - ); + expect( + getStatementPosition(generateToken(logsTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 7 })) + ).toEqual(StatementPosition.AfterFieldsKeyword); }); it('should return StatementPosition.CommandArg after a keyword', () => { - expect(getStatementPosition(generateToken(singleLineFullQuery.query, { lineNumber: 1, column: 8 }))).toEqual( - StatementPosition.CommandArg - ); + expect( + getStatementPosition(generateToken(logsTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 8 })) + ).toEqual(StatementPosition.CommandArg); }); it('should return StatementPosition.AfterCommand after a function', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 49 }))).toEqual( - StatementPosition.AfterCommand - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 49 })) + ).toEqual(StatementPosition.AfterCommand); }); it('should return StatementPosition.Function within a function', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 9 }))).toEqual( - StatementPosition.Function - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 9 })) + ).toEqual(StatementPosition.Function); }); it('should return StatementPosition.FunctionArg when providing arguments to a function', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 16 }))).toEqual( - StatementPosition.FunctionArg - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 16 })) + ).toEqual(StatementPosition.FunctionArg); }); it('should return StatementPosition.AfterFunction after a function', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 64 }))).toEqual( - StatementPosition.AfterFunction - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 64 })) + ).toEqual(StatementPosition.AfterFunction); }); it('should return StatementPosition.ArithmeticOperatorArg after an arithmetic operator', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 51 }))).toEqual( - StatementPosition.ArithmeticOperatorArg - ); - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 58 }))).toEqual( - StatementPosition.ArithmeticOperatorArg - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 51 })) + ).toEqual(StatementPosition.ArithmeticOperatorArg); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 58 })) + ).toEqual(StatementPosition.ArithmeticOperatorArg); }); it('should return StatementPosition.BooleanOperatorArg after a boolean operator', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 37 }))).toEqual( - StatementPosition.BooleanOperatorArg - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 37 })) + ).toEqual(StatementPosition.BooleanOperatorArg); }); it('should return StatementPosition.ComparisonOperatorArg after a comparison operator', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 44 }))).toEqual( - StatementPosition.ComparisonOperatorArg - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 44 })) + ).toEqual(StatementPosition.ComparisonOperatorArg); }); it('should return StatementPosition.ArithmeticOperator after an arithmetic operator', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 50 }))).toEqual( - StatementPosition.ArithmeticOperator - ); - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 4, column: 57 }))).toEqual( - StatementPosition.ArithmeticOperator - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 50 })) + ).toEqual(StatementPosition.ArithmeticOperator); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 4, column: 57 })) + ).toEqual(StatementPosition.ArithmeticOperator); }); it('should return StatementPosition.BooleanOperator after a boolean operator', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 35 }))).toEqual( - StatementPosition.BooleanOperator - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 35 })) + ).toEqual(StatementPosition.BooleanOperator); }); it('should return StatementPosition.ComparisonOperator after a comparison operator', () => { - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 2, column: 43 }))).toEqual( - StatementPosition.ComparisonOperator - ); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 2, column: 43 })) + ).toEqual(StatementPosition.ComparisonOperator); }); it('should return StatementPosition.Comment when token is a comment', () => { - expect(getStatementPosition(generateToken(singleLineFullQuery.query, { lineNumber: 1, column: 40 }))).toEqual( - StatementPosition.Comment - ); - expect(getStatementPosition(generateToken(commentOnlyQuery.query, { lineNumber: 1, column: 35 }))).toEqual( - StatementPosition.Comment - ); - expect(getStatementPosition(generateToken(multiLineFullQuery.query, { lineNumber: 5, column: 3 }))).toEqual( - StatementPosition.Comment - ); + expect( + getStatementPosition(generateToken(logsTestDataSingleLineFullQuery.query, { lineNumber: 1, column: 40 })) + ).toEqual(StatementPosition.Comment); + expect( + getStatementPosition(generateToken(logsTestDataCommentOnlyQuery.query, { lineNumber: 1, column: 35 })) + ).toEqual(StatementPosition.Comment); + expect( + getStatementPosition(generateToken(logsTestDataMultiLineFullQuery.query, { lineNumber: 5, column: 3 })) + ).toEqual(StatementPosition.Comment); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/CompletionItemProvider.test.ts index 2b5cf635293..15da6a8b764 100644 --- a/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/CompletionItemProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/CompletionItemProvider.test.ts @@ -1,7 +1,12 @@ import { Monaco, monacoTypes } from '@grafana/ui'; import { setupMockedTemplateService } from '../../../mocks/CloudWatchDataSource'; -import * as MetricMathTestData from '../../../mocks/metric-math-test-data'; +import { afterFunctionQuery } from '../../../mocks/metric-math-test-data/afterFunctionQuery'; +import { secondArgAfterSearchQuery } from '../../../mocks/metric-math-test-data/secondArgAfterSearchQuery'; +import { secondArgQuery } from '../../../mocks/metric-math-test-data/secondArgQuery'; +import { singleLineEmptyQuery } from '../../../mocks/metric-math-test-data/singleLineEmptyQuery'; +import { thirdArgAfterSearchQuery } from '../../../mocks/metric-math-test-data/thirdArgAfterSearchQuery'; +import { withinStringQuery } from '../../../mocks/metric-math-test-data/withinStringQuery'; import MonacoMock from '../../../mocks/monarch/Monaco'; import TextModel from '../../../mocks/monarch/TextModel'; import { ResourcesAPI } from '../../../resources/ResourcesAPI'; @@ -34,37 +39,37 @@ const getSuggestions = async (value: string, position: monacoTypes.IPosition) => describe('MetricMath: CompletionItemProvider', () => { describe('getSuggestions', () => { it('returns a suggestion for every metric math function when the input field is empty', async () => { - const { query, position } = MetricMathTestData.singleLineEmptyQuery; + const { query, position } = singleLineEmptyQuery; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toEqual(METRIC_MATH_FNS.length); }); it('returns a suggestion for every metric math operator when at the end of a function', async () => { - const { query, position } = MetricMathTestData.afterFunctionQuery; + const { query, position } = afterFunctionQuery; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toEqual(METRIC_MATH_OPERATORS.length); }); it('returns a suggestion for every metric math function and keyword if at the start of the second argument of a function', async () => { - const { query, position } = MetricMathTestData.secondArgQuery; + const { query, position } = secondArgQuery; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toEqual(METRIC_MATH_FNS.length + METRIC_MATH_KEYWORDS.length); }); it('does not have any particular suggestions if within a string', async () => { - const { query, position } = MetricMathTestData.withinStringQuery; + const { query, position } = withinStringQuery; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toEqual(0); }); it('returns a suggestion for every statistic if the second arg of a search function', async () => { - const { query, position } = MetricMathTestData.secondArgAfterSearchQuery; + const { query, position } = secondArgAfterSearchQuery; const suggestions = await getSuggestions(query, position); expect(suggestions.length).toEqual(METRIC_MATH_STATISTIC_KEYWORD_STRINGS.length); }); it('returns a suggestion for every period if the third arg of a search function', async () => { - const { query, position } = MetricMathTestData.thirdArgAfterSearchQuery; + const { query, position } = thirdArgAfterSearchQuery; const suggestions = await getSuggestions(query, position); // +1 because one suggestion is also added for the $__period_auto macro const expectedSuggestionsLength = METRIC_MATH_PERIODS.length + 1; diff --git a/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/statementPosition.test.ts b/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/statementPosition.test.ts index 7b8c5e41752..47d42acb605 100644 --- a/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/statementPosition.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/metric-math/completion/statementPosition.test.ts @@ -1,6 +1,11 @@ import { monacoTypes } from '@grafana/ui'; -import * as MetricMathTestQueries from '../../../mocks/metric-math-test-data'; +import { afterFunctionQuery } from '../../../mocks/metric-math-test-data/afterFunctionQuery'; +import { secondArgAfterSearchQuery } from '../../../mocks/metric-math-test-data/secondArgAfterSearchQuery'; +import { secondArgQuery } from '../../../mocks/metric-math-test-data/secondArgQuery'; +import { singleLineEmptyQuery } from '../../../mocks/metric-math-test-data/singleLineEmptyQuery'; +import { thirdArgAfterSearchQuery } from '../../../mocks/metric-math-test-data/thirdArgAfterSearchQuery'; +import { withinStringQuery } from '../../../mocks/metric-math-test-data/withinStringQuery'; import MonacoMock from '../../../mocks/monarch/Monaco'; import TextModel from '../../../mocks/monarch/TextModel'; import { linkedTokenBuilder } from '../../monarch/linkedTokenBuilder'; @@ -23,49 +28,31 @@ describe('statementPosition', () => { } it('returns PredefinedFunction when at the beginning of an empty query', () => { - const token = createToken( - MetricMathTestQueries.singleLineEmptyQuery.query, - MetricMathTestQueries.singleLineEmptyQuery.position - ); + const token = createToken(singleLineEmptyQuery.query, singleLineEmptyQuery.position); expect(getStatementPosition(token)).toEqual(StatementPosition.PredefinedFunction); }); it('returns PredefinedFuncSecondArg when in the second arg of a predefined function', () => { - const token = createToken( - MetricMathTestQueries.secondArgQuery.query, - MetricMathTestQueries.secondArgQuery.position - ); + const token = createToken(secondArgQuery.query, secondArgQuery.position); expect(getStatementPosition(token)).toEqual(StatementPosition.PredefinedFuncSecondArg); }); it('returns SearchFuncSecondArg when in the second arg of a Search function', () => { - const token = createToken( - MetricMathTestQueries.secondArgAfterSearchQuery.query, - MetricMathTestQueries.secondArgAfterSearchQuery.position - ); + const token = createToken(secondArgAfterSearchQuery.query, secondArgAfterSearchQuery.position); expect(getStatementPosition(token)).toEqual(StatementPosition.SearchFuncSecondArg); }); it('returns SearchFuncThirdArg when in the third arg of a Search function', () => { - const token = createToken( - MetricMathTestQueries.thirdArgAfterSearchQuery.query, - MetricMathTestQueries.thirdArgAfterSearchQuery.position - ); + const token = createToken(thirdArgAfterSearchQuery.query, thirdArgAfterSearchQuery.position); expect(getStatementPosition(token)).toEqual(StatementPosition.SearchFuncThirdArg); }); it('returns AfterFunction when after a function', () => { - const token = createToken( - MetricMathTestQueries.afterFunctionQuery.query, - MetricMathTestQueries.afterFunctionQuery.position - ); + const token = createToken(afterFunctionQuery.query, afterFunctionQuery.position); expect(getStatementPosition(token)).toEqual(StatementPosition.AfterFunction); }); it('returns WithinString when within a string', () => { - const token = createToken( - MetricMathTestQueries.withinStringQuery.query, - MetricMathTestQueries.withinStringQuery.position - ); + const token = createToken(withinStringQuery.query, withinStringQuery.position); expect(getStatementPosition(token)).toEqual(StatementPosition.WithinString); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/language/monarch/linkedTokenBuilder.test.ts b/public/app/plugins/datasource/cloudwatch/language/monarch/linkedTokenBuilder.test.ts index 391b286c522..0a7fe54d16d 100644 --- a/public/app/plugins/datasource/cloudwatch/language/monarch/linkedTokenBuilder.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language/monarch/linkedTokenBuilder.test.ts @@ -1,6 +1,7 @@ import { monacoTypes } from '@grafana/ui'; -import { multiLineFullQuery, singleLineFullQuery } from '../../mocks/cloudwatch-sql-test-data'; +import { sqlTestDataMultiLineFullQuery } from '../../mocks/cloudwatch-sql-test-data/multiLineFullQuery'; +import { sqlTestDataSingleLineFullQuery } from '../../mocks/cloudwatch-sql-test-data/singleLineFullQuery'; import MonacoMock from '../../mocks/monarch/Monaco'; import TextModel from '../../mocks/monarch/TextModel'; import { SQLTokenTypes } from '../cloudwatch-sql/completion/types'; @@ -11,7 +12,7 @@ import { linkedTokenBuilder } from './linkedTokenBuilder'; describe('linkedTokenBuilder', () => { describe('singleLineFullQuery', () => { - const testModel = TextModel(singleLineFullQuery.query); + const testModel = TextModel(sqlTestDataSingleLineFullQuery.query); it('should add correct references to next LinkedToken', () => { const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 }; @@ -28,7 +29,7 @@ describe('linkedTokenBuilder', () => { }); it('should add correct references to previous LinkedToken', () => { - const position: monacoTypes.IPosition = { lineNumber: 1, column: singleLineFullQuery.query.length }; + const position: monacoTypes.IPosition = { lineNumber: 1, column: sqlTestDataSingleLineFullQuery.query.length }; const current = linkedTokenBuilder( MonacoMock, cloudWatchSqlLanguageDefinition, @@ -45,7 +46,7 @@ describe('linkedTokenBuilder', () => { }); describe('multiLineFullQuery', () => { - const testModel = TextModel(multiLineFullQuery.query); + const testModel = TextModel(sqlTestDataMultiLineFullQuery.query); it('should add LinkedToken with whitespace in case empty lines', () => { const position: monacoTypes.IPosition = { lineNumber: 3, column: 0 }; diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/commentOnlyQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/commentOnlyQuery.ts index ceb105b5762..511875c109c 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/commentOnlyQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/commentOnlyQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const commentOnlyQuery = { +export const logsTestDataCommentOnlyQuery = { query: `# comment ending with whitespace `, tokens: [ [{ offset: 0, type: LogsTokenTypes.Comment, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID }], diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/empty.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/empty.ts index 6f4ae6aafba..a47317dd1fc 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/empty.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/empty.ts @@ -1,4 +1,4 @@ -export const emptyQuery = { +export const logsTestDataEmptyQuery = { query: '', tokens: [], position: { lineNumber: 1, column: 1 }, diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/filterQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/filterQuery.ts index 3cb71d829cd..725f1d00fc8 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/filterQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/filterQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const filterQuery = { +export const logsTestDataFilterQuery = { query: `filter logGroup `, tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/index.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/index.ts deleted file mode 100644 index 03832032916..00000000000 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { emptyQuery } from './empty'; -export { whitespaceOnlyQuery } from './whitespaceQuery'; -export { commentOnlyQuery } from './commentOnlyQuery'; -export { singleLineFullQuery } from './singleLineFullQuery'; -export { multiLineFullQuery } from './multiLineFullQuery'; -export { filterQuery } from './filterQuery'; -export { newCommandQuery } from './newCommandQuery'; -export { sortQuery } from './sortQuery'; diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/multiLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/multiLineFullQuery.ts index 56321e94645..2034f5dc31a 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/multiLineFullQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/multiLineFullQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const multiLineFullQuery = { +export const logsTestDataMultiLineFullQuery = { query: `fields @timestamp, unmask(@message) as msg, @memorySize | filter (@message like /error/ and bytes > 1000) | parse @message /(?<NetworkInterface>eni-.*?)/ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/newCommandQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/newCommandQuery.ts index f9bbf983e52..84730e5ddcd 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/newCommandQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/newCommandQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const newCommandQuery = { +export const logsTestDataNewCommandQuery = { query: `fields @timestamp | `, tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/singleLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/singleLineFullQuery.ts index 88779221ab9..ab2600320c7 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/singleLineFullQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/singleLineFullQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const singleLineFullQuery = { +export const logsTestDataSingleLineFullQuery = { query: `fields @timestamp, @message | limit 20 # this is a comment`, tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/sortQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/sortQuery.ts index ba535cd8479..2fa7dd472f9 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/sortQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/sortQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const sortQuery = { +export const logsTestDataSortQuery = { query: `fields @timestamp | sort `, tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/whitespaceQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/whitespaceQuery.ts index b4e93a15b69..e46bd219a16 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/whitespaceQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-logs-test-data/whitespaceQuery.ts @@ -3,7 +3,7 @@ import { monacoTypes } from '@grafana/ui'; import { LogsTokenTypes } from '../../language/logs/completion/types'; import { CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID } from '../../language/logs/definition'; -export const whitespaceOnlyQuery = { +export const logsTestDataWhitespaceOnlyQuery = { query: ` `, tokens: [ [{ offset: 0, type: LogsTokenTypes.Whitespace, language: CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID }], diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/index.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/index.ts deleted file mode 100644 index aa971ef6dd0..00000000000 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { multiLineFullQuery } from './multiLineFullQuery'; -export { singleLineFullQuery } from './singleLineFullQuery'; -export { singleLineEmptyQuery } from './singleLineEmptyQuery'; -export { singleLineTwoQueries } from './singleLineTwoQueries'; -export { multiLineIncompleteQueryWithoutNamespace } from './multiLineIncompleteQueryWithoutNamespace'; diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineFullQuery.ts index 69a735d285c..ae63482c653 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineFullQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineFullQuery.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const multiLineFullQuery = { +export const sqlTestDataMultiLineFullQuery = { query: `SELECT AVG(CPUUtilization) FROM SCHEMA("AWS/ECS", InstanceId) diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts index b09bb82cba8..59985eb4cdc 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const multiLineIncompleteQueryWithoutNamespace = { +export const sqlTestDataMultiLineIncompleteQueryWithoutNamespace = { query: `SELECT AVG(CPUUtilization) FROM `, tokens: [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineEmptyQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineEmptyQuery.ts index b3720be7b28..218f546c252 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineEmptyQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineEmptyQuery.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const singleLineEmptyQuery = { +export const sqlTestDataSingleLineEmptyQuery = { query: '', tokens: [] as monacoTypes.Token[][], }; diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineFullQuery.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineFullQuery.ts index a6021bf2189..12859236453 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineFullQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineFullQuery.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const singleLineFullQuery = { +export const sqlTestDataSingleLineFullQuery = { query: `SELECT AVG(CPUUtilization) FROM SCHEMA("AWS/EC2", InstanceId) WHERE InstanceId = 'i-03c6908092db17ac9' GROUP BY InstanceId ORDER BY AVG() DESC LIMIT 10`, tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineTwoQueries.ts b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineTwoQueries.ts index 005d93b53c6..8afe2fc0f49 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineTwoQueries.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/cloudwatch-sql-test-data/singleLineTwoQueries.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const singleLineTwoQueries = { +export const sqlTestDataSingleLineTwoQueries = { query: `SELECT AVG(CPUUtilization) FROM SCHEMA("AWS/EC2", InstanceId) WHERE InstanceId = 'i-03c6908092db17ac9' GROUP BY InstanceId ORDER BY AVG() DESC LIMIT 10 / SELECT SUM(CPUCreditUsage) FROM "AWS/ECS"`, tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/afterLabelValue.ts b/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/afterLabelValue.ts index 14d07dda089..d4100197372 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/afterLabelValue.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/afterLabelValue.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const afterLabelValue = { +export const dynamicLabelTestDataAfterLabelValue = { query: '${DATAPOINT_COUNT} ', tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/index.ts b/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/index.ts deleted file mode 100644 index b6f4eee0612..00000000000 --- a/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { afterLabelValue } from './afterLabelValue'; -export { insideLabelValue } from './insideLabelValue'; diff --git a/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/insideLabelValue.ts b/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/insideLabelValue.ts index b60d1a106ae..d05eb55cbf1 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/insideLabelValue.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/dynamic-label-test-data/insideLabelValue.ts @@ -1,6 +1,6 @@ import { monacoTypes } from '@grafana/ui'; -export const insideLabelValue = { +export const dynamicLabelTestDataInsideLabelValue = { query: '${DATAPOINT_COUNT} ', tokens: [ [ diff --git a/public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/index.ts b/public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/index.ts deleted file mode 100644 index 1d4bcf2dbf1..00000000000 --- a/public/app/plugins/datasource/cloudwatch/mocks/metric-math-test-data/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { singleLineEmptyQuery } from './singleLineEmptyQuery'; -export { afterFunctionQuery } from './afterFunctionQuery'; -export { secondArgQuery } from './secondArgQuery'; -export { secondArgAfterSearchQuery } from './secondArgAfterSearchQuery'; -export { thirdArgAfterSearchQuery } from './thirdArgAfterSearchQuery'; -export { withinStringQuery } from './withinStringQuery'; diff --git a/public/app/plugins/datasource/cloudwatch/mocks/monarch/Monaco.ts b/public/app/plugins/datasource/cloudwatch/mocks/monarch/Monaco.ts index 09fda988e8e..e1115ed2187 100644 --- a/public/app/plugins/datasource/cloudwatch/mocks/monarch/Monaco.ts +++ b/public/app/plugins/datasource/cloudwatch/mocks/monarch/Monaco.ts @@ -11,13 +11,30 @@ import { partialQueryWithFunction as cloudwatchLogsSQLPartialQueryWithFunction } import { partialQueryWithSubquery as cloudwatchLogsSQLPartialQueryWithSubquery } from '../cloudwatch-logs-sql-test-data/partialQueryWithSubquery'; import { singleLineFullQuery as cloudwatchLogsSQLSingleLineFullQuery } from '../cloudwatch-logs-sql-test-data/singleLineFullQuery'; import { whitespaceQuery as cloudwatchLogsSQLWhitespaceQuery } from '../cloudwatch-logs-sql-test-data/whitespaceQuery'; -import * as CloudwatchLogsTestData from '../cloudwatch-logs-test-data'; +import { logsTestDataCommentOnlyQuery } from '../cloudwatch-logs-test-data/commentOnlyQuery'; +import { logsTestDataEmptyQuery } from '../cloudwatch-logs-test-data/empty'; +import { logsTestDataFilterQuery } from '../cloudwatch-logs-test-data/filterQuery'; +import { logsTestDataMultiLineFullQuery } from '../cloudwatch-logs-test-data/multiLineFullQuery'; +import { logsTestDataNewCommandQuery } from '../cloudwatch-logs-test-data/newCommandQuery'; +import { logsTestDataSingleLineFullQuery } from '../cloudwatch-logs-test-data/singleLineFullQuery'; +import { logsTestDataSortQuery } from '../cloudwatch-logs-test-data/sortQuery'; +import { logsTestDataWhitespaceOnlyQuery } from '../cloudwatch-logs-test-data/whitespaceQuery'; import * as PPLMultilineQueries from '../cloudwatch-ppl-test-data/multilineQueries'; import { newCommandQuery as PPLNewCommandQuery } from '../cloudwatch-ppl-test-data/newCommandQuery'; import * as PPLSingleLineQueries from '../cloudwatch-ppl-test-data/singleLineQueries'; -import * as SQLTestData from '../cloudwatch-sql-test-data'; -import * as DynamicLabelTestData from '../dynamic-label-test-data'; -import * as MetricMathTestData from '../metric-math-test-data'; +import { sqlTestDataMultiLineFullQuery } from '../cloudwatch-sql-test-data/multiLineFullQuery'; +import { sqlTestDataMultiLineIncompleteQueryWithoutNamespace } from '../cloudwatch-sql-test-data/multiLineIncompleteQueryWithoutNamespace'; +import { sqlTestDataSingleLineEmptyQuery } from '../cloudwatch-sql-test-data/singleLineEmptyQuery'; +import { sqlTestDataSingleLineFullQuery } from '../cloudwatch-sql-test-data/singleLineFullQuery'; +import { sqlTestDataSingleLineTwoQueries } from '../cloudwatch-sql-test-data/singleLineTwoQueries'; +import { dynamicLabelTestDataAfterLabelValue } from '../dynamic-label-test-data/afterLabelValue'; +import { dynamicLabelTestDataInsideLabelValue } from '../dynamic-label-test-data/insideLabelValue'; +import { afterFunctionQuery } from '../metric-math-test-data/afterFunctionQuery'; +import { secondArgAfterSearchQuery } from '../metric-math-test-data/secondArgAfterSearchQuery'; +import { secondArgQuery } from '../metric-math-test-data/secondArgQuery'; +import { singleLineEmptyQuery } from '../metric-math-test-data/singleLineEmptyQuery'; +import { thirdArgAfterSearchQuery } from '../metric-math-test-data/thirdArgAfterSearchQuery'; +import { withinStringQuery } from '../metric-math-test-data/withinStringQuery'; // Stub for the Monaco instance. const MonacoMock: Monaco = { @@ -25,44 +42,44 @@ const MonacoMock: Monaco = { tokenize: (value: string, languageId: string) => { if (languageId === 'cloudwatch-sql') { const TestData = { - [SQLTestData.multiLineFullQuery.query]: SQLTestData.multiLineFullQuery.tokens, - [SQLTestData.singleLineFullQuery.query]: SQLTestData.singleLineFullQuery.tokens, - [SQLTestData.singleLineEmptyQuery.query]: SQLTestData.singleLineEmptyQuery.tokens, - [SQLTestData.singleLineTwoQueries.query]: SQLTestData.singleLineTwoQueries.tokens, - [SQLTestData.multiLineIncompleteQueryWithoutNamespace.query]: - SQLTestData.multiLineIncompleteQueryWithoutNamespace.tokens, + [sqlTestDataMultiLineFullQuery.query]: sqlTestDataMultiLineFullQuery.tokens, + [sqlTestDataSingleLineFullQuery.query]: sqlTestDataSingleLineFullQuery.tokens, + [sqlTestDataSingleLineEmptyQuery.query]: sqlTestDataSingleLineEmptyQuery.tokens, + [sqlTestDataSingleLineTwoQueries.query]: sqlTestDataSingleLineTwoQueries.tokens, + [sqlTestDataMultiLineIncompleteQueryWithoutNamespace.query]: + sqlTestDataMultiLineIncompleteQueryWithoutNamespace.tokens, }; return TestData[value]; } if (languageId === 'cloudwatch-MetricMath') { const TestData = { - [MetricMathTestData.singleLineEmptyQuery.query]: MetricMathTestData.singleLineEmptyQuery.tokens, - [MetricMathTestData.afterFunctionQuery.query]: MetricMathTestData.afterFunctionQuery.tokens, - [MetricMathTestData.secondArgQuery.query]: MetricMathTestData.secondArgQuery.tokens, - [MetricMathTestData.secondArgAfterSearchQuery.query]: MetricMathTestData.secondArgAfterSearchQuery.tokens, - [MetricMathTestData.withinStringQuery.query]: MetricMathTestData.withinStringQuery.tokens, - [MetricMathTestData.thirdArgAfterSearchQuery.query]: MetricMathTestData.thirdArgAfterSearchQuery.tokens, + [singleLineEmptyQuery.query]: singleLineEmptyQuery.tokens, + [afterFunctionQuery.query]: afterFunctionQuery.tokens, + [secondArgQuery.query]: secondArgQuery.tokens, + [secondArgAfterSearchQuery.query]: secondArgAfterSearchQuery.tokens, + [withinStringQuery.query]: withinStringQuery.tokens, + [thirdArgAfterSearchQuery.query]: thirdArgAfterSearchQuery.tokens, }; return TestData[value]; } if (languageId === 'cloudwatch-dynamicLabels') { const TestData = { - [DynamicLabelTestData.afterLabelValue.query]: DynamicLabelTestData.afterLabelValue.tokens, - [DynamicLabelTestData.insideLabelValue.query]: DynamicLabelTestData.insideLabelValue.tokens, + [dynamicLabelTestDataAfterLabelValue.query]: dynamicLabelTestDataAfterLabelValue.tokens, + [dynamicLabelTestDataInsideLabelValue.query]: dynamicLabelTestDataInsideLabelValue.tokens, }; return TestData[value]; } if (languageId === CLOUDWATCH_LOGS_LANGUAGE_DEFINITION_ID) { const TestData = { - [CloudwatchLogsTestData.emptyQuery.query]: CloudwatchLogsTestData.emptyQuery.tokens, - [CloudwatchLogsTestData.whitespaceOnlyQuery.query]: CloudwatchLogsTestData.whitespaceOnlyQuery.tokens, - [CloudwatchLogsTestData.commentOnlyQuery.query]: CloudwatchLogsTestData.commentOnlyQuery.tokens, - [CloudwatchLogsTestData.singleLineFullQuery.query]: CloudwatchLogsTestData.singleLineFullQuery.tokens, - [CloudwatchLogsTestData.multiLineFullQuery.query]: CloudwatchLogsTestData.multiLineFullQuery.tokens, - [CloudwatchLogsTestData.filterQuery.query]: CloudwatchLogsTestData.filterQuery.tokens, - [CloudwatchLogsTestData.newCommandQuery.query]: CloudwatchLogsTestData.newCommandQuery.tokens, - [CloudwatchLogsTestData.sortQuery.query]: CloudwatchLogsTestData.sortQuery.tokens, + [logsTestDataEmptyQuery.query]: logsTestDataEmptyQuery.tokens, + [logsTestDataWhitespaceOnlyQuery.query]: logsTestDataWhitespaceOnlyQuery.tokens, + [logsTestDataCommentOnlyQuery.query]: logsTestDataCommentOnlyQuery.tokens, + [logsTestDataSingleLineFullQuery.query]: logsTestDataSingleLineFullQuery.tokens, + [logsTestDataMultiLineFullQuery.query]: logsTestDataMultiLineFullQuery.tokens, + [logsTestDataFilterQuery.query]: logsTestDataFilterQuery.tokens, + [logsTestDataNewCommandQuery.query]: logsTestDataNewCommandQuery.tokens, + [logsTestDataSortQuery.query]: logsTestDataSortQuery.tokens, }; return TestData[value]; } diff --git a/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts b/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts index 9292bf0e741..cc4a0c6f6f0 100644 --- a/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/elasticsearch/LanguageProvider.test.ts @@ -1,11 +1,11 @@ import { AbstractLabelOperator, AbstractQuery } from '@grafana/data'; import LanguageProvider from './LanguageProvider'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import { ElasticDatasource } from './datasource'; import { createElasticDatasource } from './mocks'; -import { ElasticsearchQuery } from './types'; -const baseLogsQuery: Partial<ElasticsearchQuery> = { +const baseLogsQuery: Partial<ElasticsearchDataQuery> = { metrics: [{ type: 'logs', id: '1' }], }; diff --git a/public/app/plugins/datasource/elasticsearch/LanguageProvider.ts b/public/app/plugins/datasource/elasticsearch/LanguageProvider.ts index 36bbf4ebfaf..963c4335f4b 100644 --- a/public/app/plugins/datasource/elasticsearch/LanguageProvider.ts +++ b/public/app/plugins/datasource/elasticsearch/LanguageProvider.ts @@ -1,7 +1,7 @@ import { AbstractLabelOperator, AbstractLabelMatcher, LanguageProvider, AbstractQuery } from '@grafana/data'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import { ElasticDatasource } from './datasource'; -import { ElasticsearchQuery } from './types'; export default class ElasticsearchLanguageProvider extends LanguageProvider { declare request: (url: string, params?: any) => Promise<any>; @@ -18,7 +18,7 @@ export default class ElasticsearchLanguageProvider extends LanguageProvider { /** * Queries are transformed to an ES Logs query since it's the behaviour most users expect. **/ - importFromAbstractQuery(abstractQuery: AbstractQuery): ElasticsearchQuery { + importFromAbstractQuery(abstractQuery: AbstractQuery): ElasticsearchDataQuery { return { metrics: [ { @@ -26,12 +26,12 @@ export default class ElasticsearchLanguageProvider extends LanguageProvider { type: 'logs', }, ], - query: this.getElasticsearchQuery(abstractQuery.labelMatchers), + query: this.getElasticsearchDataQuery(abstractQuery.labelMatchers), refId: abstractQuery.refId, }; } - getElasticsearchQuery(labels: AbstractLabelMatcher[]): string { + getElasticsearchDataQuery(labels: AbstractLabelMatcher[]): string { return labels .map((label) => { switch (label.operator) { diff --git a/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts b/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts index aefa020ed31..a042cb2e7e8 100644 --- a/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/QueryBuilder.test.ts @@ -1,5 +1,6 @@ import { ElasticQueryBuilder } from './QueryBuilder'; -import { ElasticsearchQuery, TermsQuery } from './types'; +import { ElasticsearchDataQuery } from './dataquery.gen'; +import { TermsQuery } from './types'; describe('ElasticQueryBuilder', () => { const builder = new ElasticQueryBuilder({ timeField: '@timestamp' }); // es2 @@ -59,7 +60,7 @@ describe('ElasticQueryBuilder', () => { }); it('term agg and order by term', () => { - const target: ElasticsearchQuery = { + const target: ElasticsearchDataQuery = { refId: 'A', metrics: [ { type: 'count', id: '1' }, @@ -262,7 +263,7 @@ describe('ElasticQueryBuilder', () => { }); it('should return correct query for raw_document metric', () => { - const target: ElasticsearchQuery = { + const target: ElasticsearchDataQuery = { refId: 'A', metrics: [{ type: 'raw_document', id: '1', settings: {} }], timeField: '@timestamp', diff --git a/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts b/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts index a5d7f767f2b..22ed3d8d68e 100644 --- a/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts +++ b/public/app/plugins/datasource/elasticsearch/QueryBuilder.ts @@ -7,6 +7,15 @@ import { isPipelineAggregation, isPipelineAggregationWithMultipleBucketPaths, } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; +import { + DateHistogram, + ElasticsearchDataQuery, + Filters, + Histogram, + MetricAggregation, + MetricAggregationWithInlineScript, + Terms, +} from './dataquery.gen'; import { defaultBucketAgg, defaultMetricAgg, @@ -14,16 +23,7 @@ import { highlightTags, defaultGeoHashPrecisionString, } from './queryDef'; -import { - ElasticsearchQuery, - TermsQuery, - Filters, - Terms, - MetricAggregation, - MetricAggregationWithInlineScript, - Histogram, - DateHistogram, -} from './types'; +import { TermsQuery } from './types'; import { convertOrderByToMetricId, getScriptValue } from './utils'; // Omitting 1m, 1h, 1d for now, as these cover the main use cases for calendar_interval @@ -48,7 +48,11 @@ export class ElasticQueryBuilder { return filter; } - buildTermsAgg(aggDef: Terms, queryNode: { terms?: any; aggs?: Record<string, unknown> }, target: ElasticsearchQuery) { + buildTermsAgg( + aggDef: Terms, + queryNode: { terms?: any; aggs?: Record<string, unknown> }, + target: ElasticsearchDataQuery + ) { queryNode.terms = { field: aggDef.field }; if (!aggDef.settings) { @@ -168,7 +172,7 @@ export class ElasticQueryBuilder { return query; } - build(target: ElasticsearchQuery) { + build(target: ElasticsearchDataQuery) { // make sure query has defaults; target.metrics = target.metrics || [defaultMetricAgg()]; target.bucketAggs = target.bucketAggs || [defaultBucketAgg()]; @@ -448,7 +452,7 @@ export class ElasticQueryBuilder { return query; } - getLogsQuery(target: ElasticsearchQuery, limit: number) { + getLogsQuery(target: ElasticsearchDataQuery, limit: number) { let query: any = { size: 0, query: { diff --git a/public/app/plugins/datasource/elasticsearch/components/MetricPicker.tsx b/public/app/plugins/datasource/elasticsearch/components/MetricPicker.tsx index 754a4e41ac7..51eb8003f9c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/MetricPicker.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/MetricPicker.tsx @@ -3,7 +3,7 @@ import { css, cx } from '@emotion/css'; import { SelectableValue } from '@grafana/data'; import { Segment } from '@grafana/ui'; -import { MetricAggregation } from '../types'; +import { MetricAggregation } from '../dataquery.gen'; import { describeMetric } from '../utils'; const noWrap = css({ diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/AnnotationQueryEditor.tsx index ee603a5f81b..3cca8867cd3 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/AnnotationQueryEditor.tsx @@ -2,13 +2,13 @@ import { AnnotationQuery } from '@grafana/data'; import { EditorField, EditorRow } from '@grafana/plugin-ui'; import { Input, Stack } from '@grafana/ui'; -import { ElasticsearchQuery } from '../../types'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { ElasticQueryEditorProps, ElasticSearchQueryField } from './index'; type Props = ElasticQueryEditorProps & { - annotation?: AnnotationQuery<ElasticsearchQuery>; - onAnnotationChange?: (annotation: AnnotationQuery<ElasticsearchQuery>) => void; + annotation?: AnnotationQuery<ElasticsearchDataQuery>; + onAnnotationChange?: (annotation: AnnotationQuery<ElasticsearchDataQuery>) => void; }; export function ElasticsearchAnnotationsQueryEditor(props: Props) { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/BucketAggregationEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/BucketAggregationEditor.tsx index 4b9910e2c7a..c2312d99e5e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/BucketAggregationEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/BucketAggregationEditor.tsx @@ -1,11 +1,11 @@ import { SelectableValue } from '@grafana/data'; import { InlineSegmentGroup, Segment, SegmentAsync } from '@grafana/ui'; +import { BucketAggregationType, BucketAggregation } from '../../../dataquery.gen'; import { useFields } from '../../../hooks/useFields'; import { useDispatch } from '../../../hooks/useStatelessReducer'; import { segmentStyles } from '../styles'; -import { BucketAggregation, BucketAggregationType } from './../../../types'; import { SettingsEditor } from './SettingsEditor'; import { isBucketAggregationWithField } from './aggregations'; import { changeBucketAggregationField, changeBucketAggregationType } from './state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx index 76cd65f3880..a9d0ad0710c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx @@ -1,8 +1,9 @@ import { render, screen } from '@testing-library/react'; import { select } from 'react-select-event'; +import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { useDispatch } from '../../../../hooks/useStatelessReducer'; -import { DateHistogram } from '../../../../types'; import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx index 5ea427c3984..0dda5b13703 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx @@ -4,10 +4,10 @@ import { GroupBase, OptionsOrGroups } from 'react-select'; import { InternalTimeZones, SelectableValue } from '@grafana/data'; import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui'; +import { DateHistogram } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { calendarIntervals } from '../../../../QueryBuilder'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; -import { DateHistogram } from '../../../../types'; import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour'; import { changeBucketAggregationSetting } from '../state/actions'; import { bucketAggregationConfig } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx index d3c47262de3..6fded5be996 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/index.tsx @@ -3,9 +3,9 @@ import { uniqueId } from 'lodash'; import { useEffect, useRef } from 'react'; import { InlineField, Input, QueryField } from '@grafana/ui'; +import { Filters } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useDispatch, useStatelessReducer } from '../../../../../hooks/useStatelessReducer'; -import { Filters } from '../../../../../types'; import { AddRemove } from '../../../../AddRemove'; import { changeBucketAggregationSetting } from '../../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts index 30edf04bec0..e60a664f066 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/actions.ts @@ -1,6 +1,6 @@ import { createAction } from '@reduxjs/toolkit'; -import { Filter } from '../../../../../../types'; +import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; export const addFilter = createAction('@bucketAggregations/filter/add'); export const removeFilter = createAction<number>('@bucketAggregations/filter/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts index a0c5f2cf1f8..56b5ac9555c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.test.ts @@ -1,6 +1,6 @@ import { reducerTester } from 'test/core/redux/reducerTester'; -import { Filter } from '../../../../../../types'; +import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { addFilter, changeFilter, removeFilter } from './actions'; import { reducer } from './reducer'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts index ab607b9f80e..022de8233b4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/state/reducer.ts @@ -1,6 +1,7 @@ import { Action } from 'redux'; -import { Filter } from '../../../../../../types'; +import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultFilter } from '../utils'; import { addFilter, changeFilter, removeFilter } from './actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts index a216fcf4c06..adf5646381d 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/FiltersSettingsEditor/utils.ts @@ -1,3 +1,3 @@ -import { Filter } from '../../../../../types'; +import { Filter } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; export const defaultFilter = (): Filter => ({ label: '', query: '*' }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx index a0bf21c4046..14862e8f664 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx @@ -1,9 +1,16 @@ import { fireEvent, screen } from '@testing-library/react'; import selectEvent from 'react-select-event'; +import { + Average, + Derivative, + ElasticsearchDataQuery, + Terms, + TopMetrics, +} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { renderWithESProvider } from '../../../../test-helpers/render'; -import { ElasticsearchQuery, Terms, Average, Derivative, TopMetrics } from '../../../../types'; import { describeMetric } from '../../../../utils'; import { TermsSettingsEditor } from './TermsSettingsEditor'; @@ -19,7 +26,7 @@ describe('Terms Settings Editor', () => { const avg: Average = { id: '2', type: 'avg', field: '@value' }; const derivative: Derivative = { id: '3', field: avg.id, type: 'derivative' }; const topMetrics: TopMetrics = { id: '4', type: 'top_metrics' }; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', bucketAggs: [termsAgg], @@ -56,7 +63,7 @@ describe('Terms Settings Editor', () => { const avg: Average = { id: '2', type: 'avg', field: '@value' }; const derivative: Derivative = { id: '3', field: avg.id, type: 'derivative' }; const topMetrics: TopMetrics = { id: '4', type: 'top_metrics' }; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', bucketAggs: [termsAgg], diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx index 80bbd154e9f..e1bc64980ab 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.tsx @@ -3,9 +3,15 @@ import { useRef } from 'react'; import { SelectableValue } from '@grafana/data'; import { InlineField, Select, Input } from '@grafana/ui'; +import { + Terms, + ExtendedStats, + ExtendedStatMetaType, + Percentiles, + MetricAggregation, +} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; -import { MetricAggregation, Percentiles, ExtendedStatMetaType, ExtendedStats, Terms } from '../../../../types'; import { describeMetric } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; import { isPipelineAggregation } from '../../MetricAggregationsEditor/aggregations'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx index b46778d39d9..9191ca25d1c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/index.tsx @@ -2,9 +2,9 @@ import { uniqueId } from 'lodash'; import { ComponentProps, useRef } from 'react'; import { InlineField, Input } from '@grafana/ui'; +import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; -import { BucketAggregation } from '../../../../types'; import { SettingsEditorContainer } from '../../SettingsEditorContainer'; import { changeBucketAggregationSetting } from '../state/actions'; import { bucketAggregationConfig } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts index 282df98b144..3e4e5cfea7c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts @@ -1,5 +1,6 @@ +import { BucketAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultGeoHashPrecisionString } from '../../../../queryDef'; -import { BucketAggregation } from '../../../../types'; import { describeMetric, convertOrderByToMetricId } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; import { bucketAggregationConfig, orderByOptions, orderOptions } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts index 6a60679da33..3500740d18e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/aggregations.ts @@ -1,4 +1,4 @@ -import { BucketAggregationType, BucketAggregationWithField, BucketAggregation } from '../../../types'; +import { BucketAggregation, BucketAggregationWithField, BucketAggregationType } from '../../../dataquery.gen'; import { bucketAggregationConfig } from './utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/index.tsx index 83578fc33b4..3e04a4a37e9 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/index.tsx @@ -1,10 +1,10 @@ import { Button } from '@grafana/ui'; +import { BucketAggregation } from '../../../dataquery.gen'; import { useDispatch } from '../../../hooks/useStatelessReducer'; import { useQuery } from '../ElasticsearchQueryContext'; import { QueryEditorRow } from '../QueryEditorRow'; -import { BucketAggregation } from './../../../types'; import { BucketAggregationEditor } from './BucketAggregationEditor'; import { addBucketAggregation, removeBucketAggregation } from './state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts index f128d5aa582..dfab9ac0279 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/actions.ts @@ -1,6 +1,10 @@ import { createAction } from '@reduxjs/toolkit'; -import { BucketAggregation, BucketAggregationType, BucketAggregationWithField } from '../../../../types'; +import { + BucketAggregation, + BucketAggregationType, + BucketAggregationWithField, +} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; export const addBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/add'); export const removeBucketAggregation = createAction<BucketAggregation['id']>('@bucketAggs/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts index bdb9616b91d..6a34d5d7d91 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts @@ -1,5 +1,10 @@ +import { + BucketAggregation, + DateHistogram, + ElasticsearchDataQuery, +} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultBucketAgg } from '../../../../queryDef'; -import { BucketAggregation, DateHistogram, ElasticsearchQuery } from '../../../../types'; import { reducerTester } from '../../../reducerTester'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; import { initQuery } from '../../state'; @@ -28,7 +33,7 @@ describe('Bucket Aggregations Reducer', () => { settings: bucketAggregationConfig['terms'].defaultSettings, }; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), []) .whenActionIsDispatched(addBucketAggregation(firstAggregation.id)) .thenStateShouldEqual([firstAggregation]) @@ -47,7 +52,7 @@ describe('Bucket Aggregations Reducer', () => { type: 'date_histogram', }; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation]) .whenActionIsDispatched(removeBucketAggregation(firstAggregation.id)) .thenStateShouldEqual([secondAggregation]); @@ -69,7 +74,7 @@ describe('Bucket Aggregations Reducer', () => { settings: bucketAggregationConfig['histogram'].defaultSettings, }; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation]) .whenActionIsDispatched( changeBucketAggregationType({ id: secondAggregation.id, newType: expectedSecondAggregation.type }) @@ -92,7 +97,7 @@ describe('Bucket Aggregations Reducer', () => { field: 'new field', }; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation]) .whenActionIsDispatched( changeBucketAggregationField({ id: secondAggregation.id, newField: expectedSecondAggregation.field }) @@ -109,7 +114,7 @@ describe('Bucket Aggregations Reducer', () => { }, ]; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), initialState) // If the new metric aggregation is non-metric, we should remove all bucket aggregations. .whenActionIsDispatched(changeMetricType({ id: 'Some id', type: 'raw_data' })) @@ -140,7 +145,7 @@ describe('Bucket Aggregations Reducer', () => { min_doc_count: '1', }; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), [firstAggregation, secondAggregation]) .whenActionIsDispatched( changeBucketAggregationSetting({ @@ -156,7 +161,7 @@ describe('Bucket Aggregations Reducer', () => { it('Correctly adds a default Date Histogram if there is no aggregation', () => { const defaultTimeField = '@timestamp'; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer(defaultTimeField), []) .whenActionIsDispatched(initQuery()) .thenStateShouldEqual([{ ...defaultBucketAgg('2'), field: defaultTimeField }]); @@ -169,7 +174,7 @@ describe('Bucket Aggregations Reducer', () => { field: '@my_time_field', }; - reducerTester<ElasticsearchQuery['bucketAggs']>() + reducerTester<ElasticsearchDataQuery['bucketAggs']>() .givenReducer(createReducer('@timestamp'), [bucketAgg]) .whenActionIsDispatched(initQuery()) .thenStateShouldEqual([bucketAgg]); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts index 935f7a10e07..b3638e1f1d1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts @@ -1,7 +1,8 @@ import { Action } from '@reduxjs/toolkit'; +import { BucketAggregation, ElasticsearchDataQuery, Terms } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultBucketAgg } from '../../../../queryDef'; -import { ElasticsearchQuery, Terms, BucketAggregation } from '../../../../types'; import { removeEmpty } from '../../../../utils'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; import { metricAggregationConfig } from '../../MetricAggregationsEditor/utils'; @@ -18,7 +19,7 @@ import { export const createReducer = (defaultTimeField: string) => - (state: ElasticsearchQuery['bucketAggs'], action: Action): ElasticsearchQuery['bucketAggs'] => { + (state: ElasticsearchDataQuery['bucketAggs'], action: Action): ElasticsearchDataQuery['bucketAggs'] => { if (addBucketAggregation.match(action)) { const newAgg: Terms = { id: action.payload, diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.test.tsx index 9e70d487267..45a78023954 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.test.tsx @@ -3,12 +3,12 @@ import { PropsWithChildren } from 'react'; import { getDefaultTimeRange } from '@grafana/data'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { ElasticDatasource } from '../../datasource'; -import { ElasticsearchQuery } from '../../types'; import { ElasticsearchProvider, useQuery } from './ElasticsearchQueryContext'; -const query: ElasticsearchQuery = { +const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [{ id: '1', type: 'count' }], @@ -31,7 +31,7 @@ describe('ElasticsearchQueryContext', () => { /> ); - const changedQuery: ElasticsearchQuery = onChange.mock.calls[0][0]; + const changedQuery: ElasticsearchDataQuery = onChange.mock.calls[0][0]; expect(changedQuery.query).toBeDefined(); expect(changedQuery.alias).toBeDefined(); expect(changedQuery.metrics).toBeDefined(); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.tsx index b2fd49cde44..57092ae9477 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/ElasticsearchQueryContext.tsx @@ -2,21 +2,21 @@ import { Context, createContext, PropsWithChildren, useCallback, useContext, use import { TimeRange } from '@grafana/data'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { ElasticDatasource } from '../../datasource'; import { combineReducers, useStatelessReducer, DispatchContext } from '../../hooks/useStatelessReducer'; -import { ElasticsearchQuery } from '../../types'; import { createReducer as createBucketAggsReducer } from './BucketAggregationsEditor/state/reducer'; import { reducer as metricsReducer } from './MetricAggregationsEditor/state/reducer'; import { aliasPatternReducer, queryReducer, initQuery } from './state'; const DatasourceContext = createContext<ElasticDatasource | undefined>(undefined); -const QueryContext = createContext<ElasticsearchQuery | undefined>(undefined); +const QueryContext = createContext<ElasticsearchDataQuery | undefined>(undefined); const RangeContext = createContext<TimeRange | undefined>(undefined); interface Props { - query: ElasticsearchQuery; - onChange: (query: ElasticsearchQuery) => void; + query: ElasticsearchDataQuery; + onChange: (query: ElasticsearchDataQuery) => void; onRunQuery: () => void; datasource: ElasticDatasource; range: TimeRange; @@ -31,7 +31,7 @@ export const ElasticsearchProvider = ({ range, }: PropsWithChildren<Props>) => { const onStateChange = useCallback( - (query: ElasticsearchQuery, prevQuery: ElasticsearchQuery) => { + (query: ElasticsearchDataQuery, prevQuery: ElasticsearchDataQuery) => { onChange(query); if (query.query === prevQuery.query || prevQuery.query === undefined) { onRunQuery(); @@ -40,7 +40,7 @@ export const ElasticsearchProvider = ({ [onChange, onRunQuery] ); - const reducer = combineReducers<Pick<ElasticsearchQuery, 'query' | 'alias' | 'metrics' | 'bucketAggs'>>({ + const reducer = combineReducers<Pick<ElasticsearchDataQuery, 'query' | 'alias' | 'metrics' | 'bucketAggs'>>({ query: queryReducer, alias: aliasPatternReducer, metrics: metricsReducer, diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx index 64130e3ff26..89754ab6e59 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.test.tsx @@ -5,12 +5,11 @@ import { from } from 'rxjs'; import { getDefaultTimeRange } from '@grafana/data'; +import { Average, Count, ElasticsearchDataQuery, UniqueCount } from '../../../dataquery.gen'; import { ElasticDatasource } from '../../../datasource'; import { defaultBucketAgg } from '../../../queryDef'; -import { ElasticsearchQuery } from '../../../types'; import { ElasticsearchProvider } from '../ElasticsearchQueryContext'; -import { Average, Count, UniqueCount } from './../../../types'; import { MetricEditor } from './MetricEditor'; describe('Metric Editor', () => { @@ -20,7 +19,7 @@ describe('Metric Editor', () => { type: 'avg', }; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [avg], @@ -56,7 +55,7 @@ describe('Metric Editor', () => { type: 'cardinality', }; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [avg], @@ -93,7 +92,7 @@ describe('Metric Editor', () => { type: 'count', }; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [count], diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx index 52a9c01617f..bbb9398055e 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/MetricEditor.tsx @@ -5,9 +5,9 @@ import { satisfies, SemVer } from 'semver'; import { SelectableValue } from '@grafana/data'; import { InlineSegmentGroup, SegmentAsync, useTheme2 } from '@grafana/ui'; +import { MetricAggregation, MetricAggregationType } from '../../../dataquery.gen'; import { useFields } from '../../../hooks/useFields'; import { useDispatch } from '../../../hooks/useStatelessReducer'; -import { MetricAggregation, MetricAggregationType } from '../../../types'; import { MetricPicker } from '../../MetricPicker'; import { useDatasource, useQuery } from '../ElasticsearchQueryContext'; import { segmentStyles } from '../styles'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx index 62f38e2820b..70d53eddbc4 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/index.tsx @@ -3,9 +3,9 @@ import { uniqueId } from 'lodash'; import { Fragment, useEffect } from 'react'; import { Input, InlineLabel } from '@grafana/ui'; +import { BucketScript, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useStatelessReducer, useDispatch } from '../../../../../hooks/useStatelessReducer'; -import { BucketScript, MetricAggregation } from '../../../../../types'; import { AddRemove } from '../../../../AddRemove'; import { MetricPicker } from '../../../../MetricPicker'; import { changeMetricAttribute } from '../../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts index b9ecf0749e7..02fc628ecc3 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts @@ -1,4 +1,5 @@ -import { PipelineVariable } from '../../../../../../types'; +import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { reducerTester } from '../../../../../reducerTester'; import { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts index 455037ee5c2..406b1f5b590 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.ts @@ -1,6 +1,7 @@ import { Action } from '@reduxjs/toolkit'; -import { PipelineVariable } from '../../../../../../types'; +import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultPipelineVariable, generatePipelineVariableName } from '../utils'; import { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts index fdade3f9e1f..e2da781d190 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/utils.ts @@ -1,4 +1,4 @@ -import { PipelineVariable } from '../../../../../types'; +import { PipelineVariable } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; export const defaultPipelineVariable = (name: string): PipelineVariable => ({ name, pipelineAgg: '' }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx index aadca71e0ed..e4a3ad907a9 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx @@ -2,9 +2,12 @@ import { uniqueId } from 'lodash'; import { ComponentProps, useState } from 'react'; import { InlineField, Input, TextArea } from '@grafana/ui'; +import { + MetricAggregationWithSettings, + MetricAggregationWithInlineScript, +} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; -import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../../../../types'; import { getScriptValue } from '../../../../utils'; import { SettingKeyOf } from '../../../types'; import { changeMetricSetting } from '../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/TopMetricsSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/TopMetricsSettingsEditor.tsx index cea24cbfcd4..841a0ba5aff 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/TopMetricsSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/TopMetricsSettingsEditor.tsx @@ -2,10 +2,10 @@ import { css } from '@emotion/css'; import { SelectableValue } from '@grafana/data'; import { AsyncMultiSelect, InlineField, SegmentAsync, Select } from '@grafana/ui'; +import { TopMetrics } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useFields } from '../../../../hooks/useFields'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; -import { TopMetrics } from '../../../../types'; import { orderOptions } from '../../BucketAggregationsEditor/utils'; import { changeMetricSetting } from '../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.test.tsx index f8b18d13388..019cb75b9bc 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.test.tsx @@ -1,9 +1,9 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { getDefaultTimeRange } from '@grafana/data'; +import { ElasticsearchDataQuery } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { ElasticDatasource } from '../../../../datasource'; -import { ElasticsearchQuery } from '../../../../types'; import { ElasticsearchProvider } from '../../ElasticsearchQueryContext'; import { SettingsEditor } from '.'; @@ -13,7 +13,7 @@ describe('Settings Editor', () => { it('Should correctly render the settings editor and trigger correct state changes', () => { const metricId = '1'; const initialSize = '500'; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [ @@ -88,7 +88,7 @@ describe('Settings Editor', () => { describe('Rate aggregation', () => { it('should render correct settings', () => { const metricId = '1'; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [ diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx index e632d90d7cb..caa746eb56b 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx @@ -3,10 +3,10 @@ import { ComponentProps, useId, useRef, useState } from 'react'; import * as React from 'react'; import { InlineField, Input, InlineSwitch, Select } from '@grafana/ui'; +import { MetricAggregation, ExtendedStat } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { extendedStats } from '../../../../queryDef'; -import { MetricAggregation, ExtendedStat } from '../../../../types'; import { SettingsEditorContainer } from '../../SettingsEditorContainer'; import { isMetricAggregationWithInlineScript, isMetricAggregationWithMissingSupport } from '../aggregations'; import { changeMetricMeta, changeMetricSetting } from '../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/useDescription.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/useDescription.ts index c160fb940e4..44f15bc8660 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/useDescription.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/useDescription.ts @@ -1,5 +1,6 @@ +import { MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { extendedStats } from '../../../../queryDef'; -import { MetricAggregation } from '../../../../types'; const hasValue = (value: string) => (object: { value: string }) => object.value === value; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts index 509c4263054..5ee73ff6fd2 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/aggregations.ts @@ -1,15 +1,14 @@ import { - PipelineMetricAggregationWithMultipleBucketPaths, - MetricAggregationWithMeta, - MovingAverage, - MetricAggregationType, BaseMetricAggregation, MetricAggregationWithField, - MetricAggregationWithMissingSupport, - MetricAggregationWithInlineScript, PipelineMetricAggregation, + PipelineMetricAggregationWithMultipleBucketPaths, + MetricAggregationWithMissingSupport, MetricAggregationWithSettings, -} from '../../../types'; + MetricAggregationWithInlineScript, + MetricAggregationType, +} from '../../../dataquery.gen'; +import { MetricAggregationWithMeta, MovingAverage } from '../../../types'; import { metricAggregationConfig } from './utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx index cc4901eb1d1..9323afef551 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/index.tsx @@ -1,11 +1,11 @@ import { Alert, Button } from '@grafana/ui'; +import { MetricAggregation } from '../../../dataquery.gen'; import { useDispatch } from '../../../hooks/useStatelessReducer'; import { useQuery } from '../ElasticsearchQueryContext'; import { QueryEditorRow } from '../QueryEditorRow'; import { QueryEditorSpecialMetricRow } from '../QueryEditorSpecialMetricRow'; -import { MetricAggregation } from './../../../types'; import { MetricEditor } from './MetricEditor'; import { addMetric, removeMetric, toggleMetricVisibility } from './state/actions'; import { metricAggregationConfig } from './utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts index e7dc68aa1bd..f304e4876c1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/actions.ts @@ -1,6 +1,8 @@ import { createAction } from '@reduxjs/toolkit'; -import { MetricAggregationWithMeta, MetricAggregation, MetricAggregationWithSettings } from '../../../../types'; +import { MetricAggregation, MetricAggregationWithSettings } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + +import { MetricAggregationWithMeta } from '../../../../types'; export const addMetric = createAction<MetricAggregation['id']>('@metrics/add'); export const removeMetric = createAction<MetricAggregation['id']>('@metrics/remove'); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts index 0719b2024c8..5662ad399ea 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts @@ -1,5 +1,11 @@ +import { + MetricAggregation, + ElasticsearchDataQuery, + Derivative, + ExtendedStats, +} from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultMetricAgg } from '../../../../queryDef'; -import { Derivative, ElasticsearchQuery, ExtendedStats, MetricAggregation } from '../../../../types'; import { reducerTester } from '../../../reducerTester'; import { initQuery } from '../../state'; import { metricAggregationConfig } from '../utils'; @@ -27,7 +33,7 @@ describe('Metric Aggregations Reducer', () => { type: 'count', }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, []) .whenActionIsDispatched(addMetric(firstAggregation.id)) .thenStateShouldEqual([firstAggregation]) @@ -46,7 +52,7 @@ describe('Metric Aggregations Reducer', () => { type: 'count', }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched(removeMetric(firstAggregation.id)) .thenStateShouldEqual([secondAggregation]); @@ -55,7 +61,7 @@ describe('Metric Aggregations Reducer', () => { it('Should insert a default aggregation when the last one is removed', () => { const initialState: MetricAggregation[] = [{ id: '2', type: 'avg' }]; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, initialState) .whenActionIsDispatched(removeMetric(initialState[0].id)) .thenStateShouldEqual([defaultMetricAgg()]); @@ -75,7 +81,7 @@ describe('Metric Aggregations Reducer', () => { const expectedSecondAggregation: MetricAggregation = { ...secondAggregation, type: 'avg' }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched(changeMetricType({ id: secondAggregation.id, type: expectedSecondAggregation.type })) .thenStateShouldEqual([firstAggregation, { ...secondAggregation, type: expectedSecondAggregation.type }]); @@ -97,7 +103,7 @@ describe('Metric Aggregations Reducer', () => { ...metricAggregationConfig['raw_data'].defaults, }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched(changeMetricType({ id: secondAggregation.id, type: expectedAggregation.type })) .thenStateShouldEqual([expectedAggregation]); @@ -125,7 +131,7 @@ describe('Metric Aggregations Reducer', () => { field: 'new field', }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) // When changing a a pipelineAggregation field we set both pipelineAgg and field .whenActionIsDispatched(changeMetricField({ id: secondAggregation.id, field: expectedSecondAggregation.field })) @@ -146,7 +152,7 @@ describe('Metric Aggregations Reducer', () => { type: 'count', }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched(toggleMetricVisibility(firstAggregation.id)) .thenStateShouldEqual([{ ...firstAggregation, hide: true }, secondAggregation]) @@ -171,7 +177,7 @@ describe('Metric Aggregations Reducer', () => { unit: 'Changed unit', }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched( changeMetricSetting({ metric: firstAggregation, settingName: 'unit', newValue: expectedSettings.unit! }) @@ -196,7 +202,7 @@ describe('Metric Aggregations Reducer', () => { avg: false, }; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched(changeMetricMeta({ metric: firstAggregation, meta: 'avg', newValue: expectedMeta.avg! })) .thenStateShouldEqual([{ ...firstAggregation, meta: expectedMeta }, secondAggregation]); @@ -214,7 +220,7 @@ describe('Metric Aggregations Reducer', () => { const expectedHide: (typeof firstAggregation)['hide'] = false; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, [firstAggregation, secondAggregation]) .whenActionIsDispatched( changeMetricAttribute({ metric: firstAggregation, attribute: 'hide', newValue: expectedHide }) @@ -230,14 +236,14 @@ describe('Metric Aggregations Reducer', () => { }, ]; - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, initialState) .whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' }) .thenStateShouldEqual(initialState); }); it('Should correctly initialize first Metric Aggregation', () => { - reducerTester<ElasticsearchQuery['metrics']>() + reducerTester<ElasticsearchDataQuery['metrics']>() .givenReducer(reducer, []) .whenActionIsDispatched(initQuery()) .thenStateShouldEqual([defaultMetricAgg('1')]); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts index a1b8fcb860a..d44a57569f7 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts @@ -1,7 +1,8 @@ import { Action } from '@reduxjs/toolkit'; +import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasource/elasticsearch/dataquery.gen'; + import { defaultMetricAgg } from '../../../../queryDef'; -import { ElasticsearchQuery, MetricAggregation } from '../../../../types'; import { removeEmpty } from '../../../../utils'; import { initQuery } from '../../state'; import { isMetricAggregationWithMeta, isMetricAggregationWithSettings, isPipelineAggregation } from '../aggregations'; @@ -18,7 +19,10 @@ import { toggleMetricVisibility, } from './actions'; -export const reducer = (state: ElasticsearchQuery['metrics'], action: Action): ElasticsearchQuery['metrics'] => { +export const reducer = ( + state: ElasticsearchDataQuery['metrics'], + action: Action +): ElasticsearchDataQuery['metrics'] => { if (addMetric.match(action)) { return [...state!, defaultMetricAgg(action.payload)]; } diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/utils.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/utils.ts index ad27d6aebea..05118baeb45 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/utils.ts @@ -1,4 +1,5 @@ -import { MetricsConfiguration, MetricAggregation, PipelineMetricAggregationType } from '../../../types'; +import { PipelineMetricAggregationType, MetricAggregation } from '../../../dataquery.gen'; +import { MetricsConfiguration } from '../../../types'; import { defaultPipelineVariable, diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx index ccd8ba1ffe4..4dec4ab7571 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryEditorSpecialMetricRow.tsx @@ -1,6 +1,6 @@ import { InlineFieldRow, InlineLabel, InlineSegmentGroup } from '@grafana/ui'; -import { MetricAggregation } from '../../types'; +import { MetricAggregation } from '../../dataquery.gen'; import { SettingsEditor } from './MetricAggregationsEditor/SettingsEditor'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx index 695237f706f..bdfd1336d8b 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/QueryTypeSelector.tsx @@ -1,8 +1,9 @@ import { SelectableValue } from '@grafana/data'; import { RadioButtonGroup } from '@grafana/ui'; +import { MetricAggregation } from '../../dataquery.gen'; import { useDispatch } from '../../hooks/useStatelessReducer'; -import { MetricAggregation, QueryType } from '../../types'; +import { QueryType } from '../../types'; import { useQuery } from './ElasticsearchQueryContext'; import { changeMetricType } from './MetricAggregationsEditor/state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx index 0a5f34abba5..580496f0a27 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { ElasticDatasource } from '../../datasource'; -import { ElasticsearchQuery } from '../../types'; import { QueryEditor } from '.'; @@ -14,7 +14,7 @@ describe('QueryEditor', () => { describe('Alias Field', () => { it('Should correctly render and trigger changes on blur', () => { const alias = '{{metric}}'; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', alias, @@ -32,7 +32,7 @@ describe('QueryEditor', () => { ], }; - const onChange = jest.fn<void, [ElasticsearchQuery]>(); + const onChange = jest.fn<void, [ElasticsearchDataQuery]>(); render(<QueryEditor query={query} datasource={datasourceMock} onChange={onChange} onRunQuery={noop} />); @@ -56,7 +56,7 @@ describe('QueryEditor', () => { }); it('Should not be shown if last bucket aggregation is not Date Histogram', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [ @@ -74,7 +74,7 @@ describe('QueryEditor', () => { }); it('Should be shown if last bucket aggregation is Date Histogram', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [ @@ -93,7 +93,7 @@ describe('QueryEditor', () => { }); it('Should NOT show Bucket Aggregations Editor if query contains a "singleMetric" metric', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [ @@ -112,7 +112,7 @@ describe('QueryEditor', () => { }); it('Should show Bucket Aggregations Editor if query does NOT contains a "singleMetric" metric', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [ diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx index 6eebd6e843a..927ef6aa544 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx @@ -5,10 +5,11 @@ import { SemVer } from 'semver'; import { getDefaultTimeRange, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; import { Alert, InlineField, InlineLabel, Input, QueryField, useStyles2 } from '@grafana/ui'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { ElasticDatasource } from '../../datasource'; import { useNextId } from '../../hooks/useNextId'; import { useDispatch } from '../../hooks/useStatelessReducer'; -import { ElasticsearchOptions, ElasticsearchQuery } from '../../types'; +import { ElasticsearchOptions } from '../../types'; import { isSupportedVersion, isTimeSeriesQuery, unsupportedVersionMessage } from '../../utils'; import { BucketAggregationsEditor } from './BucketAggregationsEditor'; @@ -18,7 +19,7 @@ import { metricAggregationConfig } from './MetricAggregationsEditor/utils'; import { QueryTypeSelector } from './QueryTypeSelector'; import { changeAliasPattern, changeQuery } from './state'; -export type ElasticQueryEditorProps = QueryEditorProps<ElasticDatasource, ElasticsearchQuery, ElasticsearchOptions>; +export type ElasticQueryEditorProps = QueryEditorProps<ElasticDatasource, ElasticsearchDataQuery, ElasticsearchOptions>; // a react hook that returns the elasticsearch database version, // or `null`, while loading, or if it is not possible to determine the value. @@ -74,7 +75,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ }); interface Props { - value: ElasticsearchQuery; + value: ElasticsearchDataQuery; } export const ElasticSearchQueryField = ({ value, onChange }: { value?: string; onChange: (v: string) => void }) => { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts index 987551baf51..cad89cd32a7 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts @@ -1,4 +1,4 @@ -import { ElasticsearchQuery } from '../../types'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { reducerTester } from '../reducerTester'; import { aliasPatternReducer, changeAliasPattern, changeQuery, initQuery, queryReducer } from './state'; @@ -6,19 +6,19 @@ import { aliasPatternReducer, changeAliasPattern, changeQuery, initQuery, queryR describe('Query Reducer', () => { describe('On Init', () => { it('Should maintain the previous `query` if present', () => { - const initialQuery: ElasticsearchQuery['query'] = 'Some lucene query'; + const initialQuery: ElasticsearchDataQuery['query'] = 'Some lucene query'; - reducerTester<ElasticsearchQuery['query']>() + reducerTester<ElasticsearchDataQuery['query']>() .givenReducer(queryReducer, initialQuery) .whenActionIsDispatched(initQuery()) .thenStateShouldEqual(initialQuery); }); it('Should set an empty `query` if it is not already set', () => { - const initialQuery: ElasticsearchQuery['query'] = undefined; + const initialQuery: ElasticsearchDataQuery['query'] = undefined; const expectedQuery = ''; - reducerTester<ElasticsearchQuery['query']>() + reducerTester<ElasticsearchDataQuery['query']>() .givenReducer(queryReducer, initialQuery) .whenActionIsDispatched(initQuery()) .thenStateShouldEqual(expectedQuery); @@ -26,18 +26,18 @@ describe('Query Reducer', () => { }); it('Should correctly set `query`', () => { - const expectedQuery: ElasticsearchQuery['query'] = 'Some lucene query'; + const expectedQuery: ElasticsearchDataQuery['query'] = 'Some lucene query'; - reducerTester<ElasticsearchQuery['query']>() + reducerTester<ElasticsearchDataQuery['query']>() .givenReducer(queryReducer, '') .whenActionIsDispatched(changeQuery(expectedQuery)) .thenStateShouldEqual(expectedQuery); }); it('Should not change state with other action types', () => { - const initialState: ElasticsearchQuery['query'] = 'Some lucene query'; + const initialState: ElasticsearchDataQuery['query'] = 'Some lucene query'; - reducerTester<ElasticsearchQuery['query']>() + reducerTester<ElasticsearchDataQuery['query']>() .givenReducer(queryReducer, initialState) .whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' }) .thenStateShouldEqual(initialState); @@ -46,18 +46,18 @@ describe('Query Reducer', () => { describe('Alias Pattern Reducer', () => { it('Should correctly set `alias`', () => { - const expectedAlias: ElasticsearchQuery['alias'] = 'Some alias pattern'; + const expectedAlias: ElasticsearchDataQuery['alias'] = 'Some alias pattern'; - reducerTester<ElasticsearchQuery['query']>() + reducerTester<ElasticsearchDataQuery['query']>() .givenReducer(aliasPatternReducer, '') .whenActionIsDispatched(changeAliasPattern(expectedAlias)) .thenStateShouldEqual(expectedAlias); }); it('Should not change state with other action types', () => { - const initialState: ElasticsearchQuery['alias'] = 'Some alias pattern'; + const initialState: ElasticsearchDataQuery['alias'] = 'Some alias pattern'; - reducerTester<ElasticsearchQuery['query']>() + reducerTester<ElasticsearchDataQuery['query']>() .givenReducer(aliasPatternReducer, initialState) .whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' }) .thenStateShouldEqual(initialState); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts index 98182f056d6..4785e371642 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts @@ -1,6 +1,6 @@ import { Action, createAction } from '@reduxjs/toolkit'; -import { ElasticsearchQuery } from '../../types'; +import { ElasticsearchDataQuery } from '../../dataquery.gen'; /** * When the `initQuery` Action is dispatched, the query gets populated with default values where values are not present. @@ -8,11 +8,11 @@ import { ElasticsearchQuery } from '../../types'; */ export const initQuery = createAction('init'); -export const changeQuery = createAction<ElasticsearchQuery['query']>('change_query'); +export const changeQuery = createAction<ElasticsearchDataQuery['query']>('change_query'); -export const changeAliasPattern = createAction<ElasticsearchQuery['alias']>('change_alias_pattern'); +export const changeAliasPattern = createAction<ElasticsearchDataQuery['alias']>('change_alias_pattern'); -export const queryReducer = (prevQuery: ElasticsearchQuery['query'], action: Action) => { +export const queryReducer = (prevQuery: ElasticsearchDataQuery['query'], action: Action) => { if (changeQuery.match(action)) { return action.payload; } @@ -24,7 +24,7 @@ export const queryReducer = (prevQuery: ElasticsearchQuery['query'], action: Act return prevQuery; }; -export const aliasPatternReducer = (prevAliasPattern: ElasticsearchQuery['alias'], action: Action) => { +export const aliasPatternReducer = (prevAliasPattern: ElasticsearchDataQuery['alias'], action: Action) => { if (changeAliasPattern.match(action)) { return action.payload; } diff --git a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts index 38c466afa2b..79e2fddbc2d 100644 --- a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts +++ b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts @@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; type GrafanaReducer<S = StoreState, A extends Action = AnyAction> = (state: S, action: A) => S; diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index f80d969bfb0..33c3486bd31 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -14,9 +14,9 @@ import { } from '@grafana/data'; import { FetchResponse, reportInteraction, getBackendSrv, setBackendSrv, BackendSrv, config } from '@grafana/runtime'; +import { ElasticsearchDataQuery, Filters } from './dataquery.gen'; import { ElasticDatasource } from './datasource'; import { createElasticDatasource, createElasticQuery, mockResponseFrames } from './mocks'; -import { Filters, ElasticsearchQuery } from './types'; const originalConsoleError = console.error; jest.mock('@grafana/runtime', () => ({ @@ -270,7 +270,7 @@ describe('ElasticDatasource', () => { refId: 'A', metrics: [{ type: 'logs', id: '1' }], query: 'foo="bar"', - } as ElasticsearchQuery, + } as ElasticsearchDataQuery, ], app: CoreApp.Explore, }; @@ -299,7 +299,7 @@ describe('ElasticDatasource', () => { refId: 'A', metrics: [{ type: 'raw_data', id: '1' }], query: 'foo="bar"', - } as ElasticsearchQuery, + } as ElasticsearchDataQuery, ], app: CoreApp.Explore, }; @@ -328,7 +328,7 @@ describe('ElasticDatasource', () => { refId: 'A', metrics: [{ type: 'raw_data', id: '1' }], query: 'foo="bar"', - } as ElasticsearchQuery, + } as ElasticsearchDataQuery, ], app: CoreApp.Dashboard, }; @@ -342,7 +342,7 @@ describe('ElasticDatasource', () => { describe('interpolateVariablesInQueries', () => { it('should correctly interpolate variables in query', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '$var', label: '' }] }, id: '1' }], metrics: [{ type: 'count', id: '1' }], @@ -356,7 +356,7 @@ describe('ElasticDatasource', () => { it('should correctly add ad hoc filters when interpolating variables in query', () => { const adHocFilters = [{ key: 'bar', operator: '=', value: 'test' }]; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '$var', label: '' }] }, id: '1' }], metrics: [{ type: 'count', id: '1' }], @@ -368,7 +368,7 @@ describe('ElasticDatasource', () => { }); it('should correctly handle empty query strings in filters bucket aggregation', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', bucketAggs: [{ type: 'filters', settings: { filters: [{ query: '', label: '' }] }, id: '1' }], metrics: [{ type: 'count', id: '1' }], @@ -491,7 +491,7 @@ describe('ElasticDatasource', () => { describe('getDataProvider', () => { it('does not create a logs sample provider for non time series query', () => { - const options: DataQueryRequest<ElasticsearchQuery> = { + const options: DataQueryRequest<ElasticsearchDataQuery> = { ...dataQueryDefaults, targets: [ { @@ -505,7 +505,7 @@ describe('ElasticDatasource', () => { }); it('does not create a logs volume provider for hidden queries', () => { - const options: DataQueryRequest<ElasticsearchQuery> = { + const options: DataQueryRequest<ElasticsearchDataQuery> = { ...dataQueryDefaults, targets: [ { @@ -520,7 +520,7 @@ describe('ElasticDatasource', () => { }); it('does create a logs sample provider for time series query', () => { - const options: DataQueryRequest<ElasticsearchQuery> = { + const options: DataQueryRequest<ElasticsearchDataQuery> = { ...dataQueryDefaults, targets: [ { @@ -536,7 +536,7 @@ describe('ElasticDatasource', () => { describe('getLogsSampleDataProvider', () => { it("doesn't return a logs sample provider given a non time series query", () => { - const request: DataQueryRequest<ElasticsearchQuery> = { + const request: DataQueryRequest<ElasticsearchDataQuery> = { ...dataQueryDefaults, targets: [ { @@ -550,7 +550,7 @@ describe('ElasticDatasource', () => { }); it('returns a logs sample provider given a time series query', () => { - const request: DataQueryRequest<ElasticsearchQuery> = { + const request: DataQueryRequest<ElasticsearchDataQuery> = { ...dataQueryDefaults, targets: [ { @@ -577,7 +577,7 @@ describe('ElasticDatasource', () => { }); describe('modifyQuery', () => { - let query: ElasticsearchQuery; + let query: ElasticsearchDataQuery; beforeEach(() => { query = { query: '', refId: 'A' }; }); @@ -603,7 +603,7 @@ describe('ElasticDatasource', () => { }); describe('with non-empty query', () => { - let query: ElasticsearchQuery; + let query: ElasticsearchDataQuery; beforeEach(() => { query = { query: 'test:"value"', refId: 'A' }; }); @@ -645,7 +645,7 @@ describe('ElasticDatasource', () => { }); describe('with non-empty query', () => { - let query: ElasticsearchQuery; + let query: ElasticsearchDataQuery; beforeEach(() => { query = { query: 'test:"value"', refId: 'A' }; }); @@ -665,7 +665,7 @@ describe('ElasticDatasource', () => { describe('toggleQueryFilter', () => { describe('with empty query', () => { - let query: ElasticsearchQuery; + let query: ElasticsearchDataQuery; beforeEach(() => { query = { query: '', refId: 'A' }; }); @@ -698,7 +698,7 @@ describe('ElasticDatasource', () => { }); describe('with non-empty query', () => { - let query: ElasticsearchQuery; + let query: ElasticsearchDataQuery; beforeEach(() => { query = { query: 'test:"value"', refId: 'A' }; }); @@ -844,7 +844,7 @@ describe('ElasticDatasource', () => { }); describe('targetContainsTemplate', () => { - let target: ElasticsearchQuery; + let target: ElasticsearchDataQuery; beforeEach(() => { target = { refId: 'test', diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index bccb678babf..af5bbbd6114 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -60,6 +60,7 @@ import { isPipelineAggregationWithMultipleBucketPaths, } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; +import { ElasticsearchDataQuery, BucketAggregation } from './dataquery.gen'; import { isMetricAggregationWithMeta } from './guards'; import { addAddHocFilter, @@ -71,10 +72,8 @@ import { import { trackAnnotationQuery, trackQuery } from './tracking'; import { Logs, - BucketAggregation, DataLinkConfig, ElasticsearchOptions, - ElasticsearchQuery, TermsQuery, Interval, ElasticsearchAnnotationQuery, @@ -103,13 +102,13 @@ const ELASTIC_META_FIELDS = [ ]; export class ElasticDatasource - extends DataSourceWithBackend<ElasticsearchQuery, ElasticsearchOptions> + extends DataSourceWithBackend<ElasticsearchDataQuery, ElasticsearchOptions> implements DataSourceWithLogsContextSupport, - DataSourceWithQueryImportSupport<ElasticsearchQuery>, - DataSourceWithSupplementaryQueriesSupport<ElasticsearchQuery>, - DataSourceWithToggleableQueryFiltersSupport<ElasticsearchQuery>, - DataSourceWithQueryModificationSupport<ElasticsearchQuery> + DataSourceWithQueryImportSupport<ElasticsearchDataQuery>, + DataSourceWithSupplementaryQueriesSupport<ElasticsearchDataQuery>, + DataSourceWithToggleableQueryFiltersSupport<ElasticsearchDataQuery>, + DataSourceWithQueryModificationSupport<ElasticsearchDataQuery> { basicAuth?: string; withCredentials?: boolean; @@ -182,7 +181,7 @@ export class ElasticDatasource * Imports queries from AbstractQuery objects when switching between different data source types. * @returns A Promise that resolves to an array of ES queries. */ - async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<ElasticsearchQuery[]> { + async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise<ElasticsearchDataQuery[]> { return abstractQueries.map((abstractQuery) => this.languageProvider.importFromAbstractQuery(abstractQuery)); } @@ -434,10 +433,10 @@ export class ElasticDatasource * @returns An array of ES queries with interpolated variables and ad hoc filters using `applyTemplateVariables`. */ interpolateVariablesInQueries( - queries: ElasticsearchQuery[], + queries: ElasticsearchDataQuery[], scopedVars: ScopedVars, filters?: AdHocVariableFilter[] - ): ElasticsearchQuery[] { + ): ElasticsearchDataQuery[] { return queries.map((q) => this.applyTemplateVariables(q, scopedVars, filters)); } @@ -457,7 +456,7 @@ export class ElasticDatasource * Used, for example, in Query history. * @returns A text representation of the query. */ - getQueryDisplayText(query: ElasticsearchQuery) { + getQueryDisplayText(query: ElasticsearchDataQuery) { // TODO: This might be refactored a bit. const metricAggs = query.metrics; const bucketAggs = query.bucketAggs; @@ -535,8 +534,8 @@ export class ElasticDatasource */ getSupplementaryRequest( type: SupplementaryQueryType, - request: DataQueryRequest<ElasticsearchQuery> - ): DataQueryRequest<ElasticsearchQuery> | undefined { + request: DataQueryRequest<ElasticsearchDataQuery> + ): DataQueryRequest<ElasticsearchDataQuery> | undefined { switch (type) { case SupplementaryQueryType.LogsVolume: return this.getLogsVolumeDataProvider(request); @@ -561,7 +560,10 @@ export class ElasticDatasource * It retrieves supplementary queries based on the provided options and ES query. * @returns A supplemented ES query or undefined if unsupported. */ - getSupplementaryQuery(options: SupplementaryQueryOptions, query: ElasticsearchQuery): ElasticsearchQuery | undefined { + getSupplementaryQuery( + options: SupplementaryQueryOptions, + query: ElasticsearchDataQuery + ): ElasticsearchDataQuery | undefined { let isQuerySuitable = false; if (query.hide) { @@ -642,12 +644,12 @@ export class ElasticDatasource * @returns An Observable of DataQueryResponse or undefined if no suitable queries are found. */ private getLogsVolumeDataProvider( - request: DataQueryRequest<ElasticsearchQuery> - ): DataQueryRequest<ElasticsearchQuery> | undefined { + request: DataQueryRequest<ElasticsearchDataQuery> + ): DataQueryRequest<ElasticsearchDataQuery> | undefined { const logsVolumeRequest = cloneDeep(request); const targets = logsVolumeRequest.targets .map((target) => this.getSupplementaryQuery({ type: SupplementaryQueryType.LogsVolume }, target)) - .filter((query): query is ElasticsearchQuery => !!query); + .filter((query): query is ElasticsearchDataQuery => !!query); if (!targets.length) { return undefined; @@ -661,14 +663,14 @@ export class ElasticDatasource * @returns An Observable of DataQueryResponse or undefined if no suitable queries are found. */ private getLogsSampleDataProvider( - request: DataQueryRequest<ElasticsearchQuery> - ): DataQueryRequest<ElasticsearchQuery> | undefined { + request: DataQueryRequest<ElasticsearchDataQuery> + ): DataQueryRequest<ElasticsearchDataQuery> | undefined { const logsSampleRequest = cloneDeep(request); const targets = logsSampleRequest.targets; const queries = targets.map((query) => { return this.getSupplementaryQuery({ type: SupplementaryQueryType.LogsSample, limit: 100 }, query); }); - const elasticQueries = queries.filter((query): query is ElasticsearchQuery => !!query); + const elasticQueries = queries.filter((query): query is ElasticsearchDataQuery => !!query); if (!elasticQueries.length) { return undefined; @@ -680,7 +682,7 @@ export class ElasticDatasource * Required by DataSourceApi. It executes queries based on the provided DataQueryRequest. * @returns An Observable of DataQueryResponse containing the query results. */ - query(request: DataQueryRequest<ElasticsearchQuery>): Observable<DataQueryResponse> { + query(request: DataQueryRequest<ElasticsearchDataQuery>): Observable<DataQueryResponse> { const start = new Date(); return super.query(request).pipe( tap((response) => trackQuery(response, request, start)), @@ -698,7 +700,7 @@ export class ElasticDatasource * It is called from DatasourceWithBackend. * @returns `true` if the query is not hidden. */ - filterQuery(query: ElasticsearchQuery): boolean { + filterQuery(query: ElasticsearchDataQuery): boolean { if (query.hide) { return false; } @@ -952,7 +954,7 @@ export class ElasticDatasource * Implemented as part of the DataSourceAPI. Retrieves tag values that can be used for ad-hoc filtering. * @returns A Promise that resolves to an array of label values represented as MetricFindValue objects */ - getTagValues(options: DataSourceGetTagValuesOptions<ElasticsearchQuery>) { + getTagValues(options: DataSourceGetTagValuesOptions<ElasticsearchDataQuery>) { return lastValueFrom(this.getTerms({ field: options.key }, options.timeRange, true)); } @@ -960,7 +962,7 @@ export class ElasticDatasource * Implemented as part of the DataSourceAPI. * Used by alerting to check if query contains template variables. */ - targetContainsTemplate(target: ElasticsearchQuery) { + targetContainsTemplate(target: ElasticsearchDataQuery) { if (this.templateSrv.containsTemplate(target.query) || this.templateSrv.containsTemplate(target.alias)) { return true; } @@ -1025,7 +1027,7 @@ export class ElasticDatasource * It is used for example in Explore to toggle fields on and off trough log details. * @returns A new ES query with the filter toggled as specified. */ - toggleQueryFilter(query: ElasticsearchQuery, filter: ToggleFilterAction): ElasticsearchQuery { + toggleQueryFilter(query: ElasticsearchDataQuery, filter: ToggleFilterAction): ElasticsearchDataQuery { let expression = query.query ?? ''; switch (filter.type) { case 'FILTER_FOR': { @@ -1052,7 +1054,7 @@ export class ElasticDatasource * Implemented for `DataSourceWithToggleableQueryFiltersSupport`. Checks if a query expression contains a filter based on the provided filter options. * @returns A boolean value indicating whether the filter exists in the query expression. */ - queryHasFilter(query: ElasticsearchQuery, options: QueryFilterOptions): boolean { + queryHasFilter(query: ElasticsearchDataQuery, options: QueryFilterOptions): boolean { let expression = query.query ?? ''; return queryHasFilter(expression, options.key, options.value); } @@ -1062,7 +1064,7 @@ export class ElasticDatasource * It is used, for example, in the Query Builder to apply hints such as parsers, operations, etc. * @returns A new ES query with the specified modification applied. */ - modifyQuery(query: ElasticsearchQuery, action: QueryFixAction): ElasticsearchQuery { + modifyQuery(query: ElasticsearchDataQuery, action: QueryFixAction): ElasticsearchDataQuery { if (!action.options) { return query; } @@ -1120,10 +1122,10 @@ export class ElasticDatasource * @returns A modified ES query with template variables and ad hoc filters applied. */ applyTemplateVariables( - query: ElasticsearchQuery, + query: ElasticsearchDataQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[] - ): ElasticsearchQuery { + ): ElasticsearchDataQuery { // We need a separate interpolation format for lucene queries, therefore we first interpolate any // lucene query string and then everything else const interpolateBucketAgg = (bucketAgg: BucketAggregation): BucketAggregation => { @@ -1211,7 +1213,7 @@ export class ElasticDatasource }, }; - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: `log-context-${row.dataFrame.refId}-${direction}`, metrics: [logQuery], query: '', @@ -1226,7 +1228,7 @@ export class ElasticDatasource const interval = rangeUtil.calculateInterval(range, 1); - const contextRequest: DataQueryRequest<ElasticsearchQuery> = { + const contextRequest: DataQueryRequest<ElasticsearchDataQuery> = { requestId: `log-context-request-${row.dataFrame.refId}-${options?.direction}`, targets: [query], interval: interval.interval, diff --git a/public/app/plugins/datasource/elasticsearch/hooks/useFields.test.tsx b/public/app/plugins/datasource/elasticsearch/hooks/useFields.test.tsx index 5751aa51acd..96b515208e7 100644 --- a/public/app/plugins/datasource/elasticsearch/hooks/useFields.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/hooks/useFields.test.tsx @@ -5,9 +5,9 @@ import { from } from 'rxjs'; import { getDefaultTimeRange } from '@grafana/data'; import { ElasticsearchProvider } from '../components/QueryEditor/ElasticsearchQueryContext'; +import { ElasticsearchDataQuery, BucketAggregationType, MetricAggregationType } from '../dataquery.gen'; import { ElasticDatasource } from '../datasource'; import { defaultBucketAgg, defaultMetricAgg } from '../queryDef'; -import { ElasticsearchQuery, MetricAggregationType, BucketAggregationType } from '../types'; import { useFields } from './useFields'; @@ -16,7 +16,7 @@ describe('useFields hook', () => { // we can stop testing for getField to be called with the correct parameters. it("returns a function that calls datasource's getFields with the correct parameters", async () => { const timeRange = getDefaultTimeRange(); - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [defaultMetricAgg()], diff --git a/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts b/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts index df9d1271431..dd37c344fb8 100644 --- a/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts +++ b/public/app/plugins/datasource/elasticsearch/hooks/useFields.ts @@ -5,7 +5,7 @@ import { MetricFindValue, SelectableValue } from '@grafana/data'; import { isBucketAggregationType } from '../components/QueryEditor/BucketAggregationsEditor/aggregations'; import { useDatasource, useRange } from '../components/QueryEditor/ElasticsearchQueryContext'; import { isMetricAggregationType } from '../components/QueryEditor/MetricAggregationsEditor/aggregations'; -import { MetricAggregationType, BucketAggregationType } from '../types'; +import { BucketAggregationType, MetricAggregationType } from '../dataquery.gen'; type AggregationType = BucketAggregationType | MetricAggregationType; diff --git a/public/app/plugins/datasource/elasticsearch/hooks/useNextId.test.tsx b/public/app/plugins/datasource/elasticsearch/hooks/useNextId.test.tsx index e37c4d1e2bc..41b76e753b4 100644 --- a/public/app/plugins/datasource/elasticsearch/hooks/useNextId.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/hooks/useNextId.test.tsx @@ -4,14 +4,14 @@ import { PropsWithChildren } from 'react'; import { getDefaultTimeRange } from '@grafana/data'; import { ElasticsearchProvider } from '../components/QueryEditor/ElasticsearchQueryContext'; +import { ElasticsearchDataQuery } from '../dataquery.gen'; import { ElasticDatasource } from '../datasource'; -import { ElasticsearchQuery } from '../types'; import { useNextId } from './useNextId'; describe('useNextId', () => { it('Should return the next available id', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: 'A', query: '', metrics: [{ id: '1', type: 'avg' }], diff --git a/public/app/plugins/datasource/elasticsearch/hooks/useNextId.ts b/public/app/plugins/datasource/elasticsearch/hooks/useNextId.ts index 05bcd970419..758ac477442 100644 --- a/public/app/plugins/datasource/elasticsearch/hooks/useNextId.ts +++ b/public/app/plugins/datasource/elasticsearch/hooks/useNextId.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useQuery } from '../components/QueryEditor/ElasticsearchQueryContext'; -import { MetricAggregation, BucketAggregation } from '../types'; +import { MetricAggregation, BucketAggregation } from '../dataquery.gen'; const toId = <T extends { id: unknown }>(e: T): T['id'] => e.id; diff --git a/public/app/plugins/datasource/elasticsearch/mocks.ts b/public/app/plugins/datasource/elasticsearch/mocks.ts index 484a284bc55..e907103c71c 100644 --- a/public/app/plugins/datasource/elasticsearch/mocks.ts +++ b/public/app/plugins/datasource/elasticsearch/mocks.ts @@ -1,8 +1,9 @@ import { CoreApp, DataQueryRequest, DataSourceInstanceSettings, FieldType, PluginType, dateTime } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import { ElasticDatasource } from './datasource'; -import { ElasticsearchOptions, ElasticsearchQuery } from './types'; +import { ElasticsearchOptions } from './types'; export function createElasticDatasource( settings: Partial<DataSourceInstanceSettings<Partial<ElasticsearchOptions>>> = {} @@ -63,7 +64,7 @@ export function createElasticDatasource( return new ElasticDatasource(instanceSettings, templateSrv); } -export const createElasticQuery = (): DataQueryRequest<ElasticsearchQuery> => { +export const createElasticQuery = (): DataQueryRequest<ElasticsearchDataQuery> => { return { requestId: '', interval: '', diff --git a/public/app/plugins/datasource/elasticsearch/module.ts b/public/app/plugins/datasource/elasticsearch/module.ts index d6c0b285eb1..6edac3168d2 100644 --- a/public/app/plugins/datasource/elasticsearch/module.ts +++ b/public/app/plugins/datasource/elasticsearch/module.ts @@ -3,11 +3,11 @@ import { getAppEvents } from '@grafana/runtime'; import { QueryEditor } from './components/QueryEditor'; import { ConfigEditor } from './configuration/ConfigEditor'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import { ElasticDatasource } from './datasource'; import { onDashboardLoadedHandler } from './tracking'; -import { ElasticsearchQuery } from './types'; export const plugin = new DataSourcePlugin(ElasticDatasource).setQueryEditor(QueryEditor).setConfigEditor(ConfigEditor); // Subscribe to on dashboard loaded event so that we can track plugin adoption -getAppEvents().subscribe<DashboardLoadedEvent<ElasticsearchQuery>>(DashboardLoadedEvent, onDashboardLoadedHandler); +getAppEvents().subscribe<DashboardLoadedEvent<ElasticsearchDataQuery>>(DashboardLoadedEvent, onDashboardLoadedHandler); diff --git a/public/app/plugins/datasource/elasticsearch/queryDef.ts b/public/app/plugins/datasource/elasticsearch/queryDef.ts index 77a8a01b824..c343acec449 100644 --- a/public/app/plugins/datasource/elasticsearch/queryDef.ts +++ b/public/app/plugins/datasource/elasticsearch/queryDef.ts @@ -1,12 +1,12 @@ import { metricAggregationConfig, pipelineOptions } from './components/QueryEditor/MetricAggregationsEditor/utils'; import { - ElasticsearchQuery, + DateHistogram, + ElasticsearchDataQuery, ExtendedStat, MetricAggregation, - MovingAverageModelOption, MetricAggregationType, - DateHistogram, -} from './types'; + MovingAverageModelOption, +} from './dataquery.gen'; export const extendedStats: ExtendedStat[] = [ { label: 'Avg', value: 'avg' }, @@ -45,7 +45,7 @@ export function defaultBucketAgg(id = '1'): DateHistogram { export const findMetricById = (metrics: MetricAggregation[], id: MetricAggregation['id']) => metrics.find((metric) => metric.id === id); -export function hasMetricOfType(target: ElasticsearchQuery, type: MetricAggregationType): boolean { +export function hasMetricOfType(target: ElasticsearchDataQuery, type: MetricAggregationType): boolean { return !!target?.metrics?.some((m) => m.type === type); } diff --git a/public/app/plugins/datasource/elasticsearch/tracking.test.ts b/public/app/plugins/datasource/elasticsearch/tracking.test.ts index 81a86a388af..177c743700d 100644 --- a/public/app/plugins/datasource/elasticsearch/tracking.test.ts +++ b/public/app/plugins/datasource/elasticsearch/tracking.test.ts @@ -1,16 +1,16 @@ import { DashboardLoadedEvent } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import pluginJson from './plugin.json'; import { onDashboardLoadedHandler } from './tracking'; -import { ElasticsearchQuery } from './types'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), reportInteraction: jest.fn(), })); -const targets: ElasticsearchQuery[] = [ +const targets: ElasticsearchDataQuery[] = [ { refId: 'test', alias: '$varAlias', diff --git a/public/app/plugins/datasource/elasticsearch/tracking.ts b/public/app/plugins/datasource/elasticsearch/tracking.ts index 01e3c106dfc..8d98dc15e5d 100644 --- a/public/app/plugins/datasource/elasticsearch/tracking.ts +++ b/public/app/plugins/datasource/elasticsearch/tracking.ts @@ -1,9 +1,10 @@ import { CoreApp, DashboardLoadedEvent, DataQueryRequest, DataQueryResponse } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import { REF_ID_STARTER_LOG_VOLUME } from './datasource'; import pluginJson from './plugin.json'; -import { ElasticsearchAnnotationQuery, ElasticsearchQuery } from './types'; +import { ElasticsearchAnnotationQuery } from './types'; import { variableRegex } from './utils'; type ElasticSearchOnDashboardLoadedTrackingEvent = { @@ -38,7 +39,7 @@ type ElasticSearchOnDashboardLoadedTrackingEvent = { export const onDashboardLoadedHandler = ({ payload: { dashboardId, orgId, grafanaVersion, queries }, -}: DashboardLoadedEvent<ElasticsearchQuery>) => { +}: DashboardLoadedEvent<ElasticsearchDataQuery>) => { try { // We only want to track visible ElasticSearch queries const elasticsearchQueries = queries[pluginJson.id]?.filter((query) => !query.hide); @@ -74,7 +75,7 @@ export const onDashboardLoadedHandler = ({ } }; -const getQueryType = (query: ElasticsearchQuery): string | undefined => { +const getQueryType = (query: ElasticsearchDataQuery): string | undefined => { if (!query.metrics || !query.metrics.length) { return undefined; } @@ -85,7 +86,7 @@ const getQueryType = (query: ElasticsearchQuery): string | undefined => { return 'metric'; }; -const getLineLimit = (query: ElasticsearchQuery): number | undefined => { +const getLineLimit = (query: ElasticsearchDataQuery): number | undefined => { if (query.metrics?.[0]?.type !== 'logs') { return undefined; } @@ -94,12 +95,12 @@ const getLineLimit = (query: ElasticsearchQuery): number | undefined => { return lineLimit ? parseInt(lineLimit, 10) : undefined; }; -const isQueryWithChangedLineLimit = (query: ElasticsearchQuery): boolean => { +const isQueryWithChangedLineLimit = (query: ElasticsearchDataQuery): boolean => { const lineLimit = getLineLimit(query); return lineLimit !== undefined && lineLimit !== 500; }; -const isQueryWithTemplateVariables = (query: ElasticsearchQuery): boolean => { +const isQueryWithTemplateVariables = (query: ElasticsearchDataQuery): boolean => { return variableRegex.test(query.query ?? ''); }; @@ -112,7 +113,7 @@ const shouldNotReportBasedOnRefId = (refId: string): boolean => { export function trackQuery( response: DataQueryResponse, - request: DataQueryRequest<ElasticsearchQuery> & { targets: ElasticsearchQuery[] }, + request: DataQueryRequest<ElasticsearchDataQuery> & { targets: ElasticsearchDataQuery[] }, startTime: Date ): void { const { targets: queries, app } = request; diff --git a/public/app/plugins/datasource/elasticsearch/types.ts b/public/app/plugins/datasource/elasticsearch/types.ts index 635fa81e2c7..b48ab801ae8 100644 --- a/public/app/plugins/datasource/elasticsearch/types.ts +++ b/public/app/plugins/datasource/elasticsearch/types.ts @@ -18,9 +18,6 @@ import { ElasticsearchDataQuery, } from './dataquery.gen'; -export * from './dataquery.gen'; -export type { ElasticsearchDataQuery as ElasticsearchQuery } from './dataquery.gen'; - // We want to extend the settings of the Logs query with additional properties that // are not part of the schema. This is a workaround, because exporting LogsSettings // from dataquery.gen.ts and extending that produces error in SettingKeyOf. diff --git a/public/app/plugins/datasource/elasticsearch/utils.test.ts b/public/app/plugins/datasource/elasticsearch/utils.test.ts index 731efa9aefe..17a96c97fbc 100644 --- a/public/app/plugins/datasource/elasticsearch/utils.test.ts +++ b/public/app/plugins/datasource/elasticsearch/utils.test.ts @@ -1,4 +1,4 @@ -import { ElasticsearchQuery } from './types'; +import { ElasticsearchDataQuery } from './dataquery.gen'; import { flattenObject, isTimeSeriesQuery, removeEmpty } from './utils'; describe('removeEmpty', () => { @@ -38,7 +38,7 @@ describe('removeEmpty', () => { describe('isTimeSeriesQuery', () => { it('should return false when given a log query', () => { - const logsQuery: ElasticsearchQuery = { + const logsQuery: ElasticsearchDataQuery = { refId: `A`, metrics: [{ type: 'logs', id: '1' }], }; @@ -47,7 +47,7 @@ describe('isTimeSeriesQuery', () => { }); it('should return false when bucket aggs are empty', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: `A`, bucketAggs: [], }; @@ -56,7 +56,7 @@ describe('isTimeSeriesQuery', () => { }); it('returns false when empty date_histogram is not last', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: `A`, bucketAggs: [ { id: '1', type: 'date_histogram' }, @@ -68,7 +68,7 @@ describe('isTimeSeriesQuery', () => { }); it('returns true when empty date_histogram is last', () => { - const query: ElasticsearchQuery = { + const query: ElasticsearchDataQuery = { refId: `A`, bucketAggs: [ { id: '1', type: 'terms' }, diff --git a/public/app/plugins/datasource/elasticsearch/utils.ts b/public/app/plugins/datasource/elasticsearch/utils.ts index 1ca679734cf..74427d746ba 100644 --- a/public/app/plugins/datasource/elasticsearch/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/utils.ts @@ -2,7 +2,7 @@ import { gte, SemVer } from 'semver'; import { isMetricAggregationWithField } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; -import { ElasticsearchQuery, MetricAggregation, MetricAggregationWithInlineScript } from './types'; +import { ElasticsearchDataQuery, MetricAggregation, MetricAggregationWithInlineScript } from './dataquery.gen'; export const describeMetric = (metric: MetricAggregation) => { if (!isMetricAggregationWithField(metric)) { @@ -103,7 +103,7 @@ export const unsupportedVersionMessage = 'Support for Elasticsearch versions after their end-of-life (currently versions < 7.16) was removed. Using unsupported version of Elasticsearch may lead to unexpected and incorrect results.'; // To be considered a time series query, the last bucked aggregation must be a Date Histogram -export const isTimeSeriesQuery = (query: ElasticsearchQuery): boolean => { +export const isTimeSeriesQuery = (query: ElasticsearchDataQuery): boolean => { return query?.bucketAggs?.slice(-1)[0]?.type === 'date_histogram'; }; diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx index 5d8cb9aae06..389110e9517 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx @@ -153,7 +153,11 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')} /> </Field> + </ConfigSection> + <Divider /> + + <ConfigSection title="TLS/SSL Auth Details" isCollapsible> <Field label={ <Label> @@ -180,7 +184,6 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P width={WIDTH_LONG} /> </Field> - {options.jsonData.sslmode !== PostgresTLSModes.disable ? ( <Field label={ @@ -217,12 +220,8 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P /> </Field> ) : null} - </ConfigSection> - - {jsonData.sslmode !== PostgresTLSModes.disable ? ( - <> - <Divider /> - <ConfigSection title="TLS/SSL Auth Details"> + {jsonData.sslmode !== PostgresTLSModes.disable ? ( + <> {jsonData.tlsConfigurationMethod === PostgresTLSMethods.fileContent ? ( <TLSSecretsConfig showCACert={ @@ -314,9 +313,9 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P </Field> </> )} - </ConfigSection> - </> - ) : null} + </> + ) : null} + </ConfigSection> <Divider /> diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index 9dc6e60aa20..2acfab5815d 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -22,20 +22,20 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts index 55a880fef5c..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/webpack.config.ts @@ -1,4 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; // eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 43228b83c8e..3fcf2bb95c9 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", @@ -35,16 +35,16 @@ "jest": "29.7.0", "style-loader": "4.0.0", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/pyroscopeql/pyroscopeql.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/pyroscopeql/pyroscopeql.ts index 6f39f5aec5c..cddc8f7d4b5 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/pyroscopeql/pyroscopeql.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/pyroscopeql/pyroscopeql.ts @@ -31,7 +31,7 @@ export const language: languages.IMonarchLanguage = { digits: /\d+(_+\d+)*/, octaldigits: /[0-7]+(_+[0-7]+)*/, binarydigits: /[0-1]+(_+[0-1]+)*/, - hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/, + hexdigits: /[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/, integersuffix: /(ll|LL|u|U|l|L)?(ll|LL|u|U|l|L)?/, floatsuffix: /[fFlL]?/, diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts index 8e03497ddb2..f48f8b1a693 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/webpack.config.ts @@ -1,6 +1,6 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; -const configWithFallback = async (env: Record<string, unknown>) => { +const configWithFallback = async (env: Env) => { const response = await config(env); if (response !== undefined && response.resolve !== undefined) { response.resolve.fallback = { diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx index 95b67bf1f0c..3fc5d4b84c2 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx @@ -5,7 +5,6 @@ import { QueryEditorProps, SelectableValue } from '@grafana/data'; import { selectors as editorSelectors } from '@grafana/e2e-selectors'; import { InlineField, InlineFieldRow, InlineSwitch, Input, Select, Icon, TextArea } from '@grafana/ui'; -import { RandomWalkEditor, StreamingClientEditor } from './components'; import { CSVContentEditor } from './components/CSVContentEditor'; import { CSVFileEditor } from './components/CSVFileEditor'; import { CSVWavesEditor } from './components/CSVWaveEditor'; @@ -14,8 +13,10 @@ import ErrorWithSourceQueryEditor from './components/ErrorWithSourceEditor'; import { GrafanaLiveEditor } from './components/GrafanaLiveEditor'; import { NodeGraphEditor } from './components/NodeGraphEditor'; import { PredictablePulseEditor } from './components/PredictablePulseEditor'; +import { RandomWalkEditor } from './components/RandomWalkEditor'; import { RawFrameEditor } from './components/RawFrameEditor'; import { SimulationQueryEditor } from './components/SimulationQueryEditor'; +import { StreamingClientEditor } from './components/StreamingClientEditor'; import { USAQueryEditor, usaQueryModes } from './components/USAQueryEditor'; import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constants'; import { CSVWave, NodesQuery, TestDataDataQuery, TestDataQueryType, USAQuery } from './dataquery'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx index 0bdcc29f0c1..0f745337a7d 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx @@ -47,7 +47,7 @@ export const RandomWalkEditor = ({ onChange, query }: EditorProps) => { id={`randomWalk-${id}-${query.refId}`} min={min} step={step} - value={(query as any)[id as keyof TestDataDataQuery] || placeholder} + value={(query as any)[id as keyof TestDataDataQuery] ?? placeholder} placeholder={placeholder} onChange={onChange} /> diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/index.ts b/public/app/plugins/datasource/grafana-testdata-datasource/components/index.ts deleted file mode 100644 index 66ff3418758..00000000000 --- a/public/app/plugins/datasource/grafana-testdata-datasource/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { StreamingClientEditor } from './StreamingClientEditor'; -export { RandomWalkEditor } from './RandomWalkEditor'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 3a494f7bd86..0fe0876a9c8 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -14,7 +14,7 @@ "micro-memoize": "^4.1.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1", @@ -28,22 +28,22 @@ "@testing-library/user-event": "14.6.1", "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/uuid": "10.0.0", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts b/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts +++ b/public/app/plugins/datasource/grafana-testdata-datasource/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index 9b1a9918edf..8f799c3e35a 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -32,7 +32,8 @@ import { Stack, } from '@grafana/ui'; import { hasAlphaPanels } from 'app/core/config'; -import * as DFImport from 'app/features/dataframe-import'; +import { acceptedFiles, maxFileSize } from 'app/features/dataframe-import/constants'; +import { filesToDataframes } from 'app/features/dataframe-import/utils'; import { getManagedChannelInfo } from 'app/features/live/info'; import { SearchQuery } from 'app/features/search/service/types'; @@ -360,7 +361,7 @@ export class UnthemedQueryEditor extends PureComponent<Props, State> { }; onFileDrop = (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => { - DFImport.filesToDataframes(acceptedFiles).subscribe((next) => { + filesToDataframes(acceptedFiles).subscribe((next) => { const snapshot: DataFrameJSON[] = []; next.dataFrames.forEach((df) => { const dataframeJson = dataFrameToJSON(df); @@ -406,9 +407,9 @@ export class UnthemedQueryEditor extends PureComponent<Props, State> { fileListRenderer={this.fileListRenderer} options={{ onDrop: this.onFileDrop, - maxSize: DFImport.maxFileSize, + maxSize: maxFileSize, multiple: false, - accept: DFImport.acceptedFiles, + accept: acceptedFiles, }} > <FileDropzoneDefaultChildren diff --git a/public/app/plugins/datasource/grafana/utils.ts b/public/app/plugins/datasource/grafana/utils.ts index 3b89cab87bd..e907d285d13 100644 --- a/public/app/plugins/datasource/grafana/utils.ts +++ b/public/app/plugins/datasource/grafana/utils.ts @@ -4,7 +4,7 @@ import { DataFrame, DataFrameJSON, dataFrameToJSON } from '@grafana/data'; import appEvents from 'app/core/app_events'; import { GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import * as DFImport from 'app/features/dataframe-import'; +import { filesToDataframes } from 'app/features/dataframe-import/utils'; import { ShowConfirmModalEvent } from 'app/types/events'; import { defaultFileUploadQuery, GrafanaQuery, GrafanaQueryType } from './types'; @@ -62,7 +62,7 @@ export function getFileDropToQueryHandler( onFileLoaded: (query: GrafanaQuery, fileRejections: FileRejection[]) => void ) { return (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => { - DFImport.filesToDataframes(acceptedFiles).subscribe(async (next) => { + filesToDataframes(acceptedFiles).subscribe(async (next) => { const snapshot: DataFrameJSON[] = []; next.dataFrames.forEach((df: DataFrame) => { const dataframeJson = dataFrameToJSON(df); diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx index db5f9172a67..9e72fbb491e 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.test.tsx @@ -36,55 +36,55 @@ describe('UrlAndAuthenticationSection', () => { expect(onOptionsChangeMock).toHaveBeenCalled(); }); - it('renders DRBP warning for InfluxDB OSS 1.x and InfluxQL', () => { - const props = { - ...defaultProps, - options: { - ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.InfluxQL }, - }, - }; + const productsRequiringDBRP = [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise 1.x', + 'InfluxDB Cloud (TSM)', + 'InfluxDB Cloud Serverless', + ]; - render(<UrlAndAuthenticationSection {...props} />); - expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument(); + describe('UrlAndAuthenticationSection', () => { + it.each(productsRequiringDBRP)('renders DBRP warning for %s and InfluxQL', (product) => { + const props = { + ...defaultProps, + options: { + ...defaultProps.options, + jsonData: { + product, + version: InfluxVersion.InfluxQL, + }, + }, + }; + + render(<UrlAndAuthenticationSection {...props} />); + expect(screen.getByText(/requires DBRP mapping/i)).toBeInTheDocument(); + }); }); - it('renders DRBP warning for InfluxDB OSS 2.x and InfluxQL', () => { + it('does not render DBRP warning for SQL', () => { const props = { ...defaultProps, options: { ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.InfluxQL }, + jsonData: { version: InfluxVersion.SQL }, }, }; render(<UrlAndAuthenticationSection {...props} />); - expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument(); + expect(screen.queryByText(/requires DBRP mapping/i)).not.toBeInTheDocument(); }); - it('does not render DRBP warning for InfluxDB OSS 1.x and Flux', () => { + it('does not render DBRP warning for Flux', () => { const props = { ...defaultProps, options: { ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.Flux }, + jsonData: { version: InfluxVersion.Flux }, }, }; render(<UrlAndAuthenticationSection {...props} />); - expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument(); - }); - - it('does not render DRBP warning for InfluxDB OSS 2.x and Flux', () => { - const props = { - ...defaultProps, - options: { - ...defaultProps.options, - jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.Flux }, - }, - }; - - render(<UrlAndAuthenticationSection {...props} />); - expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/requires DBRP mapping/i)).not.toBeInTheDocument(); }); }); diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx index 7b0ff3fcd69..49e4a2074aa 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/UrlAndAuthenticationSection.tsx @@ -38,10 +38,16 @@ export const UrlAndAuthenticationSection = (props: Props) => { typeof v === 'string' && (v === InfluxVersion.Flux || v === InfluxVersion.InfluxQL || v === InfluxVersion.SQL); // Database + Retention Policy (DBRP) mapping is required for InfluxDB OSS 1.x and 2.x when using InfluxQL - const requiresDrbpMapping = + const requiresDbrpMapping = options.jsonData.product && options.jsonData.version === InfluxVersion.InfluxQL && - ['InfluxDB OSS 1.x', 'InfluxDB OSS 2.x'].includes(options.jsonData.product); + [ + 'InfluxDB OSS 1.x', + 'InfluxDB OSS 2.x', + 'InfluxDB Enterprise 1.x', + 'InfluxDB Cloud (TSM)', + 'InfluxDB Cloud Serverless', + ].includes(options.jsonData.product); const onProductChange = ({ value }: ComboboxOption) => { trackInfluxDBConfigV2ProductSelected({ product: value }); @@ -113,10 +119,10 @@ export const UrlAndAuthenticationSection = (props: Props) => { <Space v={2} /> - {requiresDrbpMapping && ( - <Alert severity="warning" title="InfluxQL requires DRBP mapping"> - InfluxDB OSS 1.x and 2.x users must configure a Database + Retention Policy (DBRP) mapping via the CLI or - API before data can be queried.{' '} + {requiresDbrpMapping && ( + <Alert severity="warning" title="InfluxQL requires DBRP mapping"> + {`${options.jsonData.product} requires a Database + Retention Policy (DBRP) mapping via the CLI or + API before data can be queried.`}{' '} <TextLink href="https://docs.influxdata.com/influxdb/cloud/query-data/influxql/dbrp/" external> Learn how to set this up </TextLink> diff --git a/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts b/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts index 3ae3122bffd..07b4b3fc35e 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts +++ b/public/app/plugins/datasource/influxdb/components/editor/config-v2/constants.ts @@ -30,8 +30,8 @@ export const CONFIG_SECTION_HEADERS_WITH_PDC = [ ]; export const HTTP_MODES: ComboboxOption[] = [ - { label: 'GET', value: 'GET' }, { label: 'POST', value: 'POST' }, + { label: 'GET', value: 'GET' }, ]; export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false, width?: number | 'auto') => { diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts index 2fa37357816..5c0961d4ac7 100644 --- a/public/app/plugins/datasource/jaeger/datasource.test.ts +++ b/public/app/plugins/datasource/jaeger/datasource.test.ts @@ -1,4 +1,4 @@ -import { lastValueFrom, of, throwError } from 'rxjs'; +import { lastValueFrom, of } from 'rxjs'; import { DataQueryRequest, @@ -9,18 +9,12 @@ import { PluginType, ScopedVars, } from '@grafana/data'; -import { BackendSrv, config, DataSourceWithBackend } from '@grafana/runtime'; +import { BackendSrv, DataSourceWithBackend } from '@grafana/runtime'; -import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { JaegerDatasource, JaegerJsonData } from './datasource'; -import { createFetchResponse } from './helpers/createFetchResponse'; import mockJson from './mockJsonResponse.json'; -import { - testResponse, - testResponseDataFrameFields, - testResponseEdgesFields, - testResponseNodesFields, -} from './testResponse'; +import mockSearchResponse from './mockSearchResponse.json'; +import mockTraceResponse from './mockTraceResponse.json'; import { JaegerQuery } from './types'; export const backendSrv = { fetch: jest.fn() } as unknown as BackendSrv; @@ -38,128 +32,29 @@ jest.mock('@grafana/runtime', () => ({ }), })); -const defaultQuery: DataQueryRequest<JaegerQuery> = { - requestId: '1', - interval: '0', - intervalMs: 10, - panelId: 0, - scopedVars: {}, - range: { - from: dateTime().subtract(1, 'h'), - to: dateTime(), - raw: { from: '1h', to: 'now' }, - }, - timezone: 'browser', - app: 'explore', - startTime: 0, - targets: [ - { - query: '12345', - refId: '1', - }, - ], -}; - -describe('JaegerDatasource', () => { - const defaultSearchRangeParams = `start=${Number(defaultQuery.range.from) * 1000}&end=${Number(defaultQuery.range.to) * 1000}`; - - beforeEach(() => { - jest.clearAllMocks(); - - const fetchMock = jest.spyOn(Date, 'now'); - fetchMock.mockImplementation(() => 1704106800000); // milliseconds for 2024-01-01 at 11:00am UTC - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('returns trace and graph when queried', async () => { - setupFetchMock({ data: [testResponse] }); - - const ds = new JaegerDatasource(defaultSettings); - const response = await lastValueFrom(ds.query(defaultQuery)); - expect(response.data.length).toBe(3); - expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields); - expect(response.data[1].fields).toMatchObject(testResponseNodesFields); - expect(response.data[2].fields).toMatchObject(testResponseEdgesFields); - }); - - it('returns trace when traceId with special characters is queried', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const query = { - ...defaultQuery, - targets: [ - { - query: 'a/b', - refId: '1', - }, - ], - }; - await lastValueFrom(ds.query(query)); - expect(mock).toHaveBeenCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` }); - }); - - it('should trim whitespace from traceid', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const query = { - ...defaultQuery, - targets: [ - { - query: 'a/b ', - refId: '1', - }, - ], - }; - await lastValueFrom(ds.query(query)); - expect(mock).toHaveBeenCalledWith({ url: `${defaultSettings.url}/api/traces/a%2Fb` }); - }); - - it('returns empty response if trace id is not specified', async () => { - const ds = new JaegerDatasource(defaultSettings); - const response = await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [], - }) - ); - const field = response.data[0].fields[0]; - expect(field.name).toBe('trace'); - expect(field.type).toBe(FieldType.trace); - expect(field.values.length).toBe(0); - }); - - it('should handle json file upload', async () => { +describe('upload, search and trace query types', () => { + it('should process valid JSON file uploads', async () => { const ds = new JaegerDatasource(defaultSettings); ds.uploadedJson = JSON.stringify(mockJson); - const response = await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [{ queryType: 'upload', refId: 'A' }], - }) - ); + const response = await lastValueFrom(ds.query({ ...defaultQuery, targets: [{ queryType: 'upload', refId: 'A' }] })); const field = response.data[0].fields[0]; expect(field.name).toBe('traceID'); expect(field.type).toBe(FieldType.string); expect(field.values.length).toBe(2); }); - it('should fail on invalid json file upload', async () => { + it('should reject invalid JSON file uploads', async () => { const ds = new JaegerDatasource(defaultSettings); ds.uploadedJson = JSON.stringify({ key: 'value', arr: [] }); const response = await lastValueFrom( - ds.query({ - targets: [{ queryType: 'upload', refId: 'A' }], - } as DataQueryRequest<JaegerQuery>) + ds.query({ targets: [{ queryType: 'upload', refId: 'A' }] } as DataQueryRequest<JaegerQuery>) ); expect(response.error?.message).toBe('The JSON file uploaded is not in a valid Jaeger format'); expect(response.data.length).toBe(0); }); - it('should return search results when the query type is search', async () => { - const mock = setupFetchMock({ data: [testResponse] }); + it('should return search results when query type is search', async () => { + setupQueryMock('search'); const ds = new JaegerDatasource(defaultSettings); const response = await lastValueFrom( ds.query({ @@ -167,185 +62,66 @@ describe('JaegerDatasource', () => { targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: '/api/services' }], }) ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&operation=%2Fapi%2Fservices&${defaultSearchRangeParams}&lookback=custom`, - }); + expect(response.data[0].meta.preferredVisualisationType).toBe('table'); - // Make sure that traceID field has data link configured expect(response.data[0].fields[0].config.links).toHaveLength(1); expect(response.data[0].fields[0].name).toBe('traceID'); }); - it('uses default range when no range is provided for search query,', async () => { - const mock = setupFetchMock({ data: [testResponse] }); + it('should return trace results when query type is trace', async () => { + setupQueryMock('trace'); const ds = new JaegerDatasource(defaultSettings); - const query = { - ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }], - // set range to undefined to test default range - range: undefined, - } as unknown as DataQueryRequest<JaegerQuery>; + const response = await lastValueFrom( + ds.query({ ...defaultQuery, targets: [{ queryType: undefined, refId: 'a', query: '12345' }] }) + ); - ds.query(query); - expect(mock).toHaveBeenCalledWith({ - // Check that query has time range from 6 hours ago to now (default range) - url: `${defaultSettings.url}/api/traces?service=jaeger-query&start=1704085200000000&end=1704106800000000&lookback=custom`, - }); + expect(response.data[0].meta.preferredVisualisationType).toBe('trace'); + expect(response.data[0].fields.length).toBe(7); }); +}); + +describe('node graph functionality', () => { + it('should include node graph frames when nodeGraph is enabled for trace queries', async () => { + const settingsWithNodeGraph = { + ...defaultSettings, + jsonData: { + ...defaultSettings.jsonData, + nodeGraph: { enabled: true }, + }, + }; + + const ds = new JaegerDatasource(settingsWithNodeGraph); + setupQueryMock('trace'); - it('should show the correct error message if no service name is selected', async () => { - const ds = new JaegerDatasource(defaultSettings); const response = await lastValueFrom( ds.query({ ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: undefined, operation: '/api/services' }], - }) - ); - expect(response.error?.message).toBe('You must select a service.'); - }); - - it('should remove operation from the query when all is selected', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', operation: ALL_OPERATIONS_KEY }], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - it('should convert tags from logfmt format to an object', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - await lastValueFrom( - ds.query({ - ...defaultQuery, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=true' }], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - it('should resolve templates in traceID', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - - await lastValueFrom( - ds.query({ - ...defaultQuery, - scopedVars: { - $traceid: { - text: 'traceid', - value: '5311b0dd0ca8df3463df93c99cb805a6', - }, - }, targets: [ { - query: '$traceid', + query: '12345', refId: '1', }, ], }) ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces/5311b0dd0ca8df3463df93c99cb805a6`, - }); + + expect(response.data.length).toBe(3); }); - it('should resolve templates in tags', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - await lastValueFrom( + it('should exclude node graph frames when nodeGraph is disabled for trace queries', async () => { + const settingsWithoutNodeGraph = { + ...defaultSettings, + jsonData: { + ...defaultSettings.jsonData, + nodeGraph: { enabled: false }, + }, + }; + + const ds = new JaegerDatasource(settingsWithoutNodeGraph); + setupQueryMock('trace'); + + const response = await lastValueFrom( ds.query({ - ...defaultQuery, - scopedVars: { - 'error=$error': { - text: 'error', - value: 'error=true', - }, - }, - targets: [{ queryType: 'search', refId: 'a', service: 'jaeger-query', tags: 'error=$error' }], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=jaeger-query&tags=%7B%22error%22%3A%22true%22%7D&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - it('should interpolate variables correctly', async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const text = 'interpolationText'; - await lastValueFrom( - ds.query({ - ...defaultQuery, - scopedVars: { - $interpolationVar: { - text: text, - value: text, - }, - }, - targets: [ - { - queryType: 'search', - refId: 'a', - service: '$interpolationVar', - operation: '$interpolationVar', - minDuration: '$interpolationVar', - maxDuration: '$interpolationVar', - }, - ], - }) - ); - expect(mock).toHaveBeenCalledWith({ - url: `${defaultSettings.url}/api/traces?service=interpolationText&operation=interpolationText&minDuration=interpolationText&maxDuration=interpolationText&${defaultSearchRangeParams}&lookback=custom`, - }); - }); - - describe('when jaegerBackendMigration feature toggle is enabled', () => { - let originalFeatureToggleValue: boolean | undefined; - - beforeEach(() => { - originalFeatureToggleValue = config.featureToggles.jaegerBackendMigration; - config.featureToggles.jaegerBackendMigration = true; - }); - - afterEach(() => { - config.featureToggles.jaegerBackendMigration = originalFeatureToggleValue; - }); - - it('should add node graph frames to response when nodeGraph is enabled and query is a trace ID query', async () => { - // Create a datasource with nodeGraph enabled - const settings = { - ...defaultSettings, - jsonData: { - ...defaultSettings.jsonData, - nodeGraph: { enabled: true }, - }, - }; - - const ds = new JaegerDatasource(settings); - - // Mock the super.query method to return our mock response - jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { - return of({ - data: [ - { - fields: testResponseDataFrameFields, - values: testResponseDataFrameFields.values, - }, - ], - }); - }); - - // Create a query without queryType (trace ID query) - const query = { ...defaultQuery, targets: [ { @@ -353,189 +129,31 @@ describe('JaegerDatasource', () => { refId: '1', }, ], - }; + }) + ); - // Execute the query - const response = await lastValueFrom(ds.query(query)); - // Verify that the response contains the original data plus node graph frames - expect(response.data.length).toBe(3); - }); - - it('should not add node graph frames when nodeGraph is disabled', async () => { - // Create a datasource with nodeGraph disabled - const settings = { - ...defaultSettings, - jsonData: { - ...defaultSettings.jsonData, - nodeGraph: { enabled: false }, - }, - }; - - const ds = new JaegerDatasource(settings); - - // Mock the super.query method to return our mock response - jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { - return of({ - data: [ - { - fields: testResponseDataFrameFields, - values: testResponseDataFrameFields.values, - }, - ], - }); - }); - - // Create a query without queryType (trace ID query) - const query = { - ...defaultQuery, - targets: [ - { - query: '12345', - refId: '1', - }, - ], - }; - - // Execute the query - const response = await lastValueFrom(ds.query(query)); - // Verify that the response contains only the original data - expect(response.data.length).toBe(1); - expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields); - }); + expect(response.data.length).toBe(1); }); }); -describe('when performing testDataSource', () => { - describe('and call succeeds', () => { - it('should return successfully', async () => { - setupFetchMock({ data: ['service1'] }); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('success'); - expect(response.message).toBe('Data source connected and services found.'); - }); - }); - - describe('and call succeeds, but returns no services', () => { - it('should display an error', async () => { - setupFetchMock(undefined); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('error'); - expect(response.message).toBe( - 'Data source connected, but no services received. Verify that Jaeger is configured properly.' - ); - }); - }); - - describe('and call returns error with message', () => { - it('should return the formatted error', async () => { - setupFetchMock( - undefined, - throwError({ - statusText: 'Not found', - status: 404, - data: { - message: '404 page not found', - }, - }) - ); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('error'); - expect(response.message).toBe('Jaeger: Not found. 404. 404 page not found'); - }); - }); - - describe('and call returns error without message', () => { - it('should return JSON error', async () => { - setupFetchMock( - undefined, - throwError({ - statusText: 'Bad gateway', - status: 502, - data: { - errors: ['Could not connect to Jaeger backend'], - }, - }) - ); - - const ds = new JaegerDatasource(defaultSettings); - const response = await ds.testDatasource(); - expect(response.status).toEqual('error'); - expect(response.message).toBe('Jaeger: Bad gateway. 502. {"errors":["Could not connect to Jaeger backend"]}'); - }); - }); -}); - -describe('Test behavior with unmocked time', () => { - // Tolerance for checking timestamps. - // Using a lower number seems to cause flaky tests. - const numDigits = -4; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('getTimeRange()', async () => { +describe('time range', () => { + it('should calculate correct time range', async () => { const ds = new JaegerDatasource(defaultSettings); const timeRange = ds.getTimeRange(); const now = Date.now(); - expect(timeRange.end).toBeCloseTo(now * 1000, numDigits); - expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, numDigits); - }); - - it("call for `query()` when `queryType === 'dependencyGraph'`", async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const now = Date.now(); - - ds.query({ ...defaultQuery, targets: [{ queryType: 'dependencyGraph', refId: '1' }] }); - - const url = mock.mock.calls[0][0].url; - const endTsMatch = url.match(/endTs=(\d+)/); - expect(endTsMatch).not.toBeNull(); - expect(parseInt(endTsMatch![1], 10)).toBeCloseTo(now, numDigits); - - const lookbackMatch = url.match(/lookback=(\d+)/); - expect(lookbackMatch).not.toBeNull(); - expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(3600000, -1); // due to rounding, the least significant digit is not reliable - }); - - it("call for `query()` when `queryType === 'dependencyGraph'`, using default range", async () => { - const mock = setupFetchMock({ data: [testResponse] }); - const ds = new JaegerDatasource(defaultSettings); - const now = Date.now(); - const query = JSON.parse(JSON.stringify(defaultQuery)); - // @ts-ignore - query.range = undefined; - - ds.query({ ...query, targets: [{ queryType: 'dependencyGraph', refId: '1' }] }); - - const url = mock.mock.calls[0][0].url; - const endTsMatch = url.match(/endTs=(\d+)/); - expect(endTsMatch).not.toBeNull(); - expect(parseInt(endTsMatch![1], 10)).toBeCloseTo(now, numDigits); - - const lookbackMatch = url.match(/lookback=(\d+)/); - expect(lookbackMatch).not.toBeNull(); - expect(parseInt(lookbackMatch![1], 10)).toBeCloseTo(21600000, -1); + expect(timeRange.end).toBeCloseTo(now * 1000, -4); + expect(timeRange.start).toBeCloseTo((now - 6 * 3600 * 1000) * 1000, -4); }); }); -function setupFetchMock(response: unknown, mock?: ReturnType<typeof backendSrv.fetch>) { - const defaultMock = () => mock ?? of(createFetchResponse(response)); - - const fetchMock = jest.spyOn(backendSrv, 'fetch'); - fetchMock.mockImplementation(defaultMock); - return fetchMock; +function setupQueryMock(type: 'trace' | 'search') { + return jest.spyOn(DataSourceWithBackend.prototype, 'query').mockImplementation(() => { + if (type === 'search') { + return of(mockSearchResponse); + } else { + return of(mockTraceResponse); + } + }); } const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = { @@ -560,3 +178,25 @@ const defaultSettings: DataSourceInstanceSettings<JaegerJsonData> = { }, readOnly: false, }; + +const defaultQuery: DataQueryRequest<JaegerQuery> = { + requestId: '1', + interval: '0', + intervalMs: 10, + panelId: 0, + scopedVars: {}, + range: { + from: dateTime().subtract(1, 'h'), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + timezone: 'browser', + app: 'explore', + startTime: 0, + targets: [ + { + query: '12345', + refId: '1', + }, + ], +}; diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index 6aac84c3ca0..11c6a8a3f3b 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -1,6 +1,5 @@ -import { identity, omit, pick, pickBy } from 'lodash'; -import { lastValueFrom, Observable, of } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; import { DataQueryRequest, @@ -14,25 +13,14 @@ import { MutableDataFrame, ScopedVars, toDataFrame, - urlUtil, } from '@grafana/data'; import { createNodeGraphFrames, NodeGraphOptions, SpanBarOptions } from '@grafana/o11y-ds-frontend'; -import { - BackendSrvRequest, - config, - DataSourceWithBackend, - getBackendSrv, - getTemplateSrv, - TemplateSrv, -} from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { ALL_OPERATIONS_KEY } from './components/SearchForm'; import { TraceIdTimeParamsOptions } from './configuration/TraceIdTimeParams'; -import { mapJaegerDependenciesResponse } from './dependencyGraphTransform'; import { createGraphFrames } from './graphTransform'; -import { createTableFrame, createTraceFrame } from './responseTransform'; +import { createTraceFrame } from './responseTransform'; import { JaegerQuery } from './types'; -import { convertTagsLogfmt } from './util'; export interface JaegerJsonData extends DataSourceJsonData { nodeGraph?: NodeGraphOptions; @@ -45,7 +33,7 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ traceIdTimeParams?: TraceIdTimeParamsOptions; spanBar?: SpanBarOptions; constructor( - private instanceSettings: DataSourceInstanceSettings<JaegerJsonData>, + instanceSettings: DataSourceInstanceSettings<JaegerJsonData>, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); @@ -53,25 +41,14 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ this.traceIdTimeParams = instanceSettings.jsonData.traceIdTimeParams; } - /** - * Migrated to backend with feature toggle `jaegerBackendMigration` - */ async metadataRequest(url: string, params?: Record<string, unknown>) { - if (config.featureToggles.jaegerBackendMigration) { - return await this.getResource(url, params); - } - - const res = await lastValueFrom(this._request('/api/' + url, params, { hideFromInspector: true })); - return res.data.data; + return await this.getResource(url, params); } isSearchFormValid(query: JaegerQuery): boolean { return !!query.service; } - /** - * Migrated to backend with feature toggle `jaegerBackendMigration` - */ query(options: DataQueryRequest<JaegerQuery>): Observable<DataQueryResponse> { // At this moment we expect only one target. In case we somehow change the UI to be able to show multiple // traces at one we need to change this. @@ -80,55 +57,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ return of({ data: [emptyTraceDataFrame] }); } - if (config.featureToggles.jaegerBackendMigration && target.queryType !== 'upload') { - return super.query({ ...options, targets: [target] }).pipe( - map((response) => { - // If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response - if (this.nodeGraph?.enabled && !target.queryType) { - return addNodeGraphFramesToResponse(response); - } - return response; - }) - ); - } - - // Use the internal Jaeger /dependencies API for rendering the dependency graph. - if (target.queryType === 'dependencyGraph') { - const timeRange = options.range ?? getDefaultTimeRange(); - const endTs = getTime(timeRange.to, true) / 1000; - const lookback = endTs - getTime(timeRange.from, false) / 1000; - return this._request('/api/dependencies', { endTs, lookback }).pipe(map(mapJaegerDependenciesResponse)); - } - - if (target.queryType === 'search' && !this.isSearchFormValid(target)) { - return of({ error: { message: 'You must select a service.' }, data: [] }); - } - - let { start, end } = this.getTimeRange(options.range); - - if (target.queryType !== 'search' && target.query) { - let url = `/api/traces/${encodeURIComponent(this.templateSrv.replace(target.query.trim(), options.scopedVars))}`; - if (this.traceIdTimeParams) { - url += `?start=${start}&end=${end}`; - } - - return this._request(url).pipe( - map((response) => { - const traceData = response?.data?.data?.[0]; - if (!traceData) { - return { data: [emptyTraceDataFrame] }; - } - let data = [createTraceFrame(traceData)]; - if (this.nodeGraph?.enabled) { - data.push(...createGraphFrames(traceData)); - } - return { - data, - }; - }) - ); - } - if (target.queryType === 'upload') { if (!this.uploadedJson) { return of({ data: [] }); @@ -146,38 +74,13 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ } } - let jaegerInterpolated = pick(this.applyTemplateVariables(target, options.scopedVars), [ - 'service', - 'operation', - 'tags', - 'minDuration', - 'maxDuration', - 'limit', - ]); - // remove empty properties - let jaegerQuery = pickBy(jaegerInterpolated, identity); - - if (jaegerQuery.operation === ALL_OPERATIONS_KEY) { - jaegerQuery = omit(jaegerQuery, 'operation'); - } - - if (jaegerQuery.tags) { - jaegerQuery = { - ...jaegerQuery, - tags: convertTagsLogfmt(jaegerQuery.tags.toString()), - }; - } - - // TODO: this api is internal, used in jaeger ui. Officially they have gRPC api that should be used. - return this._request(`/api/traces`, { - ...jaegerQuery, - ...this.getTimeRange(options.range), - lookback: 'custom', - }).pipe( + return super.query({ ...options, targets: [target] }).pipe( map((response) => { - return { - data: [createTableFrame(response.data.data, this.instanceSettings)], - }; + // If the node graph is enabled and the query is a trace ID query, add the node graph frames to the response + if (this.nodeGraph?.enabled && !target.queryType) { + return addNodeGraphFramesToResponse(response); + } + return response; }) ); } @@ -215,49 +118,8 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ }; } - /** - * Migrated to backend with feature toggle `jaegerBackendMigration` - */ async testDatasource() { - if (config.featureToggles.jaegerBackendMigration) { - return await super.testDatasource(); - } - - return lastValueFrom( - this._request('/api/services').pipe( - map((res) => { - const values = res?.data?.data || []; - const testResult = - values.length > 0 - ? { status: 'success', message: 'Data source connected and services found.' } - : { - status: 'error', - message: - 'Data source connected, but no services received. Verify that Jaeger is configured properly.', - }; - return testResult; - }), - catchError((err) => { - let message = 'Jaeger: '; - if (err.statusText) { - message += err.statusText; - } else { - message += 'Cannot connect to Jaeger'; - } - - if (err.status) { - message += `. ${err.status}`; - } - - if (err.data && err.data.message) { - message += `. ${err.data.message}`; - } else if (err.data) { - message += `. ${JSON.stringify(err.data)}`; - } - return of({ status: 'error', message: message }); - }) - ) - ); + return await super.testDatasource(); } getTimeRange(range = getDefaultTimeRange()): { start: number; end: number } { @@ -270,21 +132,6 @@ export class JaegerDatasource extends DataSourceWithBackend<JaegerQuery, JaegerJ getQueryDisplayText(query: JaegerQuery) { return query.query || ''; } - - private _request( - apiUrl: string, - data?: Record<string, unknown>, - options?: Partial<BackendSrvRequest> - ): Observable<Record<string, any>> { - const params = data ? urlUtil.serializeParams(data) : ''; - const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; - const req = { - ...options, - url, - }; - - return getBackendSrv().fetch(req); - } } function getTime(date: string | DateTime, roundUp: boolean) { diff --git a/public/app/plugins/datasource/jaeger/mockSearchResponse.json b/public/app/plugins/datasource/jaeger/mockSearchResponse.json new file mode 100644 index 00000000000..7ce11be4aea --- /dev/null +++ b/public/app/plugins/datasource/jaeger/mockSearchResponse.json @@ -0,0 +1,52 @@ +{ + "data": [ + { + "fields": [ + { + "name": "traceID", + "values": ["test-trace-id"], + "config": { + "displayName": "Trace ID", + "links": [ + { + "title": "Trace: ${__value.raw}", + "internal": { + "query": { + "query": "${__value.raw}" + }, + "datasourceUid": "test-uid", + "datasourceName": "test-name" + } + } + ] + } + }, + { + "name": "traceName", + "values": ["test-service: test-operation"], + "config": { + "displayName": "Trace name" + } + }, + { + "name": "startTime", + "values": [1605873894680], + "config": { + "displayName": "Start time" + } + }, + { + "name": "duration", + "values": [1000], + "config": { + "displayName": "Duration", + "unit": "µs" + } + } + ], + "meta": { + "preferredVisualisationType": "table" + } + } + ] +} diff --git a/public/app/plugins/datasource/jaeger/mockTraceResponse.json b/public/app/plugins/datasource/jaeger/mockTraceResponse.json new file mode 100644 index 00000000000..08867e2197f --- /dev/null +++ b/public/app/plugins/datasource/jaeger/mockTraceResponse.json @@ -0,0 +1,21 @@ +{ + "data": [ + { + "fields": [ + { "name": "traceID", "values": ["3fa414edcef6ad90", "3fa414edcef6ad90"] }, + { "name": "spanID", "values": ["3fa414edcef6ad90", "0f5c1808567e4403"] }, + { "name": "parentSpanID", "values": [null, "3fa414edcef6ad90"] }, + { "name": "operationName", "values": ["HTTP GET - api_traces_traceid", "/tempopb.Querier/FindTraceByID"] }, + { "name": "serviceName", "values": ["tempo-querier", "tempo-querier"] }, + { "name": "startTime", "values": [1605873894680.409, 1605873894680.587] }, + { "name": "duration", "values": [1049.141, 1.847] } + ], + "meta": { + "preferredVisualisationType": "trace", + "custom": { + "traceFormat": "jaeger" + } + } + } + ] +} diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 1e505684375..160045a9948 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -15,7 +15,7 @@ "logfmt": "^1.3.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-window": "1.8.11", "rxjs": "7.8.2", "stream-browserify": "3.0.0", @@ -29,7 +29,7 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/logfmt": "^1.2.3", "@types/node": "22.15.0", "@types/react": "18.3.18", @@ -37,16 +37,16 @@ "@types/react-window": "1.8.8", "@types/uuid": "10.0.0", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/jaeger/webpack.config.ts b/public/app/plugins/datasource/jaeger/webpack.config.ts index 10ef5bd69a2..0346a64c5f2 100644 --- a/public/app/plugins/datasource/jaeger/webpack.config.ts +++ b/public/app/plugins/datasource/jaeger/webpack.config.ts @@ -1,6 +1,10 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import { createRequire } from 'node:module'; -const configWithFallback = async (env: Record<string, unknown>) => { +import config, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; + +const require = createRequire(import.meta.url); + +const configWithFallback = async (env: Env) => { const response = await config(env); if (response !== undefined && response.resolve !== undefined) { response.resolve.fallback = { diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index 49e58e8f233..5b1c9b8c9a6 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -26,6 +26,7 @@ const MAX_LABEL_COUNT = 1000; const MAX_VALUE_COUNT = 10000; const MAX_AUTO_SELECT = 4; const EMPTY_SELECTOR = '{}'; +const collator = new Intl.Collator('en', { sensitivity: 'accent' }); export interface BrowserProps { languageProvider: LokiLanguageProvider; @@ -68,7 +69,10 @@ export function buildSelector(labels: SelectableLabel[]): string { const selectedLabels = []; for (const label of labels) { if (label.selected && label.values && label.values.length > 0) { - const selectedValues = label.values.filter((value) => value.selected).map((value) => value.name); + const selectedValues = label.values + .filter((value) => value.selected) + .map((value) => value.name) + .sort(collator.compare); // sort selected values alphabetically if (selectedValues.length > 1) { selectedLabels.push(`${label.name}=~"${selectedValues.map(escapeLabelValueInRegexSelector).join('|')}"`); } else if (selectedValues.length === 1) { @@ -97,7 +101,13 @@ export function facetLabels( label.values?.filter((value) => value.selected).map((value) => value.name) || [] ); // Values for this label have not been requested yet, let's use the facetted ones as the initial values - existingValues = possibleValues.map((value) => ({ name: value, selected: selectedValues.has(value) })); + existingValues = possibleValues + .slice() + .sort(collator.compare) // sort raw label values alphabetically + .map((value) => ({ + name: value, + selected: selectedValues.has(value), + })); } return { ...label, loading: false, values: existingValues, facets: existingValues.length }; } diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index c63332522cd..8d49fe6f27b 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1809,6 +1809,162 @@ describe('LokiDatasource', () => { }); }); + describe('scopes application', () => { + let ds: LokiDatasource; + let origBackendSrv: BackendSrv; + + beforeEach(() => { + origBackendSrv = getBackendSrv(); + ds = createLokiDatasource(templateSrvStub); + // Enable the required feature toggles + config.featureToggles.scopeFilters = true; + config.featureToggles.logQLScope = true; + }); + + afterEach(() => { + setBackendSrv(origBackendSrv); + // Reset feature toggles to false + config.featureToggles.scopeFilters = false; + config.featureToggles.logQLScope = false; + }); + + it('should apply scopes to queries when feature toggles are enabled', async () => { + const mockScopes = [ + { + metadata: { name: 'test-scope' }, + spec: { + title: 'Test Scope', + type: 'test', + description: 'Test scope description', + category: 'test-category', + filters: [ + { key: 'environment', value: 'production', operator: 'equals' as const }, + { key: 'service', value: 'api', operator: 'equals' as const }, + ], + }, + }, + ]; + + const query: DataQueryRequest<LokiQuery> = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: mockScopes, + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: [ + { key: 'environment', value: 'production', operator: 'equals' }, + { key: 'service', value: 'api', operator: 'equals' }, + ], + }), + ]), + }), + }) + ); + }); + + it('should not apply scopes when feature toggles are disabled', async () => { + // Disable the required feature toggles + config.featureToggles.scopeFilters = false; + config.featureToggles.logQLScope = false; + + const mockScopes = [ + { + metadata: { name: 'test-scope' }, + spec: { + title: 'Test Scope', + type: 'test', + description: 'Test scope description', + category: 'test-category', + filters: [{ key: 'environment', value: 'production', operator: 'equals' as const }], + }, + }, + ]; + + const query: DataQueryRequest<LokiQuery> = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: mockScopes, + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: undefined, + }), + ]), + }), + }) + ); + }); + + it('should handle empty scopes array', async () => { + const query: DataQueryRequest<LokiQuery> = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: [], + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: [], + }), + ]), + }), + }) + ); + }); + + it('should handle undefined scopes', async () => { + const query: DataQueryRequest<LokiQuery> = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: undefined, + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: undefined, + }), + ]), + }), + }) + ); + }); + }); + describe('getQueryStats', () => { let ds: LokiDatasource; let query: LokiQuery; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 4ded88df124..468b52b005e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -299,7 +299,14 @@ export class LokiDatasource query(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> { const queries = request.targets .map(getNormalizedLokiQuery) // used to "fix" the deprecated `.queryType` prop - .map((q) => ({ ...q, maxLines: q.maxLines ?? this.maxLines })); + .map((q) => ({ + ...q, + maxLines: q.maxLines ?? this.maxLines, + scopes: + config.featureToggles.scopeFilters && config.featureToggles.logQLScope + ? request.scopes?.flatMap((scope) => scope.spec.filters) + : undefined, + })); const fixedRequest: DataQueryRequest<LokiQuery> = { ...request, diff --git a/public/app/plugins/datasource/loki/package.json b/public/app/plugins/datasource/loki/package.json index a321554f011..6ccdca31bb9 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -6,7 +6,7 @@ "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "12.1.0-pre", - "@grafana/lezer-logql": "0.2.7", + "@grafana/lezer-logql": "0.2.8", "@grafana/llm": "0.22.1", "@grafana/monaco-logql": "^0.0.8", "@grafana/runtime": "12.1.0-pre", @@ -17,7 +17,7 @@ "micro-memoize": "^4.1.2", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1", @@ -31,7 +31,7 @@ "@testing-library/user-event": "14.6.1", "@types/d3-random": "^3.0.2", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", @@ -44,9 +44,9 @@ "@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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index e57c3053c32..82a585739aa 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -72,6 +72,21 @@ describe('runSplitQuery()', () => { }); }); + test('Interpolates queries before execution', async () => { + const request = createRequest([{ expr: 'count_over_time({a="b"}[$__auto])', refId: 'A', step: '$step' }]); + datasource = createLokiDatasource({ + replace: (input = '') => { + return input.replace('$__auto', '5m').replace('$step', '5m'); + }, + getVariables: () => [], + }); + jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [] })); + await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { + expect(jest.mocked(datasource.runQuery).mock.calls[0][0].targets[0].expr).toBe('count_over_time({a="b"}[5m])'); + expect(jest.mocked(datasource.runQuery).mock.calls[0][0].targets[0].step).toBe('5m'); + }); + }); + test('Skips partial updates as an option', async () => { await expect(runSplitQuery(datasource, request, { skipPartialUpdates: true })).toEmitValuesWith((emitted) => { // 3 days, 3 chunks, 3 requests. diff --git a/public/app/plugins/datasource/loki/querySplitting.ts b/public/app/plugins/datasource/loki/querySplitting.ts index 069ed4e7b90..d0fd1678455 100644 --- a/public/app/plugins/datasource/loki/querySplitting.ts +++ b/public/app/plugins/datasource/loki/querySplitting.ts @@ -293,7 +293,10 @@ export function runSplitQuery( request: DataQueryRequest<LokiQuery>, options: QuerySplittingOptions = {} ) { - const queries = request.targets.filter((query) => !query.hide).filter((query) => query.expr); + const queries = request.targets + .filter((query) => !query.hide) + .filter((query) => query.expr) + .map((query) => datasource.applyTemplateVariables(query, request.scopedVars, request.filters)); const [nonSplittingQueries, normalQueries] = partition(queries, (query) => !querySupportsSplitting(query)); const [logQueries, metricQueries] = partition(normalQueries, (query) => isLogsQuery(query.expr)); diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts index 566eddeae81..04cb8c15055 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts @@ -53,11 +53,9 @@ describe('runShardSplitQuery()', () => { request = createRequest([{ expr: '$SELECTOR', refId: 'A', direction: LokiQueryDirection.Scan }]); datasource = createLokiDatasource(); datasource.languageProvider.fetchLabelValues = jest.fn(); - datasource.interpolateVariablesInQueries = jest.fn().mockImplementation((queries: LokiQuery[]) => { - return queries.map((query) => { - query.expr = query.expr.replace('$SELECTOR', '{a="b"}'); - return query; - }); + datasource.applyTemplateVariables = jest.fn().mockImplementation((query: LokiQuery) => { + query.expr = query.expr.replace('$SELECTOR', '{a="b"}'); + return query; }); jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1', '10', '2', '20', '3']); const { metricFrameA } = getMockFrames(); @@ -83,6 +81,25 @@ describe('runShardSplitQuery()', () => { }); }); + test('Interpolates queries before execution', async () => { + const request = createRequest([{ expr: 'count_over_time({a="b"}[$__auto])', refId: 'A', step: '$step' }]); + datasource = createLokiDatasource({ + replace: (input = '') => { + return input.replace('$__auto', '5m').replace('$step', '5m'); + }, + getVariables: () => [], + }); + jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [] })); + datasource.languageProvider.fetchLabelValues = jest.fn(); + jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1', '10', '2', '20', '3']); + await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => { + expect(jest.mocked(datasource.runQuery).mock.calls[0][0].targets[0].expr).toBe( + 'count_over_time({a="b", __stream_shard__=~"20|10"} | drop __stream_shard__[5m])' + ); + expect(jest.mocked(datasource.runQuery).mock.calls[0][0].targets[0].step).toBe('5m'); + }); + }); + test('Users query splitting for querying over a day', async () => { await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => { // 5 shards, 3 groups + empty shard group, 4 requests @@ -92,7 +109,7 @@ describe('runShardSplitQuery()', () => { test('Interpolates queries before running', async () => { await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => { - expect(datasource.interpolateVariablesInQueries).toHaveBeenCalledTimes(1); + expect(datasource.applyTemplateVariables).toHaveBeenCalledTimes(5); expect(datasource.runQuery).toHaveBeenCalledWith({ intervalMs: expect.any(Number), @@ -153,11 +170,9 @@ describe('runShardSplitQuery()', () => { }); test('Sends the whole stream selector to fetch values', async () => { - datasource.interpolateVariablesInQueries = jest.fn().mockImplementation((queries: LokiQuery[]) => { - return queries.map((query) => { - query.expr = query.expr.replace('$SELECTOR', '{service_name="test", filter="true"}'); - return query; - }); + datasource.applyTemplateVariables = jest.fn().mockImplementation((query: LokiQuery) => { + query.expr = query.expr.replace('$SELECTOR', '{service_name="test", filter="true"}'); + return query; }); await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => { diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.ts index 9f8dc397b8d..6a949ece553 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.ts @@ -46,10 +46,10 @@ import { LokiQuery } from './types'; */ export function runShardSplitQuery(datasource: LokiDatasource, request: DataQueryRequest<LokiQuery>) { - const queries = datasource - .interpolateVariablesInQueries(request.targets, request.scopedVars) + const queries = request.targets .filter((query) => query.expr) - .filter((query) => !query.hide); + .filter((query) => !query.hide) + .map((query) => datasource.applyTemplateVariables(query, request.scopedVars, request.filters)); return splitQueriesByStreamShard(datasource, request, queries); } diff --git a/public/app/plugins/datasource/loki/streaming.test.ts b/public/app/plugins/datasource/loki/streaming.test.ts index 04a87d20856..3b0d172106f 100644 --- a/public/app/plugins/datasource/loki/streaming.test.ts +++ b/public/app/plugins/datasource/loki/streaming.test.ts @@ -8,20 +8,21 @@ jest.mock('@grafana/runtime', () => ({ })); describe('convertToWebSocketUrl', () => { - const { location } = window; + const win: typeof globalThis = window; + const { location } = win; beforeEach(() => { // @ts-ignore - delete window.location; - window.location = {} as Location; + delete win.location; + win.location = {} as Location; }); afterEach(() => { - window.location = location; + win.location = location; }); it('should convert HTTP URL to WebSocket URL', () => { - window.location.protocol = 'http:'; - window.location.host = 'example.com'; + win.location.protocol = 'http:'; + win.location.host = 'example.com'; const httpUrl = '/api/ds/proxy/1/api/v1/tail/loki?query=a'; const expectedWebSocketUrl = 'ws://example.com/grafana/api/ds/proxy/1/api/v1/tail/loki?query=a'; @@ -32,8 +33,8 @@ describe('convertToWebSocketUrl', () => { }); it('should convert HTTPS URL to WebSocket URL', () => { - window.location.protocol = 'https:'; - window.location.host = 'example.com'; + win.location.protocol = 'https:'; + win.location.host = 'example.com'; const httpsUrl = '/api/ds/proxy/1/api/v1/tail/loki?query=a'; const expectedWebSocketUrl = 'wss://example.com/grafana/api/ds/proxy/1/api/v1/tail/loki?query=a'; diff --git a/public/app/plugins/datasource/loki/webpack.config.ts b/public/app/plugins/datasource/loki/webpack.config.ts index 55a880fef5c..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/loki/webpack.config.ts +++ b/public/app/plugins/datasource/loki/webpack.config.ts @@ -1,4 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; // eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/mssql/module.ts b/public/app/plugins/datasource/mssql/module.ts index 7cf4eeaafe1..7ba2f9c512f 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -8,7 +8,11 @@ import { MssqlDatasource } from './datasource'; import pluginJson from './plugin.json'; import { MssqlOptions } from './types'; -initPluginTranslations(pluginJson.id, [loadSQLResources]); +// don't load plugin translations in test environments +// we don't use them anyway, and top-level await won't work currently in jest +if (process.env.NODE_ENV !== 'test') { + await initPluginTranslations(pluginJson.id, [loadSQLResources]); +} export const plugin = new DataSourcePlugin<MssqlDatasource, SQLQuery, MssqlOptions>(MssqlDatasource) .setQueryEditor(SqlQueryEditorLazy) diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index d0bb696d900..7b8f678c101 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -23,21 +23,21 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "i18next-parser": "9.3.0", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development", "i18n-extract": "i18next --config locales/i18next-parser.config.cjs" }, "packageManager": "yarn@4.9.2" diff --git a/public/app/plugins/datasource/mssql/webpack.config.ts b/public/app/plugins/datasource/mssql/webpack.config.ts index 2e981f9ea7c..8a119f88315 100644 --- a/public/app/plugins/datasource/mssql/webpack.config.ts +++ b/public/app/plugins/datasource/mssql/webpack.config.ts @@ -1,9 +1,9 @@ import type { Configuration } from 'webpack'; import { merge } from 'webpack-merge'; -import grafanaConfig from '@grafana/plugin-configs/webpack.config'; +import grafanaConfig, { type Env } from '@grafana/plugin-configs/webpack.config.ts'; -const config = async (env: Record<string, unknown>): Promise<Configuration> => { +const config = async (env: Env): Promise<Configuration> => { const baseConfig = await grafanaConfig(env); return merge(baseConfig, { diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index 8ee9163a902..71f24047aa4 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -22,20 +22,20 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/mysql/webpack.config.ts b/public/app/plugins/datasource/mysql/webpack.config.ts index 55a880fef5c..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/mysql/webpack.config.ts +++ b/public/app/plugins/datasource/mysql/webpack.config.ts @@ -1,4 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; // eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/parca/lang/lang.ts b/public/app/plugins/datasource/parca/lang/lang.ts index b5fddd473d1..81522d0be83 100644 --- a/public/app/plugins/datasource/parca/lang/lang.ts +++ b/public/app/plugins/datasource/parca/lang/lang.ts @@ -31,7 +31,7 @@ export const language: languages.IMonarchLanguage = { digits: /\d+(_+\d+)*/, octaldigits: /[0-7]+(_+[0-7]+)*/, binarydigits: /[0-1]+(_+[0-1]+)*/, - hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/, + hexdigits: /[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/, integersuffix: /(ll|LL|u|U|l|L)?(ll|LL|u|U|l|L)?/, floatsuffix: /[fFlL]?/, diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index d3d998cfeab..5690290bb3a 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -22,21 +22,21 @@ "@testing-library/dom": "10.4.0", "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/parca/webpack.config.ts b/public/app/plugins/datasource/parca/webpack.config.ts index 4da5a990cfa..31247bb11af 100644 --- a/public/app/plugins/datasource/parca/webpack.config.ts +++ b/public/app/plugins/datasource/parca/webpack.config.ts @@ -1,3 +1,3 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; export default config; diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 09c9f8e8840..455e45f1b02 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -21,14 +21,14 @@ "@opentelemetry/semantic-conventions": "1.34.0", "buffer": "6.0.3", "events": "3.3.0", - "i18next": "^24.0.0", + "i18next": "^25.0.0", "lodash": "4.17.21", "lru-cache": "11.1.0", "monaco-editor": "0.34.1", "prismjs": "1.30.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "semver": "7.7.2", @@ -44,27 +44,27 @@ "@testing-library/react": "16.2.0", "@testing-library/user-event": "14.6.1", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/semver": "7.7.0", "@types/uuid": "10.0.0", - "glob": "11.0.1", + "glob": "11.0.3", "react-select-event": "5.5.1", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "peerDependencies": { "@grafana/runtime": "*" }, "scripts": { - "build": "webpack -c ./webpack.config.ts --env production", + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", "build:stats": "yarn build --env stats --profile --json=compilation-stats.json", - "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" + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/tempo/webpack.config.ts b/public/app/plugins/datasource/tempo/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/tempo/webpack.config.ts +++ b/public/app/plugins/datasource/tempo/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index c8ac7eebad0..21fcf1d7aae 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -14,7 +14,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-dom": "18.3.1", - "react-select": "5.10.1", + "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", "tslib": "2.8.1" @@ -25,21 +25,21 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.2.0", "@types/jest": "29.5.14", - "@types/lodash": "4.17.15", + "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "ts-node": "10.9.2", - "typescript": "5.7.3", + "typescript": "5.8.3", "webpack": "5.97.1" }, "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" + "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", + "build:commit": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production --env commit=$(git rev-parse --short HEAD)", + "dev": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -w -c ./webpack.config.ts --env development" }, "packageManager": "yarn@4.9.2" } diff --git a/public/app/plugins/datasource/zipkin/webpack.config.ts b/public/app/plugins/datasource/zipkin/webpack.config.ts index 4da5a990cfa..7931eb9cb5c 100644 --- a/public/app/plugins/datasource/zipkin/webpack.config.ts +++ b/public/app/plugins/datasource/zipkin/webpack.config.ts @@ -1,3 +1,4 @@ -import config from '@grafana/plugin-configs/webpack.config'; +import config from '@grafana/plugin-configs/webpack.config.ts'; +// eslint-disable-next-line no-barrel-files/no-barrel-files export default config; diff --git a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx index d864dfaba37..48bc6d561fe 100644 --- a/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx +++ b/public/app/plugins/panel/alertlist/GroupByWithLoading.tsx @@ -10,7 +10,7 @@ import { isAsyncRequestMapSlicePending, isAsyncRequestMapSliceSettled, } from 'app/features/alerting/unified/utils/redux'; -import { useDispatch } from 'app/types'; +import { useDispatch } from 'app/types/store'; import { AlertingRule } from 'app/types/unified-alerting'; import { PromRuleType } from 'app/types/unified-alerting-dto'; diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 6043d715801..913ddbfcda6 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -38,7 +38,7 @@ import { flattenCombinedRules, getFirstActiveAt } from 'app/features/alerting/un import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Matcher } from 'app/plugins/datasource/alertmanager/types'; -import { ThunkDispatch, useDispatch } from 'app/types'; +import { ThunkDispatch, useDispatch } from 'app/types/store'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { AlertingAction, useAlertingAbility } from '../../../features/alerting/unified/hooks/useAbilities'; diff --git a/public/app/plugins/panel/candlestick/fields.ts b/public/app/plugins/panel/candlestick/fields.ts index 29ce56ec079..42fb07732df 100644 --- a/public/app/plugins/panel/candlestick/fields.ts +++ b/public/app/plugins/panel/candlestick/fields.ts @@ -9,7 +9,7 @@ import { } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; -import { findField } from 'app/features/dimensions'; +import { findField } from 'app/features/dimensions/utils'; import { prepareGraphableFields } from '../timeseries/utils'; diff --git a/public/app/plugins/panel/canvas/components/SetBackground.tsx b/public/app/plugins/panel/canvas/components/SetBackground.tsx index 7d33b53df2a..4fea18ad6a5 100644 --- a/public/app/plugins/panel/canvas/components/SetBackground.tsx +++ b/public/app/plugins/panel/canvas/components/SetBackground.tsx @@ -5,8 +5,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { ResourceDimensionMode } from '@grafana/schema'; import { Portal, useTheme2 } from '@grafana/ui'; import { Scene } from 'app/features/canvas/runtime/scene'; -import { MediaType, ResourceFolderName } from 'app/features/dimensions'; import { ResourcePickerPopover } from 'app/features/dimensions/editors/ResourcePickerPopover'; +import { MediaType, ResourceFolderName } from 'app/features/dimensions/types'; import { AnchorPoint } from '../types'; diff --git a/public/app/plugins/panel/canvas/editor/options.ts b/public/app/plugins/panel/canvas/editor/options.ts index 79420b34187..dd107d010cb 100644 --- a/public/app/plugins/panel/canvas/editor/options.ts +++ b/public/app/plugins/panel/canvas/editor/options.ts @@ -3,8 +3,10 @@ import { PanelOptionsSupplier } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; import { ConnectionDirection } from 'app/features/canvas/element'; import { SVGElements } from 'app/features/canvas/runtime/element'; -import { ColorDimensionEditor, ResourceDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors'; import { BackgroundSizeEditor } from 'app/features/dimensions/editors/BackgroundSizeEditor'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { ResourceDimensionEditor } from 'app/features/dimensions/editors/ResourceDimensionEditor'; +import { ScaleDimensionEditor } from 'app/features/dimensions/editors/ScaleDimensionEditor'; import { CanvasConnection, CanvasElementOptions } from '../panelcfg.gen'; import { LineStyle } from '../types'; diff --git a/public/app/plugins/panel/dashlist/DashList.tsx b/public/app/plugins/panel/dashlist/DashList.tsx index e3044da47d2..d5c13d5627a 100644 --- a/public/app/plugins/panel/dashlist/DashList.tsx +++ b/public/app/plugins/panel/dashlist/DashList.tsx @@ -23,7 +23,7 @@ import impressionSrv from 'app/core/services/impression_srv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardSearchItem } from 'app/features/search/types'; import { VariablesChanged } from 'app/features/variables/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useDispatch, useSelector } from 'app/types/store'; import { Options } from './panelcfg.gen'; import { getStyles } from './styles'; diff --git a/public/app/plugins/panel/dashlist/migrations.ts b/public/app/plugins/panel/dashlist/migrations.ts index 25f420108d2..a48c8f4f736 100644 --- a/public/app/plugins/panel/dashlist/migrations.ts +++ b/public/app/plugins/panel/dashlist/migrations.ts @@ -1,6 +1,6 @@ import { PanelModel } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; -import { FolderDTO } from 'app/types'; +import { FolderDTO } from 'app/types/folders'; import { Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index 9c406873728..14399a2893d 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -1,6 +1,8 @@ import { css } from '@emotion/css'; import { Global } from '@emotion/react'; -import { Map as OpenLayersMap, MapBrowserEvent, View } from 'ol'; +import OpenLayersMap from 'ol/Map'; +import MapBrowserEvent from 'ol/MapBrowserEvent'; +import View from 'ol/View'; import Attribution from 'ol/control/Attribution'; import ScaleLine from 'ol/control/ScaleLine'; import Zoom from 'ol/control/Zoom'; @@ -273,11 +275,11 @@ export class GeomapPanel extends Component<Props, State> { this.setState({ ttipOpen: false, ttip: undefined }); }; - pointerClickListener = (evt: MapBrowserEvent<MouseEvent>) => { + pointerClickListener = (evt: MapBrowserEvent<PointerEvent>) => { pointerClickListener(evt, this); }; - pointerMoveListener = (evt: MapBrowserEvent<MouseEvent>) => { + pointerMoveListener = (evt: MapBrowserEvent<PointerEvent>) => { pointerMoveListener(evt, this); }; diff --git a/public/app/plugins/panel/geomap/components/DebugOverlay.tsx b/public/app/plugins/panel/geomap/components/DebugOverlay.tsx index ae838fef89b..de267de908f 100644 --- a/public/app/plugins/panel/geomap/components/DebugOverlay.tsx +++ b/public/app/plugins/panel/geomap/components/DebugOverlay.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { Map } from 'ol'; +import Map from 'ol/Map'; import { Coordinate } from 'ol/coordinate'; import { transform } from 'ol/proj'; import { PureComponent } from 'react'; diff --git a/public/app/plugins/panel/geomap/components/MarkersLegend.tsx b/public/app/plugins/panel/geomap/components/MarkersLegend.tsx index 1a06f88bdfb..cb897193034 100644 --- a/public/app/plugins/panel/geomap/components/MarkersLegend.tsx +++ b/public/app/plugins/panel/geomap/components/MarkersLegend.tsx @@ -17,7 +17,7 @@ import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; import { getThresholdItems } from 'app/core/components/TimelineChart/utils'; import { config } from 'app/core/config'; -import { DimensionSupplier } from 'app/features/dimensions'; +import { DimensionSupplier } from 'app/features/dimensions/types'; import { StyleConfigState } from '../style/types'; import { MapLayerState } from '../types'; diff --git a/public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts b/public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts index 0c6dc3ea15f..f1ff42c083d 100644 --- a/public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts +++ b/public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts @@ -200,24 +200,24 @@ export class MeasureVectorLayer extends VectorLayer<VectorSource> { } const segmentPoint = new Point(segment.getCoordinateAt(0.5)); this.segmentStyles[count].setGeometry(segmentPoint); - this.segmentStyles[count].getText().setText(label); + this.segmentStyles[count].getText()?.setText(label); styles.push(this.segmentStyles[count]); count++; }); } if (label!) { this.labelStyle.setGeometry(point!); - this.labelStyle.getText().setText(label); + this.labelStyle.getText()?.setText(label); styles.push(this.labelStyle); } if ( tip && type === 'Point' && geometry instanceof Point && - !this.modify.getOverlay().getSource().getFeatures().length + !this.modify.getOverlay().getSource()?.getFeatures().length ) { this.tipPoint = geometry; - this.tipStyle.getText().setText(tip); + this.tipStyle.getText()?.setText(tip); styles.push(this.tipStyle); } } diff --git a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx index fb36d762eb5..c7f07fb2dba 100644 --- a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx @@ -23,13 +23,11 @@ import { } from '@grafana/ui'; import { NumberValueEditor } from 'app/core/components/OptionsUI/number'; import { SliderValueEditor } from 'app/core/components/OptionsUI/slider'; -import { - ColorDimensionEditor, - ResourceDimensionEditor, - ScaleDimensionEditor, - ScalarDimensionEditor, - TextDimensionEditor, -} from 'app/features/dimensions/editors'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { ResourceDimensionEditor } from 'app/features/dimensions/editors/ResourceDimensionEditor'; +import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor'; +import { ScaleDimensionEditor } from 'app/features/dimensions/editors/ScaleDimensionEditor'; +import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; import { ResourceFolderName, defaultTextConfig, MediaType } from 'app/features/dimensions/types'; import { @@ -140,7 +138,12 @@ export const StyleEditor = (props: Props) => { settings: { resourceType: 'icon', folderName: ResourceFolderName.Marker, - placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label', + placeholderText: hasTextLabel + ? t('geomap.style-editor.placeholderText-select-symbol', 'Select a symbol') + : t( + 'geomap.style-editor.placeholderText-select-symbol-or-add-text', + 'Select a symbol or add a text label' + ), placeholderValue: defaultStyleConfig.symbol.fixed, showSourceRadio: false, maxFiles, @@ -230,7 +233,12 @@ export const StyleEditor = (props: Props) => { settings: { resourceType: MediaType.Icon, folderName: ResourceFolderName.Marker, - placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label', + placeholderText: hasTextLabel + ? t('geomap.style-editor.placeholderText-select-symbol', 'Select a symbol') + : t( + 'geomap.style-editor.placeholderText-select-symbol-or-add-text', + 'Select a symbol or add a text label' + ), placeholderValue: defaultStyleConfig.symbol.fixed, showSourceRadio: false, maxFiles, @@ -244,9 +252,15 @@ export const StyleEditor = (props: Props) => { value={value?.symbolAlign?.vertical ?? defaultStyleConfig.symbolAlign.vertical} onChange={onAlignVerticalChange} options={[ - { value: VerticalAlign.Top, label: capitalize(VerticalAlign.Top) }, - { value: VerticalAlign.Center, label: capitalize(VerticalAlign.Center) }, - { value: VerticalAlign.Bottom, label: capitalize(VerticalAlign.Bottom) }, + { value: VerticalAlign.Top, label: t('geomap.style-editor.vertical-align-options.label-top', 'Top') }, + { + value: VerticalAlign.Center, + label: t('geomap.style-editor.vertical-align-options.label-center', 'Center'), + }, + { + value: VerticalAlign.Bottom, + label: t('geomap.style-editor.vertical-align-options.label-bottom', 'Bottom'), + }, ]} /> </Field> @@ -255,9 +269,18 @@ export const StyleEditor = (props: Props) => { value={value?.symbolAlign?.horizontal ?? defaultStyleConfig.symbolAlign.horizontal} onChange={onAlignHorizontalChange} options={[ - { value: HorizontalAlign.Left, label: capitalize(HorizontalAlign.Left) }, - { value: HorizontalAlign.Center, label: capitalize(HorizontalAlign.Center) }, - { value: HorizontalAlign.Right, label: capitalize(HorizontalAlign.Right) }, + { + value: HorizontalAlign.Left, + label: t('geomap.style-editor.horizontal-align-options.label-left', 'Left'), + }, + { + value: HorizontalAlign.Center, + label: t('geomap.style-editor.horizontal-align-options.label-center', 'Center'), + }, + { + value: HorizontalAlign.Right, + label: t('geomap.style-editor.horizontal-align-options.label-right', 'Right'), + }, ]} /> </Field> diff --git a/public/app/plugins/panel/geomap/editor/layerEditor.tsx b/public/app/plugins/panel/geomap/editor/layerEditor.tsx index 865b9f94654..4f70a82e587 100644 --- a/public/app/plugins/panel/geomap/editor/layerEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/layerEditor.tsx @@ -2,6 +2,7 @@ import { get as lodashGet, isEqual } from 'lodash'; import { FrameGeometrySourceMode, getFrameMatchers, MapLayerOptions } from '@grafana/data'; import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; import { addLocationFields } from 'app/features/geo/editor/locationEditor'; @@ -69,7 +70,7 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map builder.addSelect({ path: 'type', - name: 'Layer type', // required, but hide space + name: t('geomap.layer-editor.name-layer-type', 'Layer type'), // required, but hide space settings: { options: layerTypes.options, }, @@ -80,7 +81,7 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map builder.addCustomEditor({ id: 'filterData', path: 'filterData', - name: 'Data', + name: t('geomap.layer-editor.name-data', 'Data'), editor: FrameSelectionEditor, defaultValue: undefined, }); @@ -108,11 +109,11 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map if (handler.registerOptionsUI) { handler.registerOptionsUI(builder, context); } - if (!isEqual(opts.category, ['Base layer'])) { + if (!isEqual(opts.category, [t('geomap.layer-editor.category-base-layer', 'Base layer')])) { if (!layer.hideOpacity) { builder.addSliderInput({ path: 'opacity', - name: 'Opacity', + name: t('geomap.layer-editor.name-opacity', 'Opacity'), defaultValue: 1, settings: { min: 0, @@ -123,8 +124,8 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map } builder.addBooleanSwitch({ path: 'layer-tooltip', - name: 'Display tooltip', - description: 'Show the tooltip for layer', + name: t('geomap.layer-editor.name-display-tooltip', 'Display tooltip'), + description: t('geomap.layer-editor.description-display-tooltip', 'Show the tooltip for layer'), defaultValue: true, }); } diff --git a/public/app/plugins/panel/geomap/layers/basemaps/carto.ts b/public/app/plugins/panel/geomap/layers/basemaps/carto.ts index 45f65b2b267..92f3c54502f 100644 --- a/public/app/plugins/panel/geomap/layers/basemaps/carto.ts +++ b/public/app/plugins/panel/geomap/layers/basemaps/carto.ts @@ -1,4 +1,4 @@ -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import TileLayer from 'ol/layer/Tile'; import XYZ from 'ol/source/XYZ'; @@ -33,7 +33,12 @@ export const carto: MapLayerRegistryItem<CartoConfig> = { * Function that configures transformation and returns a transformer * @param options */ - create: async (map: Map, options: MapLayerOptions<CartoConfig>, eventBus: EventBus, theme: GrafanaTheme2) => ({ + create: async ( + map: OpenLayersMap, + options: MapLayerOptions<CartoConfig>, + eventBus: EventBus, + theme: GrafanaTheme2 + ) => ({ init: () => { const cfg = { ...defaultCartoConfig, ...options.config }; let style: string | undefined = cfg.theme; diff --git a/public/app/plugins/panel/geomap/layers/basemaps/esri.ts b/public/app/plugins/panel/geomap/layers/basemaps/esri.ts index 87c3bb96170..b19e0e0679d 100644 --- a/public/app/plugins/panel/geomap/layers/basemaps/esri.ts +++ b/public/app/plugins/panel/geomap/layers/basemaps/esri.ts @@ -1,4 +1,4 @@ -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import { MapLayerRegistryItem, MapLayerOptions, GrafanaTheme2, RegistryItem, Registry, EventBus } from '@grafana/data'; @@ -60,7 +60,12 @@ export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = { description: 'Add layer from an ESRI ArcGIS MapServer', isBaseMap: true, - create: async (map: Map, options: MapLayerOptions<ESRIXYZConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async ( + map: OpenLayersMap, + options: MapLayerOptions<ESRIXYZConfig>, + eventBus: EventBus, + theme: GrafanaTheme2 + ) => { const cfg = { ...options.config }; const svc = publicServiceRegistry.getIfExists(cfg.server ?? DEFAULT_SERVICE)!; if (svc.id !== CUSTOM_SERVICE) { diff --git a/public/app/plugins/panel/geomap/layers/basemaps/generic.ts b/public/app/plugins/panel/geomap/layers/basemaps/generic.ts index 1642465584c..2ed2a022037 100644 --- a/public/app/plugins/panel/geomap/layers/basemaps/generic.ts +++ b/public/app/plugins/panel/geomap/layers/basemaps/generic.ts @@ -1,4 +1,4 @@ -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import TileLayer from 'ol/layer/Tile'; import XYZ from 'ol/source/XYZ'; @@ -23,7 +23,12 @@ export const xyzTiles: MapLayerRegistryItem<XYZConfig> = { description: 'Add map from a generic tile layer', isBaseMap: true, - create: async (map: Map, options: MapLayerOptions<XYZConfig>, eventBus: EventBus, theme: GrafanaTheme2) => ({ + create: async ( + map: OpenLayersMap, + options: MapLayerOptions<XYZConfig>, + eventBus: EventBus, + theme: GrafanaTheme2 + ) => ({ init: () => { const cfg = { ...options.config }; if (!cfg.url) { diff --git a/public/app/plugins/panel/geomap/layers/basemaps/osm.ts b/public/app/plugins/panel/geomap/layers/basemaps/osm.ts index b826b79a0c4..c644702b1ca 100644 --- a/public/app/plugins/panel/geomap/layers/basemaps/osm.ts +++ b/public/app/plugins/panel/geomap/layers/basemaps/osm.ts @@ -1,4 +1,4 @@ -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import TileLayer from 'ol/layer/Tile'; import OSM from 'ol/source/OSM'; @@ -14,7 +14,7 @@ export const standard: MapLayerRegistryItem = { * Function that configures transformation and returns a transformer * @param options */ - create: async (map: Map, options: MapLayerOptions, eventBus: EventBus) => ({ + create: async (map: OpenLayersMap, options: MapLayerOptions, eventBus: EventBus) => ({ init: () => { return new TileLayer({ source: new OSM(), diff --git a/public/app/plugins/panel/geomap/layers/data/dayNightLayer.tsx b/public/app/plugins/panel/geomap/layers/data/dayNightLayer.tsx index 346c922900c..1908e147ca1 100644 --- a/public/app/plugins/panel/geomap/layers/data/dayNightLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/dayNightLayer.tsx @@ -1,5 +1,5 @@ import Feature from 'ol/Feature'; -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import Point from 'ol/geom/Point'; import { Group as LayerGroup } from 'ol/layer'; import VectorImage from 'ol/layer/VectorImage'; @@ -60,7 +60,7 @@ export const dayNightLayer: MapLayerRegistryItem<DayNightConfig> = { * @param options * @param theme */ - create: async (map: Map, options: MapLayerOptions<DayNightConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<DayNightConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { // Assert default values const config = { ...defaultConfig, diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts b/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts index 19ee2480669..b09aa0a2576 100644 --- a/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts +++ b/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts @@ -1,5 +1,5 @@ import { FeatureLike } from 'ol/Feature'; -import OlMap from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import { unByKey } from 'ol/Observable'; import GeoJSON from 'ol/format/GeoJSON'; import VectorImage from 'ol/layer/VectorImage'; @@ -17,7 +17,7 @@ import { EventBus, } from '@grafana/data'; import { ComparisonOperation } from '@grafana/schema'; -import { findField } from 'app/features/dimensions'; +import { findField } from 'app/features/dimensions/utils'; import { StyleEditor } from '../../editor/StyleEditor'; import { polyStyle } from '../../style/markers'; @@ -78,7 +78,7 @@ export const dynamicGeoJSONLayer: MapLayerRegistryItem<DynamicGeoJSONMapperConfi * @param options * @param theme */ - create: async (map: OlMap, options: MapLayerOptions<DynamicGeoJSONMapperConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<DynamicGeoJSONMapperConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { const config = { ...defaultOptions, ...options.config }; const source = new VectorSource({ diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts b/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts index 03ff6603504..b5d0a6da133 100644 --- a/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts +++ b/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts @@ -1,5 +1,5 @@ import { FeatureLike } from 'ol/Feature'; -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import { unByKey } from 'ol/Observable'; import GeoJSON from 'ol/format/GeoJSON'; import VectorImage from 'ol/layer/VectorImage'; @@ -73,7 +73,7 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = { * Function that configures transformation and returns a transformer * @param options */ - create: async (map: Map, options: MapLayerOptions<GeoJSONMapperConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<GeoJSONMapperConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { const config = { ...defaultOptions, ...options.config }; // Interpolate variables in the URL diff --git a/public/app/plugins/panel/geomap/layers/data/heatMap.tsx b/public/app/plugins/panel/geomap/layers/data/heatMap.tsx index 9ac42690507..9e1af30413d 100644 --- a/public/app/plugins/panel/geomap/layers/data/heatMap.tsx +++ b/public/app/plugins/panel/geomap/layers/data/heatMap.tsx @@ -1,4 +1,4 @@ -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import { Point } from 'ol/geom'; import * as layer from 'ol/layer'; @@ -12,8 +12,8 @@ import { PanelData, } from '@grafana/data'; import { ScaleDimensionConfig } from '@grafana/schema'; -import { getScaledDimension } from 'app/features/dimensions'; -import { ScaleDimensionEditor } from 'app/features/dimensions/editors'; +import { ScaleDimensionEditor } from 'app/features/dimensions/editors/ScaleDimensionEditor'; +import { getScaledDimension } from 'app/features/dimensions/scale'; import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource'; import { getLocationMatchers } from 'app/features/geo/utils/location'; @@ -46,9 +46,11 @@ export const heatmapLayer: MapLayerRegistryItem<HeatmapConfig> = { /** * Function that configures transformation and returns a transformer + * @param map * @param options + * @param theme */ - create: async (map: Map, options: MapLayerOptions<HeatmapConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<HeatmapConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { const config = { ...defaultOptions, ...options.config }; const location = await getLocationMatchers(options.location); diff --git a/public/app/plugins/panel/geomap/layers/data/lastPointTracker.ts b/public/app/plugins/panel/geomap/layers/data/lastPointTracker.ts index 300324d5539..42436808daf 100644 --- a/public/app/plugins/panel/geomap/layers/data/lastPointTracker.ts +++ b/public/app/plugins/panel/geomap/layers/data/lastPointTracker.ts @@ -1,5 +1,5 @@ import Feature from 'ol/Feature'; -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import * as layer from 'ol/layer'; import * as source from 'ol/source'; import * as style from 'ol/style'; @@ -25,9 +25,11 @@ export const lastPointTracker: MapLayerRegistryItem<LastPointConfig> = { /** * Function that configures transformation and returns a transformer + * @param map * @param options + * @param theme */ - create: async (map: Map, options: MapLayerOptions<LastPointConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<LastPointConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { const point = new Feature({}); const config = { ...defaultOptions, ...options.config }; diff --git a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx index fff70ecda1a..001177223ed 100644 --- a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx @@ -1,4 +1,4 @@ -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import { Point } from 'ol/geom'; import { VectorImage } from 'ol/layer'; import LayerGroup from 'ol/layer/Group'; @@ -14,7 +14,9 @@ import { GrafanaTheme2, FrameGeometrySourceMode, EventBus, + PanelOptionsEditorBuilder, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource'; import { getLocationMatchers } from 'app/features/geo/utils/location'; @@ -67,7 +69,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { * @param options * @param theme */ - create: async (map: Map, options: MapLayerOptions<MarkersConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<MarkersConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { // Assert default values const config = { ...defaultOptions, @@ -86,8 +88,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { let hasVector = hasText; const layers = new LayerGroup({ - // If text and no symbol, only show text - fall back on default symbol - layers: hasVector && symbol ? [symbolLayer, vectorLayer] : hasVector && !symbol ? [vectorLayer] : [symbolLayer], + layers: hasVector ? (symbol ? [symbolLayer, vectorLayer] : [vectorLayer]) : [symbolLayer], }); const legendProps = new ReplaySubject<MarkersLegendProps>(1); @@ -175,7 +176,11 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { processedMarkers.add(markerKey); } - if (!isLineString) { + // Set style to be used by LineString + if (isLineString) { + const lineStringStyle = style.maker(values); + feature.setStyle(lineStringStyle); + } else { const colorString = tinycolor(theme.visualization.getColorByName(values.color)).toString(); const colorValues = getRGBValues(colorString); @@ -198,12 +203,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { const textStyle = textMarker(values); feature.setStyle(textStyle); } - - // Set style to be used by LineString - if (isLineString) { - const lineStringStyle = style.maker(values); - feature.setStyle(lineStringStyle); - } }); // Update hasVector state after processing all features @@ -212,10 +211,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { // Update layer visibility based on current hasVector state const layersArray = layers.getLayers(); layersArray.clear(); - if (hasVector && symbol) { - layersArray.extend([symbolLayer, vectorLayer]); - } else if (hasVector && !symbol) { - layersArray.extend([vectorLayer]); + if (hasVector) { + layersArray.extend(symbol ? [symbolLayer, vectorLayer] : [vectorLayer]); } else { layersArray.extend([symbolLayer]); } @@ -225,12 +222,12 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { }, // Marker overlay options - registerOptionsUI: (builder) => { + registerOptionsUI: (builder: PanelOptionsEditorBuilder<MapLayerOptions<MarkersConfig>>) => { builder .addCustomEditor({ id: 'config.style', path: 'config.style', - name: 'Styles', + name: t('geomap.markers-layer.name-styles', 'Styles'), editor: StyleEditor, settings: { displayRotation: true, @@ -239,8 +236,8 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = { }) .addBooleanSwitch({ path: 'config.showLegend', - name: 'Show legend', - description: 'Show map legend', + name: t('geomap.markers-layer.name-show-legend', 'Show legend'), + description: t('geomap.markers-layer.description-show-legend', 'Show map legend'), defaultValue: defaultOptions.showLegend, }); }, diff --git a/public/app/plugins/panel/geomap/layers/data/networkLayer.tsx b/public/app/plugins/panel/geomap/layers/data/networkLayer.tsx index 733c938fd95..e6375e79159 100644 --- a/public/app/plugins/panel/geomap/layers/data/networkLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/networkLayer.tsx @@ -1,7 +1,6 @@ import { isNumber } from 'lodash'; -import { Feature } from 'ol'; -import { FeatureLike } from 'ol/Feature'; -import Map from 'ol/Map'; +import Feature, { FeatureLike } from 'ol/Feature'; +import OpenLayersMap from 'ol/Map'; import { Geometry, LineString, Point, SimpleGeometry } from 'ol/geom'; import VectorImage from 'ol/layer/VectorImage'; import { Fill, Stroke, Style, Text } from 'ol/style'; @@ -76,10 +75,9 @@ export const networkLayer: MapLayerRegistryItem<NetworkConfig> = { * Function that configures transformation and returns a transformer * @param map * @param options - * @param eventBus * @param theme */ - create: async (map: Map, options: MapLayerOptions<NetworkConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<NetworkConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { // Assert default values const config = { ...defaultOptions, @@ -90,9 +88,8 @@ export const networkLayer: MapLayerRegistryItem<NetworkConfig> = { const edgeStyle = await getStyleConfigState(config.edgeStyle); const location = await getLocationMatchers(options.location); const source = new FrameVectorSource(location); - const vectorLayer = new VectorImage({ - source, + source }); const hasArrows = config.arrow === 1 || config.arrow === -1 || config.arrow === 2; diff --git a/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx b/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx index ed9a77ce960..86932984cc2 100644 --- a/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/photosLayer.tsx @@ -1,12 +1,22 @@ import { FeatureLike } from 'ol/Feature'; -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import VectorImage from 'ol/layer/VectorImage'; import { Stroke, Style } from 'ol/style'; import Photo from 'ol-ext/style/Photo'; -import { MapLayerRegistryItem, PanelData, GrafanaTheme2, EventBus, PluginState, FieldType, Field } from '@grafana/data'; -import { FrameGeometrySourceMode, MapLayerOptions } from '@grafana/schema'; -import { findField } from 'app/features/dimensions'; +import { + MapLayerRegistryItem, + PanelData, + GrafanaTheme2, + EventBus, + PluginState, + FieldType, + Field, + MapLayerOptions, +} from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { FrameGeometrySourceMode } from '@grafana/schema'; +import { findField } from 'app/features/dimensions/utils'; import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource'; import { getLocationMatchers } from 'app/features/geo/utils/location'; @@ -69,7 +79,7 @@ export const photosLayer: MapLayerRegistryItem<PhotoConfig> = { * @param options * @param theme */ - create: async (map: Map, options: MapLayerOptions<PhotoConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<PhotoConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { // Assert default values const config = { ...defaultOptions, @@ -187,7 +197,7 @@ export const photosLayer: MapLayerRegistryItem<PhotoConfig> = { name: 'Image Source field', settings: { filter: (f: Field) => f.type === FieldType.string, - noFieldsMessage: 'No string fields found', + noFieldsMessage: t('geomap.photos-layer.noFieldsMessage-no-string-fields', 'No string fields found'), }, }) .addRadio({ diff --git a/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx b/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx index 366b687c5aa..16fd90076bd 100644 --- a/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx @@ -1,6 +1,6 @@ import { isNumber } from 'lodash'; import Feature, { FeatureLike } from 'ol/Feature'; -import Map from 'ol/Map'; +import OpenLayersMap from 'ol/Map'; import { LineString, Point, SimpleGeometry } from 'ol/geom'; import { Group as LayerGroup } from 'ol/layer'; import VectorImage from 'ol/layer/VectorImage'; @@ -20,9 +20,10 @@ import { DataHoverClearEvent, DataFrame, FieldType, - colorManipulator + colorManipulator, + MapLayerOptions, } from '@grafana/data'; -import { MapLayerOptions, FrameGeometrySourceMode } from '@grafana/schema'; +import { FrameGeometrySourceMode } from '@grafana/schema'; import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource'; import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location'; @@ -84,7 +85,7 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = { * Function that configures transformation and returns a transformer * @param options */ - create: async (map: Map, options: MapLayerOptions<RouteConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { + create: async (map: OpenLayersMap, options: MapLayerOptions<RouteConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { // Assert default values const config = { ...defaultOptions, @@ -102,7 +103,7 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = { const styleBase = routeStyle(style.base); if (style.config.size && style.config.size.fixed) { // Applies width to base style if specified - styleBase.getStroke().setWidth(style.config.size.fixed); + styleBase.getStroke()?.setWidth(style.config.size.fixed); } vectorLayer.setStyle(styleBase); } else { @@ -306,7 +307,30 @@ export const routeLayer: MapLayerRegistryItem<RouteConfig> = { style.dims = getStyleDimension(frame, style, theme); } - source.updateLineString(frame); + source.clear(true); + const info = getGeometryField(frame, location); + if (!info.field) { + source.changed(); + break; + } + const coords: number[][] = []; + for (const v of info.field.values) { + if (v instanceof Point) { + coords.push(v.getCoordinates()); + } + } + if (coords.length >= 2) { + const geometry = new LineString(coords); + source['addFeatureInternal']( + new Feature({ + frame, + rowIndex: 0, + geometry, + }) + ); + } + source.changed(); + break; // Only the first frame for now! } }, diff --git a/public/app/plugins/panel/geomap/layers/registry.ts b/public/app/plugins/panel/geomap/layers/registry.ts index 8657a55cb36..15e6fbb7861 100644 --- a/public/app/plugins/panel/geomap/layers/registry.ts +++ b/public/app/plugins/panel/geomap/layers/registry.ts @@ -1,4 +1,4 @@ -import { Map as OpenLayersMap } from 'ol'; +import OpenLayersMap from 'ol/Map'; import { MapLayerRegistryItem, diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index b5376ea7acf..f8c7825f896 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -1,5 +1,5 @@ import { PanelPlugin } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { commonOptionsBuilder } from '@grafana/ui'; @@ -20,13 +20,13 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel) }, }) .setPanelOptions((builder, context) => { - let category = ['Map view']; + let category = [t('geomap.category-map-view', 'Map view')]; builder.addCustomEditor({ category, id: 'view', path: 'view', - name: 'Initial view', // don't show it - description: 'This location will show when the panel first loads.', + name: t('geomap.name-initial-view', 'Initial view'), // don't show it + description: t('geomap.description-initial-view', 'This location will show when the panel first loads.'), editor: MapViewEditor, defaultValue: defaultMapViewConfig, }); @@ -34,8 +34,11 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel) builder.addBooleanSwitch({ category, path: 'view.shared', - description: 'Use the same view across multiple panels. Note: this may require a dashboard reload.', - name: 'Share view', + description: t( + 'geomap.description-share-view', + 'Use the same view across multiple panels. Note: this may require a dashboard reload.' + ), + name: t('geomap.name-share-view', 'Share view'), defaultValue: defaultMapViewConfig.shared, }); @@ -44,8 +47,8 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel) if (!state?.layers) { // TODO? show spinner? } else { - const layersCategory = ['Map layers']; - const basemapCategory = ['Basemap layer']; + const layersCategory = [t('geomap.category-map-layers', 'Map layers')]; + const basemapCategory = [t('geomap.category-basemap-layer', 'Basemap layer')]; builder.addCustomEditor({ category: layersCategory, id: 'layers', @@ -93,59 +96,70 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel) } // The controls section - category = ['Map controls']; + category = [t('geomap.category-map-controls', 'Map controls')]; builder .addBooleanSwitch({ category, path: 'controls.showZoom', - description: 'Show zoom control buttons in the upper left corner', - name: 'Show zoom control', + description: t('geomap.description-show-zoom', 'Show zoom control buttons in the upper left corner'), + name: t('geomap.name-show-zoom', 'Show zoom control'), defaultValue: true, }) .addBooleanSwitch({ category, path: 'controls.mouseWheelZoom', - description: 'Enable zoom control via mouse wheel', - name: 'Mouse wheel zoom', + description: t('geomap.description-mouse-wheel-zoom', 'Enable zoom control via mouse wheel'), + name: t('geomap.name-mouse-wheel-zoom', 'Mouse wheel zoom'), defaultValue: true, }) .addBooleanSwitch({ category, path: 'controls.showAttribution', - name: 'Show attribution', - description: 'Show the map source attribution info in the lower right', + name: t('geomap.name-show-attribution', 'Show attribution'), + description: t( + 'geomap.description-show-attribution', + 'Show the map source attribution info in the lower right' + ), defaultValue: true, }) .addBooleanSwitch({ category, path: 'controls.showScale', - name: 'Show scale', - description: 'Indicate map scale', + name: t('geomap.name-show-scale', 'Show scale'), + description: t('geomap.description-show-scale', 'Indicate map scale'), defaultValue: false, }) .addBooleanSwitch({ category, path: 'controls.showMeasure', - name: 'Show measure tools', - description: 'Show tools for making measurements on the map', + name: t('geomap.name-show-measure', 'Show measure tools'), + description: t('geomap.description-show-measure', 'Show tools for making measurements on the map'), defaultValue: false, }) .addBooleanSwitch({ category, path: 'controls.showDebug', - name: 'Show debug', - description: 'Show map info', + name: t('geomap.name-show-debug', 'Show debug'), + description: t('geomap.description-show-debug', 'Show map info'), defaultValue: false, }) .addRadio({ category, path: 'tooltip.mode', - name: 'Tooltip', + name: t('geomap.name-tooltip', 'Tooltip'), defaultValue: TooltipMode.Details, settings: { options: [ - { label: 'None', value: TooltipMode.None, description: 'Show contents on click, not hover' }, - { label: 'Details', value: TooltipMode.Details, description: 'Show popup on hover' }, + { + label: t('geomap.tooltip-options.label-none', 'None'), + value: TooltipMode.None, + description: t('geomap.tooltip-options.description-none', 'Show contents on click, not hover'), + }, + { + label: t('geomap.tooltip-options.label-details', 'Details'), + value: TooltipMode.Details, + description: t('geomap.tooltip-options.description-details', 'Show popup on hover'), + }, ], }, }); diff --git a/public/app/plugins/panel/geomap/style/markers.test.ts b/public/app/plugins/panel/geomap/style/markers.test.ts index cd7e5acf452..c7ce4cb845b 100644 --- a/public/app/plugins/panel/geomap/style/markers.test.ts +++ b/public/app/plugins/panel/geomap/style/markers.test.ts @@ -1,9 +1,17 @@ -import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; +import { getPublicOrAbsoluteUrl } from 'app/features/dimensions/resource'; -import { getWebGLStyle } from './markers'; +import { + getWebGLStyle, + baseCircleStyle, + baseShapeStyle, + sizeExpression, + opacityExpression, + rotationExpression, + offsetExpression, +} from './markers'; // Mock dependencies -jest.mock('app/features/dimensions', () => ({ +jest.mock('app/features/dimensions/resource', () => ({ getPublicOrAbsoluteUrl: jest.fn(), })); @@ -14,40 +22,30 @@ describe('getWebGLStyle', () => { it('returns default circle style when no symbol is provided', async () => { const result = await getWebGLStyle(); - expect(result).toEqual({ - symbol: { - symbolType: 'circle', - size: ['get', 'size', 'number'], - color: ['color', ['get', 'red'], ['get', 'green'], ['get', 'blue']], - offset: ['array', ['get', 'offsetX'], ['get', 'offsetY']], - rotation: ['get', 'rotation', 'number'], - opacity: ['get', 'opacity', 'number'], - }, - }); + expect(result).toEqual(baseCircleStyle); }); it('returns circle style for known WebGL regular shape', async () => { const result = await getWebGLStyle('img/icons/marker/circle.svg'); - if (result.symbol) { - expect(result.symbol.symbolType).toBe('circle'); - expect(result.symbol).not.toHaveProperty('src'); - } + expect(result).toEqual(baseCircleStyle); }); - it('returns square style for known WebGL regular shape', async () => { + it('returns shape style for square WebGL regular shape', async () => { const result = await getWebGLStyle('img/icons/marker/square.svg'); - if (result.symbol) { - expect(result.symbol.symbolType).toBe('square'); - expect(result.symbol).not.toHaveProperty('src'); - } + expect(result).toEqual({ + ...baseShapeStyle, + 'shape-points': 4, + 'shape-angle': Math.PI / 4, + }); }); - it('returns triangle style for known WebGL regular shape', async () => { + it('returns shape style for triangle WebGL regular shape', async () => { const result = await getWebGLStyle('img/icons/marker/triangle.svg'); - if (result.symbol) { - expect(result.symbol.symbolType).toBe('triangle'); - expect(result.symbol).not.toHaveProperty('src'); - } + expect(result).toEqual({ + ...baseShapeStyle, + 'shape-points': 3, + 'shape-angle': 0, + }); }); it('returns image style with src for custom SVG symbol', async () => { @@ -62,10 +60,12 @@ describe('getWebGLStyle', () => { } as Response) ); const result = await getWebGLStyle('test.svg'); - if (result.symbol) { - expect(result.symbol.symbolType).toBe('image'); - expect(result.symbol.src).toContain('data:image/svg+xml'); - } + expect(result['icon-src']).toContain('data:image/svg+xml'); + expect(result['icon-width']).toEqual(sizeExpression); + expect(result['icon-height']).toEqual(sizeExpression); + expect(result['icon-opacity']).toEqual(opacityExpression); + expect(result['icon-rotation']).toEqual(rotationExpression); + expect(result['icon-displacement']).toEqual(offsetExpression); }); it('includes background circle with opacity-adjusted stroke when opacity is provided', async () => { @@ -79,12 +79,10 @@ describe('getWebGLStyle', () => { } as Response) ); const result = await getWebGLStyle('custom.svg', 0.5); - if (result.symbol?.src) { - expect(result.symbol.symbolType).toBe('image'); - expect(result.symbol.src).toContain('circle'); - const decodedSrc = decodeURIComponent(result.symbol.src); - expect(decodedSrc).toContain('stroke="rgba(255,255,255,0.2)"'); // 0.1 / 0.5 = 0.2 - } + const iconSrc = result['icon-src'] as string; + expect(iconSrc).toContain('circle'); + const decodedSrc = decodeURIComponent(iconSrc); + expect(decodedSrc).toContain('stroke="rgba(255,255,255,0.2)"'); // 0.1 / 0.5 = 0.2 }); it('handles fetch error gracefully', async () => { @@ -94,10 +92,8 @@ describe('getWebGLStyle', () => { (getPublicOrAbsoluteUrl as jest.Mock).mockReturnValue('error.svg'); global.fetch = jest.fn(() => Promise.reject(new Error('Fetch failed'))); const result = await getWebGLStyle('error.svg'); - if (result.symbol) { - expect(result.symbol.symbolType).toBe('image'); - expect(result.symbol.src).toBe(''); // Empty SVG - } + expect(result['icon-src']).toBe(''); // Empty SVG + // Verify console.error was called with the expected error expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Fetch failed')); diff --git a/public/app/plugins/panel/geomap/style/markers.ts b/public/app/plugins/panel/geomap/style/markers.ts index 04772af1189..fd8682866ca 100644 --- a/public/app/plugins/panel/geomap/style/markers.ts +++ b/public/app/plugins/panel/geomap/style/markers.ts @@ -1,10 +1,10 @@ import { Fill, RegularShape, Stroke, Circle, Style, Icon, Text } from 'ol/style'; -import { LiteralStyle } from 'ol/style/literal'; +import type { FlatStyle } from 'ol/style/flat'; import tinycolor from 'tinycolor2'; import { Registry, RegistryItem, textUtil } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { getPublicOrAbsoluteUrl } from 'app/features/dimensions'; +import { getPublicOrAbsoluteUrl } from 'app/features/dimensions/resource'; import { defaultStyleConfig, DEFAULT_SIZE, StyleConfigValues, StyleMaker } from './types'; import { getDisplacement } from './utils'; @@ -32,12 +32,6 @@ const MarkerShapePath = { x: 'img/icons/marker/x-mark.svg', }; -const WebGLRegularShapes: Record<string, string> = { - circle: 'img/icons/marker/circle.svg', - square: 'img/icons/marker/square.svg', - triangle: 'img/icons/marker/triangle.svg', -}; - export function getFillColor(cfg: StyleConfigValues) { const opacity = cfg.opacity == null ? 0.8 : cfg.opacity; if (opacity === 1) { @@ -319,34 +313,75 @@ export function getMarkerAsPath(shape?: string): string | undefined { return undefined; } -// Returns literal style for WebGL markers -export async function getWebGLStyle(symbol?: string, opacity?: number): Promise<LiteralStyle> { - // style expressions - const symbolStyle: LiteralStyle = { - symbol: { - symbolType: 'circle', - size: ['get', 'size', 'number'], - color: ['color', ['get', 'red'], ['get', 'green'], ['get', 'blue']], - offset: ['array', ['get', 'offsetX'], ['get', 'offsetY']], - rotation: ['get', 'rotation', 'number'], - opacity: ['get', 'opacity', 'number'], - }, - }; - // set symbolType and src if a symbol is provided - if (symbol && symbolStyle.symbol) { - const imageString = 'image'; - const symbolType = Object.keys(WebGLRegularShapes).find((key) => WebGLRegularShapes[key] === symbol) ?? imageString; - symbolStyle.symbol = { ...symbolStyle.symbol, symbolType }; - if (symbolType === imageString) { - const backgroundOpacity = opacity === 0 ? 0 : 0.1 / (opacity ?? 1); - symbolStyle.symbol = { - ...symbolStyle.symbol, - src: await prepareSVG(getPublicOrAbsoluteUrl(symbol), undefined, backgroundOpacity), - }; - } +// Common expressions used across different style types +export const colorExpression = ['color', ['get', 'red'], ['get', 'green'], ['get', 'blue'], ['get', 'opacity']]; +export const sizeExpression = ['get', 'size']; +export const opacityExpression = ['get', 'opacity']; +export const rotationExpression = ['get', 'rotation']; +export const offsetExpression = ['array', ['get', 'offsetX'], ['get', 'offsetY']]; + +// Base style for regular shapes +export const baseShapeStyle = { + 'shape-radius': ['/', sizeExpression, 2], + 'shape-fill-color': colorExpression, + 'shape-stroke-color': colorExpression, + 'shape-stroke-width': 1, + 'shape-opacity': opacityExpression, + 'shape-rotation': rotationExpression, + 'shape-displacement': offsetExpression, +}; + +// Base style for circles +export const baseCircleStyle = { + 'circle-radius': ['/', sizeExpression, 2], + 'circle-fill-color': colorExpression, + 'circle-stroke-color': colorExpression, + 'circle-stroke-width': 1, + 'circle-opacity': opacityExpression, + 'circle-displacement': offsetExpression, +}; + +// Returns style configuration for WebGL markers +export async function getWebGLStyle(symbol?: string, opacity?: number): Promise<FlatStyle> { + // Handle circle explicitly (before generic SVG check) + if (symbol === MarkerShapePath.circle) { + return baseCircleStyle; } - return symbolStyle; + // Handle square as WebGL regular shape + if (symbol === MarkerShapePath.square) { + return { + ...baseShapeStyle, + 'shape-points': 4, + 'shape-angle': Math.PI / 4, + }; + } + + // Handle triangle as WebGL regular shape + if (symbol === MarkerShapePath.triangle) { + return { + ...baseShapeStyle, + 'shape-points': 3, + 'shape-angle': 0, + }; + } + + // Handle custom SVG symbols and other shapes as icons + if (symbol && symbol.endsWith('.svg')) { + const backgroundOpacity = opacity === 0 ? 0 : 0.1 / (opacity ?? 1); + return { + 'icon-src': await prepareSVG(getPublicOrAbsoluteUrl(symbol), undefined, backgroundOpacity), + 'icon-width': sizeExpression, + 'icon-height': sizeExpression, + 'icon-opacity': opacityExpression, + 'icon-rotation': rotationExpression, + 'icon-displacement': offsetExpression, + 'icon-color': colorExpression, + }; + } + + // Default to circle (also handles MarkerShapePath.circle) + return baseCircleStyle; } // Will prepare symbols as necessary @@ -388,7 +423,7 @@ export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): P image: new RegularShape({ fill: new Fill({ color: 'rgba(0,0,0,0)' }), points: 4, - radius: cfg.size, + radius: radius, rotation: (rotation * Math.PI) / 180 + Math.PI / 4, }), }), diff --git a/public/app/plugins/panel/geomap/style/types.ts b/public/app/plugins/panel/geomap/style/types.ts index a1e7329ee33..72703a90476 100644 --- a/public/app/plugins/panel/geomap/style/types.ts +++ b/public/app/plugins/panel/geomap/style/types.ts @@ -9,7 +9,7 @@ import { ScalarDimensionMode, TextDimensionConfig, } from '@grafana/schema'; -import { DimensionSupplier } from 'app/features/dimensions'; +import { DimensionSupplier } from 'app/features/dimensions/types'; export enum GeometryTypeId { Point = 'point', diff --git a/public/app/plugins/panel/geomap/types.ts b/public/app/plugins/panel/geomap/types.ts index 22804bc6b29..2ef58f6e65e 100644 --- a/public/app/plugins/panel/geomap/types.ts +++ b/public/app/plugins/panel/geomap/types.ts @@ -1,5 +1,5 @@ -import { Map as OpenLayersMap } from 'ol'; import { FeatureLike } from 'ol/Feature'; +import OpenLayersMap from 'ol/Map'; import { Units } from 'ol/control/ScaleLine'; import BaseLayer from 'ol/layer/Base'; import { Subject } from 'rxjs'; diff --git a/public/app/plugins/panel/geomap/utils/checkFeatureMatchesStyleRule.test.ts b/public/app/plugins/panel/geomap/utils/checkFeatureMatchesStyleRule.test.ts index 7dc2d839ea0..60e90245fba 100644 --- a/public/app/plugins/panel/geomap/utils/checkFeatureMatchesStyleRule.test.ts +++ b/public/app/plugins/panel/geomap/utils/checkFeatureMatchesStyleRule.test.ts @@ -1,4 +1,4 @@ -import { Feature } from 'ol'; +import Feature from 'ol/Feature'; import { ComparisonOperation } from '@grafana/schema'; diff --git a/public/app/plugins/panel/geomap/utils/getFeatures.test.ts b/public/app/plugins/panel/geomap/utils/getFeatures.test.ts index 0b2b31a3f45..787503a5eff 100644 --- a/public/app/plugins/panel/geomap/utils/getFeatures.test.ts +++ b/public/app/plugins/panel/geomap/utils/getFeatures.test.ts @@ -1,4 +1,4 @@ -import { Feature } from 'ol'; +import Feature from 'ol/Feature'; import { Point } from 'ol/geom'; import { GeometryTypeId } from '../style/types'; diff --git a/public/app/plugins/panel/geomap/utils/layers.ts b/public/app/plugins/panel/geomap/utils/layers.ts index 8c20a95ad0f..a6e9ecbbff0 100644 --- a/public/app/plugins/panel/geomap/utils/layers.ts +++ b/public/app/plugins/panel/geomap/utils/layers.ts @@ -1,5 +1,6 @@ -import { Map as OpenLayersMap } from 'ol'; import { FeatureLike } from 'ol/Feature'; +import OpenLayersMap from 'ol/Map'; +import BaseLayer from 'ol/layer/Base'; import LayerGroup from 'ol/layer/Group'; import WebGLPointsLayer from 'ol/layer/WebGLPoints'; import { Subject } from 'rxjs'; @@ -14,6 +15,8 @@ import { MapLayerState } from '../types'; import { getNextLayerName } from './utils'; +const layerStateMap = new WeakMap<BaseLayer, MapLayerState>(); + export const applyLayerFilter = ( handler: MapLayerHandler<unknown>, options: MapLayerOptions<unknown>, @@ -148,18 +151,16 @@ export async function initLayer( }; panel.byName.set(UID, state); - // eslint-disable-next-line - (state.layer as any).__state = state; + layerStateMap.set(state.layer, state); // Pass state into WebGLPointsLayers contained in a LayerGroup if (layer instanceof LayerGroup) { layer .getLayers() .getArray() - .forEach((layer) => { + .forEach((layer: BaseLayer) => { if (layer instanceof WebGLPointsLayer) { - // eslint-disable-next-line - (layer as any).__state = state; + layerStateMap.set(layer, state); } }); } @@ -169,6 +170,6 @@ export async function initLayer( return state; } -export const getMapLayerState = (l: any): MapLayerState => { - return l?.__state; +export const getMapLayerState = (l: BaseLayer | undefined): MapLayerState | undefined => { + return l ? layerStateMap.get(l) : undefined; }; diff --git a/public/app/plugins/panel/geomap/utils/tooltip.test.ts b/public/app/plugins/panel/geomap/utils/tooltip.test.ts index a5b721da7d4..3f006391ad6 100644 --- a/public/app/plugins/panel/geomap/utils/tooltip.test.ts +++ b/public/app/plugins/panel/geomap/utils/tooltip.test.ts @@ -1,4 +1,5 @@ -import { Feature, MapBrowserEvent } from 'ol'; +import Feature from 'ol/Feature'; +import MapBrowserEvent from 'ol/MapBrowserEvent'; import { Point } from 'ol/geom'; import WebGLPointsLayer from 'ol/layer/WebGLPoints'; import VectorSource from 'ol/source/Vector'; @@ -55,9 +56,9 @@ jest.mock('./layers', () => { describe('tooltip utils', () => { let panel: GeomapPanel; - let mockEvent: MapBrowserEvent<MouseEvent>; - let mockWebGLLayer: WebGLPointsLayer<VectorSource<Point>>; - let mockVectorSource: VectorSource<Point>; + let mockEvent: MapBrowserEvent<PointerEvent>; + let mockWebGLLayer: WebGLPointsLayer<VectorSource<Feature<Point>>>; + let mockVectorSource: VectorSource<Feature<Point>>; // Consolidated feature constants let feature1: Feature; @@ -72,12 +73,15 @@ describe('tooltip utils', () => { // Create mock objects panel = new GeomapPanel({} as PanelProps<Options>); + + // Create a proper MouseEvent instance to pass the instanceof check + const mouseEvent = new MouseEvent('pointermove'); + Object.defineProperty(mouseEvent, 'pageX', { value: 100 }); + Object.defineProperty(mouseEvent, 'pageY', { value: 100 }); + mockEvent = { - originalEvent: { - pageX: 100, - pageY: 100, - }, - } as MapBrowserEvent<MouseEvent>; + originalEvent: mouseEvent, + } as MapBrowserEvent<PointerEvent>; // Create features for testing feature1 = new Feature({ @@ -111,19 +115,16 @@ describe('tooltip utils', () => { }); // Create mock vector source - mockVectorSource = new VectorSource<Point>(); + mockVectorSource = new VectorSource<Feature<Point>>(); mockVectorSource.forEachFeature = jest.fn(); // Create mock WebGL layer mockWebGLLayer = new WebGLPointsLayer({ source: mockVectorSource, style: { - symbol: { - symbolType: 'circle', - size: 8, - color: '#000000', - opacity: 1, - }, + 'circle-radius': 8, + 'circle-fill-color': '#000000', + 'circle-opacity': 1, }, }); mockWebGLLayer.getSource = jest.fn().mockReturnValue(mockVectorSource); diff --git a/public/app/plugins/panel/geomap/utils/tooltip.ts b/public/app/plugins/panel/geomap/utils/tooltip.ts index 81ed086afac..05ed7e781f6 100644 --- a/public/app/plugins/panel/geomap/utils/tooltip.ts +++ b/public/app/plugins/panel/geomap/utils/tooltip.ts @@ -1,6 +1,6 @@ import { debounce } from 'lodash'; -import { MapBrowserEvent } from 'ol'; import { FeatureLike } from 'ol/Feature'; +import MapBrowserEvent from 'ol/MapBrowserEvent'; import { Point } from 'ol/geom'; import WebGLPointsLayer from 'ol/layer/WebGLPoints'; import { toLonLat } from 'ol/proj'; @@ -16,14 +16,17 @@ import { getMapLayerState } from './layers'; export const setTooltipListeners = (panel: GeomapPanel) => { // Tooltip listener - panel.map?.on('singleclick', panel.pointerClickListener); - panel.map?.on('pointermove', debounce(panel.pointerMoveListener, 200)); + panel.map?.on('singleclick', (evt) => pointerClickListener(evt, panel)); + panel.map?.on( + 'pointermove', + debounce((evt) => pointerMoveListener(evt, panel), 200) + ); panel.map?.getViewport().addEventListener('mouseout', (evt: MouseEvent) => { panel.props.eventBus.publish(new DataHoverClearEvent()); }); }; -export const pointerClickListener = (evt: MapBrowserEvent<MouseEvent>, panel: GeomapPanel) => { +export const pointerClickListener = (evt: MapBrowserEvent, panel: GeomapPanel) => { if (pointerMoveListener(evt, panel)) { evt.preventDefault(); evt.stopPropagation(); @@ -32,7 +35,7 @@ export const pointerClickListener = (evt: MapBrowserEvent<MouseEvent>, panel: Ge } }; -export const pointerMoveListener = (evt: MapBrowserEvent<MouseEvent>, panel: GeomapPanel) => { +export const pointerMoveListener = (evt: MapBrowserEvent, panel: GeomapPanel) => { // If measure menu is open, bypass tooltip logic and display measuring mouse events if (panel.state.measureMenuActive) { return true; @@ -43,6 +46,10 @@ export const pointerMoveListener = (evt: MapBrowserEvent<MouseEvent>, panel: Geo return false; } + if (!(evt.originalEvent instanceof MouseEvent)) { + return false; + } + const mouse = evt.originalEvent; const pixel = panel.map.getEventPixel(mouse); const hover = toLonLat(panel.map.getCoordinateFromPixel(pixel)); diff --git a/public/app/plugins/panel/geomap/utils/utils.test.ts b/public/app/plugins/panel/geomap/utils/utils.test.ts index d338233d63c..6ea2c428435 100644 --- a/public/app/plugins/panel/geomap/utils/utils.test.ts +++ b/public/app/plugins/panel/geomap/utils/utils.test.ts @@ -15,10 +15,16 @@ jest.mock('@grafana/runtime', () => ({ })); // Mock the dimensions module since it's imported by utils.ts -jest.mock('app/features/dimensions', () => ({ +jest.mock('app/features/dimensions/color', () => ({ getColorDimension: jest.fn(), +})); +jest.mock('app/features/dimensions/scalar', () => ({ getScalarDimension: jest.fn(), +})); +jest.mock('app/features/dimensions/scale', () => ({ getScaledDimension: jest.fn(), +})); +jest.mock('app/features/dimensions/text', () => ({ getTextDimension: jest.fn(), })); @@ -32,8 +38,8 @@ import { hasVariableDependencies, hasLayerData } from './utils'; // Test fixtures const createTestFeature = () => new Feature(new Point([0, 0])); -const createTestVectorSource = (hasFeature = false): VectorSource<Point> => { - const source = new VectorSource<Point>(); +const createTestVectorSource = (hasFeature = false): VectorSource<Feature<Point>> => { + const source = new VectorSource<Feature<Point>>(); if (hasFeature) { source.addFeature(createTestFeature()); } @@ -41,12 +47,9 @@ const createTestVectorSource = (hasFeature = false): VectorSource<Point> => { }; const createTestWebGLStyle = () => ({ - symbol: { - symbolType: 'circle', - size: 8, - color: '#000000', - opacity: 1, - }, + 'circle-radius': 8, + 'circle-fill-color': '#000000', + 'circle-opacity': 1, }); describe('hasVariableDependencies', () => { diff --git a/public/app/plugins/panel/geomap/utils/utils.ts b/public/app/plugins/panel/geomap/utils/utils.ts index 23426d54db4..f3efa3c1b4f 100644 --- a/public/app/plugins/panel/geomap/utils/utils.ts +++ b/public/app/plugins/panel/geomap/utils/utils.ts @@ -1,4 +1,5 @@ -import { Map as OpenLayersMap } from 'ol'; +import Feature from 'ol/Feature'; +import OpenLayersMap from 'ol/Map'; import Geometry from 'ol/geom/Geometry'; import Point from 'ol/geom/Point'; import { defaults as interactionDefaults } from 'ol/interaction'; @@ -14,8 +15,12 @@ import TileSource from 'ol/source/Tile'; import VectorSource from 'ol/source/Vector'; import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { getTemplateSrv } from '@grafana/runtime'; -import { getColorDimension, getScalarDimension, getScaledDimension, getTextDimension } from 'app/features/dimensions'; +import { getColorDimension } from 'app/features/dimensions/color'; +import { getScalarDimension } from 'app/features/dimensions/scalar'; +import { getScaledDimension } from 'app/features/dimensions/scale'; +import { getTextDimension } from 'app/features/dimensions/text'; import { getGrafanaDatasource } from 'app/plugins/datasource/grafana/datasource'; import { GeomapPanel } from '../GeomapPanel'; @@ -129,13 +134,13 @@ export const notifyPanelEditor = (geomapPanel: GeomapPanel, layers: MapLayerStat export const getNextLayerName = (panel: GeomapPanel) => { let idx = panel.layers.length; // since basemap is 0, this looks right while (true && idx < 100) { - const name = `Layer ${idx++}`; + const name = t('geomap.utils.get-next-layer-name', 'Layer {{name}}', { name: idx++ }); if (!panel.byName.has(name)) { return name; } } - return `Layer ${Date.now()}`; + return t('geomap.utils.get-next-layer-name', 'Layer {{name}}', { name: Date.now() }); }; export function isSegmentVisible( @@ -174,9 +179,9 @@ export const isUrl = (url: string) => { export function hasLayerData( layer: | LayerGroup - | VectorLayer<VectorSource<Geometry>> - | VectorImage<VectorSource<Geometry>> - | WebGLPointsLayer<VectorSource<Point>> + | VectorLayer<VectorSource<Feature<Geometry>>> + | VectorImage<VectorSource<Feature<Geometry>>> + | WebGLPointsLayer<VectorSource<Feature<Point>>> | TileLayer<TileSource> | ImageLayer<ImageSource> | BaseLayer diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index a55bdd30a46..971bb97365f 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -114,6 +114,12 @@ interface LogsPanelProps extends PanelProps<Options> { * When the feature toggle newLogsPanel is enabled, you can pass extra options to the LogLineMenu component. * These options are an array of items with { label, onClick } or { divider: true } for dividers. * logLineMenuCustomItems?: LogLineMenuCustomItem[]; + * + * Use the default, bigger, font size, or a smaller one. Defaults to "default". + * fontSize?: 'default' | 'small' + * + * Set the mode used by the Log Details panel. Displayed as a sidebar, or inline below the log line. Defaults to "inline". + * detailsMode?: 'inline' | 'sidebar' */ } interface LogsPermalinkUrlState { @@ -153,6 +159,7 @@ export const LogsPanel = ({ onNewLogsReceived, fontSize, syntaxHighlighting, + detailsMode: detailsModeProp, ...options }, id, @@ -511,6 +518,9 @@ export const LogsPanel = ({ const onClickShowField = isOnClickShowField(options.onClickShowField) ? options.onClickShowField : showField; const onClickHideField = isOnClickHideField(options.onClickHideField) ? options.onClickHideField : hideField; + // In Dashboards, default to inline. Otherwise, let apps control or have automatic behavior. + const detailsMode = detailsModeProp ? detailsModeProp : app === CoreApp.Dashboard ? 'inline' : undefined; + return ( <> {contextRow && ( @@ -535,6 +545,7 @@ export const LogsPanel = ({ app={isCoreApp(app) ? app : CoreApp.Dashboard} containerElement={scrollElement} dedupStrategy={dedupStrategy} + detailsMode={detailsMode} displayedFields={displayedFields} enableLogDetails={enableLogDetails} fontSize={fontSize} @@ -565,6 +576,7 @@ export const LogsPanel = ({ onOpenContext={onOpenContext} onPermalinkClick={showPermaLink() ? onPermalinkClick : undefined} permalinkedLogId={getLogsPanelState()?.logs?.id ?? undefined} + setDisplayedFields={setDisplayedFields} showControls={Boolean(showControls)} showTime={showTime} sortOrder={sortOrder} diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index 0e339918d65..cccc4cfab01 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -108,6 +108,21 @@ export const plugin = new PanelPlugin<Options>(LogsPanel) }, ], }, + }) + .addRadio({ + path: 'detailsMode', + name: t('logs.name-details-mode', 'Log Details panel mode'), + category, + description: '', + settings: { + options: [ + { value: 'inline', label: t('logs.name-details-options.label-inline', 'Inline') }, + { + value: 'sidebar', + label: t('logs.name-details-options.label-sidebar', 'Sidebar'), + }, + ], + }, }); } diff --git a/public/app/plugins/panel/logs/panelcfg.cue b/public/app/plugins/panel/logs/panelcfg.cue index cd21fc2e041..70ddf33b412 100644 --- a/public/app/plugins/panel/logs/panelcfg.cue +++ b/public/app/plugins/panel/logs/panelcfg.cue @@ -40,6 +40,7 @@ composableKinds: PanelCfg: { dedupStrategy: common.LogsDedupStrategy enableInfiniteScrolling?: bool fontSize?: "default" | "small" @cuetsy(kind="enum", memberNames="default|small") + detailsMode?: "inline" | "sidebar" @cuetsy(kind="enum", memberNames="inline|sidebar") // TODO: figure out how to define callbacks onClickFilterLabel?: _ onClickFilterOutLabel?: _ diff --git a/public/app/plugins/panel/logs/panelcfg.gen.ts b/public/app/plugins/panel/logs/panelcfg.gen.ts index 5af4d61f578..a07151c7fda 100644 --- a/public/app/plugins/panel/logs/panelcfg.gen.ts +++ b/public/app/plugins/panel/logs/panelcfg.gen.ts @@ -13,6 +13,7 @@ import * as common from '@grafana/schema'; export interface Options { controlsStorageKey?: string; dedupStrategy: common.LogsDedupStrategy; + detailsMode?: ('inline' | 'sidebar'); displayedFields?: Array<string>; enableInfiniteScrolling?: boolean; enableLogDetails: boolean; diff --git a/public/app/plugins/panel/nodeGraph/Edge.tsx b/public/app/plugins/panel/nodeGraph/Edge.tsx index 7a8a2911c67..ea1fa18f963 100644 --- a/public/app/plugins/panel/nodeGraph/Edge.tsx +++ b/public/app/plugins/panel/nodeGraph/Edge.tsx @@ -17,11 +17,10 @@ interface Props { onClick: (event: MouseEvent<SVGElement>, link: EdgeDatumLayout) => void; onMouseEnter: (id: string) => void; onMouseLeave: (id: string) => void; - processedNodesLength: number; } export const Edge = memo(function Edge(props: Props) { - const { edge, onClick, onMouseEnter, onMouseLeave, hovering, svgIdNamespace, processedNodesLength } = props; + const { edge, onClick, onMouseEnter, onMouseLeave, hovering, svgIdNamespace } = props; // Not great typing but after we do layout these properties are full objects not just references const { source, target, sourceNodeRadius, targetNodeRadius } = edge as { @@ -59,7 +58,6 @@ export const Edge = memo(function Edge(props: Props) { <EdgeArrowMarker id={markerId} fill={edgeColor} headHeight={arrowHeadHeight} /> <EdgeArrowMarker id={coloredMarkerId} fill={highlightedEdgeColor} headHeight={arrowHeadHeight} /> <g - key={`${edge.id}-${edge.source.y ?? ''}-${processedNodesLength}-g`} onClick={(event) => onClick(event, edge)} style={{ cursor: 'pointer' }} aria-label={t('nodeGraph.edge.aria-label-from-to', 'Edge from: {{from}} to: {{to}}', { diff --git a/public/app/plugins/panel/nodeGraph/NodeGraph.tsx b/public/app/plugins/panel/nodeGraph/NodeGraph.tsx index 08a06af7b27..4a398abe7a7 100644 --- a/public/app/plugins/panel/nodeGraph/NodeGraph.tsx +++ b/public/app/plugins/panel/nodeGraph/NodeGraph.tsx @@ -290,6 +290,7 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode, onMouseLeave={clearEdgeHover} svgIdNamespace={svgIdNamespace} processedNodesLength={processed.nodes.length} + processedEdgesLength={processed.edges.length} /> )} <Nodes @@ -430,26 +431,28 @@ interface EdgesProps { onMouseEnter: (id: string) => void; onMouseLeave: (id: string) => void; processedNodesLength: number; + processedEdgesLength: number; } const Edges = memo(function Edges(props: EdgesProps) { return ( <> - {props.edges.map((e) => ( - <Edge - key={`${e.id}-${e.source.y ?? ''}-${props.processedNodesLength}`} - edge={e} - hovering={ - (e.source as NodeDatum).id === props.nodeHoveringId || - (e.target as NodeDatum).id === props.nodeHoveringId || - props.edgeHoveringId === e.id - } - onClick={props.onClick} - onMouseEnter={props.onMouseEnter} - onMouseLeave={props.onMouseLeave} - svgIdNamespace={props.svgIdNamespace} - processedNodesLength={props.processedNodesLength} - /> - ))} + {props.edges.map((e, index) => { + return ( + <Edge + key={`${e.id}-${e.source.y ?? ''}-${props.processedNodesLength}-${props.processedEdgesLength}-${index}`} + edge={e} + hovering={ + (e.source as NodeDatum).id === props.nodeHoveringId || + (e.target as NodeDatum).id === props.nodeHoveringId || + props.edgeHoveringId === e.id + } + onClick={props.onClick} + onMouseEnter={props.onMouseEnter} + onMouseLeave={props.onMouseLeave} + svgIdNamespace={props.svgIdNamespace} + /> + ); + })} </> ); }); @@ -462,7 +465,7 @@ interface EdgeLabelsProps { const EdgeLabels = memo(function EdgeLabels(props: EdgeLabelsProps) { return ( <> - {props.edges.map((e, index) => { + {props.edges.map((e) => { // We show the edge label in case user hovers over the edge directly or if they hover over node edge is // connected to. const shouldShow = diff --git a/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx index ad5d9e400be..ee4d69c1add 100644 --- a/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/table-new/TableCellOptionEditor.tsx @@ -91,6 +91,7 @@ let cellDisplayModeOptions: Array<SelectableValue<TableCellOptions>> = [ { value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' }, { value: { type: TableCellDisplayMode.Image }, label: 'Image' }, { value: { type: TableCellDisplayMode.Actions }, label: 'Actions' }, + { value: { type: TableCellDisplayMode.Pill }, label: 'Pill' }, ]; const getStyles = (theme: GrafanaTheme2) => ({ diff --git a/public/app/plugins/panel/table/table-new/TablePanel.tsx b/public/app/plugins/panel/table/table-new/TablePanel.tsx index 903a74f65c3..5af5472f19b 100644 --- a/public/app/plugins/panel/table/table-new/TablePanel.tsx +++ b/public/app/plugins/panel/table/table-new/TablePanel.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { ActionModel, @@ -26,7 +26,7 @@ import { Options } from './panelcfg.gen'; interface Props extends PanelProps<Options> {} export function TablePanel(props: Props) { - const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables } = props; + const { data, height, width, options, fieldConfig, id, timeRange, replaceVariables, transparent } = props; useMemo(() => { cacheFieldDisplayNames(data.series); @@ -34,6 +34,10 @@ export function TablePanel(props: Props) { const theme = useTheme2(); const panelContext = usePanelContext(); + const _getActions = useCallback( + (frame: DataFrame, field: Field, rowIndex: number) => getCellActions(frame, field, rowIndex, replaceVariables), + [replaceVariables] + ); const frames = hasDeprecatedParentRowIndex(data.series) ? migrateFromParentRowIndexToNestedFrames(data.series) : data.series; @@ -75,9 +79,9 @@ export function TablePanel(props: Props) { timeRange={timeRange} enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair} fieldConfig={fieldConfig} - getActions={getCellActions} - replaceVariables={replaceVariables} + getActions={_getActions} structureRev={data.structureRev} + transparent={transparent} /> ); @@ -159,28 +163,39 @@ const getCellActions = ( field: Field, rowIndex: number, replaceVariables: InterpolateFunction | undefined -) => { - const actions: Array<ActionModel<Field>> = []; - const actionLookup = new Set<string>(); +): Array<ActionModel<Field>> => { + const numActions = field.config.actions?.length ?? 0; - const actionsModel = getActions( - dataFrame, - field, - field.state!.scopedVars!, - replaceVariables ?? replaceVars, - field.config.actions ?? [], - { valueRowIndex: rowIndex } - ); + if (numActions > 0) { + const actions = getActions( + dataFrame, + field, + field.state!.scopedVars!, + replaceVariables ?? replaceVars, + field.config.actions ?? [], + { valueRowIndex: rowIndex } + ); - actionsModel.forEach((action) => { - const key = `${action.title}`; - if (!actionLookup.has(key)) { - actions.push(action); - actionLookup.add(key); + if (actions.length === 1) { + return actions; + } else { + const actionsOut: Array<ActionModel<Field>> = []; + const actionLookup = new Set<string>(); + + actions.forEach((action) => { + const key = action.title; + + if (!actionLookup.has(key)) { + actionsOut.push(action); + actionLookup.add(key); + } + }); + + return actionsOut; } - }); + } - return actions; + return []; }; const tableStyles = { diff --git a/public/app/plugins/panel/table/table-new/module.tsx b/public/app/plugins/panel/table/table-new/module.tsx index 06d61bc11f9..a40d4c0b047 100644 --- a/public/app/plugins/panel/table/table-new/module.tsx +++ b/public/app/plugins/panel/table/table-new/module.tsx @@ -102,6 +102,13 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel) description: t('table-new.description-column-filter', 'Enables/disables field filters in table'), defaultValue: defaultTableFieldOptions.filterable, }) + .addBooleanSwitch({ + path: 'wrapHeaderText', + name: t('table.name-wrap-header-text', 'Wrap header text'), + description: t('table.description-wrap-header-text', 'Enables text wrapping for column headers'), + category, + defaultValue: defaultTableFieldOptions.wrapHeaderText, + }) .addBooleanSwitch({ path: 'hidden', name: t('table-new.name-hide-in-table', 'Hide in table'), diff --git a/public/app/plugins/panel/traces/TracesPanel.tsx b/public/app/plugins/panel/traces/TracesPanel.tsx index 5ad8aebecd8..7a9c9394827 100644 --- a/public/app/plugins/panel/traces/TracesPanel.tsx +++ b/public/app/plugins/panel/traces/TracesPanel.tsx @@ -6,7 +6,7 @@ import { Field, LinkModel, PanelProps } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { getDataSourceSrv } from '@grafana/runtime'; import { TraceView } from 'app/features/explore/TraceView/TraceView'; -import { SpanLinkFunc } from 'app/features/explore/TraceView/components'; +import { SpanLinkFunc } from 'app/features/explore/TraceView/components/types/links'; import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform'; import { replaceSearchVariables, SearchProps } from '../../../features/explore/TraceView/useSearch'; diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index ff54529bbe4..d5a7cff51d3 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -15,7 +15,7 @@ import { TooltipHoverMode } from '@grafana/ui/internal'; import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; -import { findFieldIndex } from 'app/features/dimensions'; +import { findFieldIndex } from 'app/features/dimensions/utils'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; import { prepareGraphableFields } from '../timeseries/utils'; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index e1baf824794..41221a4e772 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -17,7 +17,8 @@ import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; import { getAppPluginRoutes } from 'app/features/plugins/routes'; import { getProfileRoutes } from 'app/features/profile/routes'; -import { AccessControlAction, DashboardRoutes } from 'app/types'; +import { AccessControlAction } from 'app/types/accessControl'; +import { DashboardRoutes } from 'app/types/dashboard'; import { SafeDynamicImport } from '../core/components/DynamicImports/SafeDynamicImport'; import { RouteDescriptor } from '../core/navigation/types'; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index c00680f5aae..db0c15c8f7f 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -2,7 +2,7 @@ import { configureStore as reduxConfigureStore, createListenerMiddleware } from import { setupListeners } from '@reduxjs/toolkit/query'; import { Middleware } from 'redux'; -import { alertingAPIv0alpha1 } from '@grafana/alerting/unstable'; +import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import { cloudMigrationAPI } from 'app/features/migrate-to-cloud/api'; @@ -43,7 +43,7 @@ export function configureStore(initialState?: Partial<StoreState>) { getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat( listenerMiddleware.middleware, alertingApi.middleware, - alertingAPIv0alpha1.middleware, + alertingPackageAPI.middleware, publicDashboardApi.middleware, browseDashboardsAPI.middleware, cloudMigrationAPI.middleware, diff --git a/public/app/store/store.ts b/public/app/store/store.ts index 496a3e4b081..557ec7566f3 100644 --- a/public/app/store/store.ts +++ b/public/app/store/store.ts @@ -1,7 +1,7 @@ import { Store } from 'redux'; import { initialKeyedVariablesState } from 'app/features/variables/state/keyedVariablesReducer'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; export let store: Store<StoreState>; diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index 684a3a158b1..a3662165c92 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -1,12 +1,8 @@ -import { OrgRole } from '@grafana/data'; - export enum TeamPermissionLevel { Admin = 4, Member = 0, } -export { OrgRole as OrgRole }; - export type PermissionLevel = 'view' | 'edit' | 'admin'; /** @deprecated Use PermissionLevel instead */ diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts index 37778e66a7a..e732ecc06cd 100644 --- a/public/app/types/apiKeys.ts +++ b/public/app/types/apiKeys.ts @@ -1,6 +1,4 @@ -import { WithAccessControlMetadata } from '@grafana/data'; - -import { OrgRole } from './acl'; +import { OrgRole, WithAccessControlMetadata } from '@grafana/data'; export interface ApiKey extends WithAccessControlMetadata { id?: number; diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 4ec85c07e64..d0a3d7a9873 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -194,6 +194,10 @@ export class OpenExtensionSidebarEvent extends BusEventWithPayload<OpenExtension static type = 'open-extension-sidebar'; } +export class CloseExtensionSidebarEvent extends BusEventBase { + static type = 'close-extension-sidebar'; +} + /** * @deprecated use ShowModalReactEvent instead that has this capability built in */ diff --git a/public/app/types/index.ts b/public/app/types/index.ts deleted file mode 100644 index 546c2ab628d..00000000000 --- a/public/app/types/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * from './teams'; -export * from './alerting'; -export * from './location'; -export * from './folders'; -export * from './dashboard'; -export * from './acl'; -export * from './apiKeys'; -export * from './user'; -export * from './serviceaccount'; -export * from './datasources'; -export * from './plugins'; -export * from './organization'; -export * from './appNotifications'; -export * from './explore'; -export * from './store'; -export * from './ldap'; -export * from './query'; -export * from './accessControl'; -export * from './supportBundles'; -export * from './settings'; - -import * as CoreEvents from './events'; -export { CoreEvents }; diff --git a/public/app/types/organization.ts b/public/app/types/organization.ts index 926bced7bd5..367c7fcd55c 100644 --- a/public/app/types/organization.ts +++ b/public/app/types/organization.ts @@ -1,4 +1,4 @@ -import { UserOrg } from 'app/types'; +import { UserOrg } from 'app/types/user'; export interface Organization { name: string; diff --git a/public/app/types/serviceaccount.ts b/public/app/types/serviceaccount.ts index 85c2fe1c0de..2d9a56e95b7 100644 --- a/public/app/types/serviceaccount.ts +++ b/public/app/types/serviceaccount.ts @@ -1,6 +1,7 @@ -import { WithAccessControlMetadata } from '@grafana/data'; +import { OrgRole, WithAccessControlMetadata } from '@grafana/data'; -import { ApiKey, OrgRole, Role } from '.'; +import { Role } from './accessControl'; +import { ApiKey } from './apiKeys'; export interface OrgServiceAccount extends WithAccessControlMetadata { serviceAccountId: number; diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 0978062b808..c6440d0bac1 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -132,6 +132,7 @@ interface GrafanaPromRuleDTOBase extends PromRuleDTOBase { folderUid: string; isPaused: boolean; queriedDatasourceUIDs?: string[]; + provenance?: string; } export interface PromAlertingRuleDTO extends PromRuleDTOBase { diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 67264b0f298..e5cd1805e7f 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,7 +1,6 @@ -import { SelectableValue, WithAccessControlMetadata } from '@grafana/data'; -import { Role } from 'app/types'; +import { OrgRole, SelectableValue, WithAccessControlMetadata } from '@grafana/data'; -import { OrgRole } from '.'; +import { Role } from './accessControl'; export interface OrgUser extends WithAccessControlMetadata { avatarUrl: string; diff --git a/public/img/irm_logo.svg b/public/img/irm_logo.svg new file mode 100644 index 00000000000..7c25966a158 --- /dev/null +++ b/public/img/irm_logo.svg @@ -0,0 +1,45 @@ +<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="47" height="47" rx="4" fill="#CCCCDC" fill-opacity="0.14"/> +<path d="M23.408 9.27604C24.3127 9.27604 25.0461 8.54267 25.0461 7.63802C25.0461 6.73337 24.3127 6 23.408 6C22.5034 6 21.77 6.73337 21.77 7.63802C21.77 8.54267 22.5034 9.27604 23.408 9.27604Z" fill="url(#paint0_linear_191_6486)"/> +<path d="M24.4494 17.572C24.273 17.572 24.1292 17.7159 24.1292 17.8923V25.2967C24.1292 25.4731 24.273 25.6169 24.4494 25.6169H26.6086C26.7728 25.6169 26.9112 25.4921 26.9275 25.3279L27.6698 17.9235C27.6888 17.7349 27.5409 17.5707 27.3509 17.5707H24.4481L24.4494 17.572Z" fill="url(#paint1_linear_191_6486)"/> +<path d="M19.6519 17.5721C19.4619 17.5721 19.3139 17.7363 19.3329 17.9249L20.0698 25.3293C20.0861 25.4935 20.2246 25.6183 20.3888 25.6183H22.5262C22.7026 25.6183 22.8465 25.4745 22.8465 25.298V17.8937C22.8465 17.7173 22.7026 17.5734 22.5262 17.5734H19.6519V17.5721Z" fill="url(#paint2_linear_191_6486)"/> +<path d="M31.6775 17.5721H29.2836C29.1194 17.5721 28.981 17.6969 28.9647 17.8611L28.2223 25.2655C28.2033 25.4541 28.3513 25.6183 28.5413 25.6183H30.2037C30.3571 25.6183 30.4887 25.5097 30.5186 25.3605L31.9937 17.9561C32.0331 17.758 31.8811 17.5734 31.6789 17.5734L31.6775 17.5721Z" fill="url(#paint3_linear_191_6486)"/> +<path d="M18.7749 25.2655L18.038 17.8611C18.0217 17.6969 17.8833 17.5721 17.7191 17.5721H15.3211C15.1189 17.5721 14.9669 17.7566 15.0062 17.9548L16.4814 25.3591C16.5113 25.5097 16.6429 25.617 16.7962 25.617H18.456C18.646 25.617 18.7939 25.4527 18.7749 25.2641V25.2655Z" fill="url(#paint4_linear_191_6486)"/> +<path d="M17.1547 12.4449H29.6631C29.9427 12.4449 30.1897 12.263 30.2738 11.9957C30.4027 11.5831 30.0947 11.1651 29.6631 11.1651H23.8887V9.86366C23.7339 9.89759 23.5738 9.91523 23.4082 9.91523C23.2427 9.91523 23.0825 9.89623 22.9278 9.86366V11.1651H17.1547C16.7232 11.1651 16.4151 11.5831 16.544 11.9957C16.6282 12.263 16.8752 12.4449 17.1547 12.4449Z" fill="url(#paint5_linear_191_6486)"/> +<path d="M29.0008 16.0683L29.602 14.1426C29.6671 13.9363 29.5124 13.7259 29.2966 13.7259H17.5211C17.3053 13.7259 17.1506 13.9363 17.2157 14.1426L17.8169 16.0683C17.859 16.2026 17.9825 16.2936 18.1223 16.2936H28.6941C28.8339 16.2936 28.9587 16.2026 28.9994 16.0683H29.0008Z" fill="url(#paint6_linear_191_6486)"/> +<path d="M28.2891 26.8992H18.5275C18.1733 26.8992 17.887 27.1855 17.887 27.5397V38.136C17.887 39.1986 18.7474 40.059 19.81 40.059H27.0067C28.0693 40.059 28.9297 39.1986 28.9297 38.136V27.5397C28.9297 27.1855 28.6433 26.8992 28.2891 26.8992Z" fill="url(#paint7_linear_191_6486)"/> +<defs> +<linearGradient id="paint0_linear_191_6486" x1="23.408" y1="6.85314" x2="23.408" y2="9.27604" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint1_linear_191_6486" x1="25.9003" y1="19.6661" x2="25.9003" y2="25.6169" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint2_linear_191_6486" x1="21.0889" y1="19.6674" x2="21.0889" y2="25.6183" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint3_linear_191_6486" x1="30.1103" y1="19.6674" x2="30.1103" y2="25.6183" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint4_linear_191_6486" x1="16.8883" y1="19.6671" x2="16.8883" y2="25.617" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint5_linear_191_6486" x1="23.4089" y1="10.5359" x2="23.4089" y2="12.4449" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint6_linear_191_6486" x1="23.4089" y1="14.3946" x2="23.4089" y2="16.2936" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +<linearGradient id="paint7_linear_191_6486" x1="23.4083" y1="30.3262" x2="23.4083" y2="40.059" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F55F3E"/> +<stop offset="1" stop-color="#FF8833"/> +</linearGradient> +</defs> +</svg> diff --git a/public/img/plugins/logicmonitor.svg b/public/img/plugins/logicmonitor.svg index f533e96b843..20855d2bb3b 100644 --- a/public/img/plugins/logicmonitor.svg +++ b/public/img/plugins/logicmonitor.svg @@ -1 +1,5 @@ -<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#fff;}.cls-4{fill:#0076ff;}</style><clipPath id="clip-path"><rect class="cls-1" x="-5" y="29.5" width="1511" height="408"></rect></clipPath></defs><title>svg \ No newline at end of file + + + + + diff --git a/public/img/synthetic_monitoring_logo.svg b/public/img/synthetic_monitoring_logo.svg new file mode 100644 index 00000000000..3b5b9f49deb --- /dev/null +++ b/public/img/synthetic_monitoring_logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 6a1e7dbaab0..3c9c70f8bd5 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Některé funkce jsou stabilní (GA) a povolené ve výchozím nastavení, zatímco některé funkce jsou v současné době ve fázi předběžné beta verze, připravené k implementaci.", "confirm-modal-body-2": "Před provedením úprav doporučujeme porozumět důsledkům každé změny funkce.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Obecná dostupnost", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Odstranit organizaci", + "confirmText-delete": "", "title-delete": "Odstranit" }, "anon-users": { "not-found": "Nebyli nalezeni žádní anonymní uživatelé." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Vynutit odhlášení ze všech zařízení" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Nemáte oprávnění k prohlížení uživatelů v této organizaci. Chcete-li aktualizovat tuto organizaci, obraťte se na správce serveru.", "heading": "Upravit organizaci", @@ -208,9 +216,11 @@ "not-editable": "Tuto uživatelskou roli nelze upravovat, protože je synchronizována s vaším poskytovatelem ověření. Podrobnosti najdete v <1>dokumentaci k ověření Grafana." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Role" }, + "confirmText-delete": "", "delete-aria-label": "Odstranit uživatele: {{name}}", "title-delete": "Odstranit" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Tato systémová nastavení jsou definována v grafana.ini nebo custom.ini (nebo přepsána v proměnných ENV). Chcete-li je změnit, musíte restartovat Grafanu." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licence Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Změnit", + "confirmText-change": "", "grafana-admin-key": "Správce Grafana", "grafana-admin-no": "Ne", "grafana-admin-yes": "Ano", "title": "Oprávnění" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Odstranit uživatele", "disable-button": "Deaktivovat uživatele", "edit-button": "Upravit", @@ -312,6 +330,9 @@ "title-delete-user": "Odstranit uživatele", "title-disable-user": "Zakázat uživatele" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Prohlížeč a operační systém", "force-logout-all-button": "Vynutit odhlášení ze všech zařízení", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Aktivní časování" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Přidat další" }, @@ -465,6 +490,9 @@ "label-muting-grouping-and-timings-optional": "Ztlumení, seskupení a časování (nepovinné)", "title-muting-grouping-and-timings": "Ztlumení, seskupení a časování" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Kopírovat odkaz", "duplicate": "Duplikovat", @@ -488,6 +516,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Upravit YAML", @@ -554,6 +585,7 @@ "view-configuration": "Zobrazit konfiguraci" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Interní konfiguraci správce výstrah Grafana nelze ručně změnit. Chcete-li změnit tuto konfiguraci, upravte jednotlivé zdroje přes uživatelské rozhraní.", "gma-manual-configuration-is-not-supported": "Ruční změny konfigurace nejsou podporovány", "message": { @@ -568,11 +600,13 @@ "title-resetting-alertmanager-configuration": "Probíhá obnovení konfigurace správce výstrah" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Porovnat", "restore": "Obnovit", "text-latest": "Nejnovější" }, + "confirmText-yes-restore-configuration": "", "loading": "Načítání…", "no-previous-configurations": "Žádné předchozí konfigurace", "this-might-take-a-while": "Může to chvíli trvat…", @@ -852,8 +886,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Další akce pro kontaktní bod „{{contactPointName}}“", + "ariaLabel-delete": "", "button-edit": "Upravit", "button-view": "Zobrazit", + "export-ariaLabel-export": "", "export-label-export": "Exportovat", "label-delete": "Odstranit", "label-manage-permissions": "Spravovat oprávnění", @@ -1392,6 +1428,7 @@ "label-disable-resolved-message": "Zakázat vyřešenou zprávu" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Musí být kladné celé číslo.", "must-enter-a-group-name": "Musíte zadat název skupiny" @@ -1483,15 +1520,12 @@ "group-details": { "ds-features-error": "Chyba při načítání podrobností o zdroji dat", "edit": "Upravit", - "evaluations-to-fire": "Cykly hodnocení ke spuštění", "export": "Exportovat", "folder": "Složka", "group-loading-error": "Chyba při načítání skupiny", "interval": "Interval", "namespace": "Jmenný prostor", - "pending-period": "Období čekání", - "recording": "Probíhá záznam", - "rule-name": "Jméno pravidla" + "new": "" }, "group-edit": { "ds-error": "Chyba při načítání podrobností o zdroji dat", @@ -1537,8 +1571,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Začínáme", "label-insights": "观察", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Výstrahy" }, "import-to-gma": { @@ -1840,7 +1886,11 @@ "other-data-sources": "Ostatní zdroje dat" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Zakázáno", @@ -2093,9 +2143,11 @@ "update-errors": { "conflict": "Strom zásad oznamování byl aktualizován jiným uživatelem.", "error-code": "Chybová zpráva: „{{error}}“", - "fallback": "Při aktualizaci vašich zásad oznamování se něco pokazilo.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Aktualizujte stránku a zkuste to znovu.", - "title": "Chyba při ukládání zásady oznamování" + "title": "" }, "n-more-policies_one": "{{count}} další zásady", "n-more-policies_few": "{{count}} další zásady", @@ -2153,6 +2205,7 @@ "query-and-expressions-step": { "add-query": "Přidat dotaz", "body-queries-expressions-configured": "Vytvořte alespoň jeden dotaz nebo výraz, na který chcete být upozorněni", + "confirmText-deactivate": "", "expressions": "Výrazy", "loading-data-sources": "Načítání zdrojů dat…", "manipulate-returned-queries-other-operations": "Spravujte data získaná z dotazů pomocí matematických a dalších operací.", @@ -2202,6 +2255,9 @@ "label-export-all": "Exportovat vše" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Nelze načíst editor dotazů, důvod: {{errorMessage}}" }, @@ -2220,6 +2276,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Pro zkopírované pravidlo budete muset nastavit novou hodnotící skupinu, protože původní skupina byla zajištěna a nelze ji použít pro pravidla vytvořená v uživatelském rozhraní.", "body-not-provisioned": "Nové pravidlo <1>nebude označeno jako zajištěné pravidlo.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Kopírovat zajištěné pravidlo výstrahy" }, "redirect-to-rule-viewer": { @@ -2419,8 +2476,6 @@ "title-inspect-alert-rule": "Zkontrolovat pravidlo výstrahy" }, "rule-list": { - "cannot-find-rule-details-for": "Nelze najít podrobnosti o pravidle pro UID {{uid}}", - "cannot-load-rule-details-for": "Nelze načíst podrobnosti o pravidle pro UID {{uid}}", "configure-datasource": "Konfigurovat", "draft-new-rule": "Navrhnout nové pravidlo", "ds-error": { @@ -2624,6 +2679,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Nenašli jste požadovaný zdroj dat? Některé zdroje dat nepodporují upozorňování. Další informace zobrazíte kliknutím na ikonu." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Přidat nového správce výstrah", "builtin-alertmanager": "Vestavěný správce výstrah", @@ -2776,6 +2836,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Vyberte šablonu oznámení", "loading": "Načítání…", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Vyberte šablonu oznámení" } @@ -2802,6 +2865,8 @@ }, "templates-table": { "actions": "Akce", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Nejsou definovány žádné šablony.", "template-group": "Skupina šablony", "title-delete-template-group": "Odstranit skupinu šablon" @@ -2854,6 +2919,11 @@ "timestamp": { "time-ago": "(před {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Cílová složka není prázdná, některá pravidla mohou být přepsána nebo odstraněna. Opravdu chcete importovat tato pravidla výstrah do pravidel spravovaných Grafanou?", @@ -2929,6 +2999,11 @@ "title-delete-contact-point": "Odstranit kontaktní bod" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Odstranit zásadu oznamování" @@ -3085,7 +3160,8 @@ "annotation-field-mapper": { "annotation": "Anotace", "first-value": "První hodnota", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Přidat dotaz na vysvětlivky", @@ -3093,6 +3169,16 @@ "info-box-content-2": "Další informace najdete v <2> dokumentaci k vysvětlivkám.", "title": "Nejsou k dispozici žádné vlastní dotazy s vysvětlivkami" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "Počet událostí: {{numEvents}} (z {{numFields}} polí)", "no-events-found": "Nebyly nalezeny žádné události", @@ -3168,6 +3254,9 @@ "define-allowed-teams-ids-label": "Definovat povolená ID týmů", "display-name-description": "Zobrazí se na přihlašovací stránce jako „Přihlásit se přes…“. Užitečné, pokud používáte více než jednoho poskytovatele identity nebo protokoly SSO.", "display-name-label": "Zobrazované jméno", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Název klíče, který se použije pro vyhledání e-mailu uživatele v mapě atributů ID tokenu OAuth2.", "email-attribute-name-label": "Název atributu e-mailu", "email-attribute-path-description": "Výraz JMESPath, který se použije pro vyhledání e-mailu uživatele z informací o uživateli.", @@ -3219,7 +3308,7 @@ "team-ids-github": "Celočíselný seznam ID týmů.", "team-ids-label": "ID týmů", "team-ids-numbers": "ID týmů musí být čísla.", - "team-ids-other": "Řetězcový seznam ID týmu.", + "team-ids-other": "", "team-ids-placeholder": "Zadejte ID týmů a stiskněte klávesu Enter pro přidání", "teams-url-description": "Adresa URL použitá k dotazu na ID týmů. Pokud není nastavena, výchozí hodnota je /teams.", "teams-url-description-oauth": "Pokud nakonfigurujete „{{ teamsURLLabel }}“, musíte také nakonfigurovat „{{ teamIDsAttributePathLabel }}“.", @@ -3263,6 +3352,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Obnovit výchozí hodnoty" }, + "confirmText-reset": "", "disable": "Zakázat", "disabling": "Probíhá zakazování…", "discard": "Zahodit", @@ -3509,11 +3599,11 @@ "button-create": "Vytvořit", "button-creating": "Vytváření…", "cancel": "Zrušit", - "error-invalid-folder-name": "Neplatný název složky", "error-required": "Název složky je povinný", "folder-name-input-placeholder-enter-folder-name": "Zadejte název složky", "label-folder-name": "Název složky", "text-pull-request-created": "Byl vytvořen pull request se změnami v této složce:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull request byl vytvořen", "title-this-repository-is-read-only": "Toto úložiště je jen pro čtení" }, @@ -3931,6 +4021,7 @@ "tooltip": "Zavřít" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Při načítání možností došlo k chybě." @@ -4199,8 +4290,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Dynamicky vypočítá interval vydělením časového rozsahu zadaným počtem" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4362,12 +4453,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Přejít na externí stránku?" } }, "render-left-actions": { - "text-public": "Veřejná", "tooltip-view-as-scene": "Zobrazit jako scénu" } }, @@ -4560,6 +4653,14 @@ "errors": { "failed-to-load": "Nástěnku se nepodařilo načíst" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Nepodařilo se načíst nástěnku" @@ -4576,6 +4677,13 @@ "editable": "Upravitelné", "readonly": "Jen pro čtení" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4625,6 +4733,18 @@ "transform-data": "Transformovat data" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4882,6 +5002,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Opravdu chcete obnovit nástěnku na verzi {{version}}? Veškeré neuložené změny budou ztraceny.", + "confirmText-restore-version": "", "title-restore-version": "Obnovit verzi" }, "row-options-button": { @@ -4932,6 +5053,9 @@ "title-not-unique": "Tento název není jedinečný" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Uložit jako" }, @@ -4966,6 +5090,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Ve vybrané složce už existuje nástěnka se stejným názvem.<1><2>Chcete přesto tuto nástěnku uložit?", "body-version-mismatch": "Tuto nástěnku aktualizoval jiný uživatel<1><2>Chcete přesto tuto nástěnku uložit?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Konflikt", "title-version-mismatch": "Konflikt" }, @@ -5131,7 +5256,6 @@ "playlist-next": "Přejít na další nástěnku", "playlist-previous": "Přejít na předchozí nástěnku", "playlist-stop": "Zastavit playlist", - "public-dashboard": "Veřejná", "refresh": "Obnovit nástěnku", "save": "Uložit nástěnku", "save-dashboard": { @@ -5162,7 +5286,9 @@ "label-apply-transformation-to": "Použít transformaci na" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Ladění", "title-disable-transformation": "Zakázat transformaci", "title-filter": "Filtrovat", @@ -5182,12 +5308,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Hledat transformaci", "show-images": "Zobrazit obrázky", - "title-add-another-transformation": "Přidejte další transformaci" + "title-add-another-transformation": "Přidejte další transformaci", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Přidejte další transformaci" }, + "confirmText-delete-all": "", "delete-all-transformations": "Odstranit všechny transformace", "title-delete-all-transformations": "Odstranit všechny transformace?", "tooltip-clear-search": "Vymazat vyhledávání", @@ -5224,6 +5355,7 @@ "version-history-table": { "aria-label-toggle-selection": "Přepnout výběr verze {{version}}", "date": "Datum", + "name-latest": "", "notes": "Poznámky", "restore": "Obnovit", "updated-by": "Aktualizoval/a", @@ -5300,7 +5432,8 @@ "description-enables-users-custom-values": "Umožňuje uživatelům přidávat vlastní hodnoty do seznamu", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Zadejte rozměry ve formátu CSV: {{name}}, {{value}}", "label-data-source": "Zdroj dat", - "label-use-static-key-dimensions": "Použít dimenze statického klíče" + "label-use-static-key-dimensions": "Použít dimenze statického klíče", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5373,6 +5506,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Odvolat veřejnou adresu URL" } @@ -5384,6 +5520,7 @@ }, "custom-variable-form": { "custom-options": "Vlastní možnosti", + "name-values-separated-comma": "", "selection-options": "Možnosti výběru" }, "dashboard-edit-pane-renderer": { @@ -5402,6 +5539,12 @@ "label-type": "Typ", "label-url": "URL", "label-with-tags": "S tagy", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Otevřít nástěnku" }, "dashboard-link-list": { @@ -5414,15 +5557,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Hodnota ještě není uložena v databázi Grafany", "not-yet-saved": "Hodnota není uložena v databázi Grafany", - "open-pull-request-in-git-hub": "Otevřít pull request v GitHubu", - "title-dashboard-loaded-branch-git-hub": "Tato nástěnka je načtena z větve na GitHubu.", "title-dashboard-loaded-external-repository": "Tato nástěnka je načtena z externího úložiště", - "title-dashboard-loaded-request-git-hub": "Tato nástěnka je načtena z pull request na GitHubu.", - "title-error-loading-dashboard": "Chyba při načítání nástěnky", - "value-not-saved": "Hodnota ještě není uložena v databázi Grafany", - "view-pull-request-in-git-hub": "Zobrazit pull request v GitHubu" + "title-error-loading-dashboard": "Chyba při načítání nástěnky" }, "dashboard-scene": { "text": { @@ -5448,6 +5585,8 @@ "data-source-options": "Možnosti zdroje dat", "description-instance-name-filter": "Filtr regulárních výrazů, pro které můžete vybrat instance zdroje dat v seznamu hodnot proměnných. Ponechte prázdné pro vše.", "example-instance-name-filter": "Příklad: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Možnosti výběru" }, "default-grid-layout-manager": { @@ -5493,12 +5632,61 @@ "empty-transformations-message": { "add-transformation": "Přidat transformaci" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Možnosti sloupce", "row-height-options": "Možnosti výšky řádku" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5523,7 +5711,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Zadejte rozměry ve formátu CSV: {{name}}, {{value}}", "group-by-options": "Možnosti Seřadit podle", "label-data-source": "Zdroj dat", - "label-use-static-group-by-dimensions": "Použít rozměry statické skupiny" + "label-use-static-group-by-dimensions": "Použít rozměry statické skupiny", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Kopírovat do schránky", @@ -5559,9 +5748,14 @@ "apply": "Použít" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Vypočtená hodnota neklesne pod tuto prahovou hodnotu", "description-step-count": "Kolikrát by měl být aktuální časový rozsah rozdělen pro výpočet hodnoty", - "interval-options": "Možnosti intervalu" + "interval-options": "Možnosti intervalu", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5583,11 +5777,28 @@ "usage-count_many": "Použito na {{count}} nástěnkách", "usage-count_other": "Použito na {{count}} nástěnkách" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Ve vybrané složce už existuje nástěnka se stejným názvem. Chcete přesto tuto nástěnku uložit?", "title-name-already-exists": "Název již existuje" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Přejít na externí stránku?" } @@ -5623,6 +5834,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Přidejte další transformaci", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Odstranit všechny transformace", "title-delete-all-transformations": "Odstranit všechny transformace?" }, @@ -5675,9 +5888,13 @@ "description-examples": "Pojmenované skupiny zachycení lze použít k oddělení zobrazovaného textu a hodnoty (<1>viz příklady).", "description-optional": "Volitelné, pokud chcete extrahovat část názvu řady nebo segmentu uzlu metriky.", "label-data-source": "Zdroj dat", + "label-static-options-sort": "", "label-target-data-source": "Cílový zdroj dat", + "label-use-static-options": "", + "name-regex": "", "query-options": "Možnosti dotazu", - "selection-options": "Možnosti výběru" + "selection-options": "Možnosti výběru", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5690,6 +5907,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Opravdu chcete obnovit nástěnku na verzi {{version}}? Veškeré neuložené změny budou ztraceny.", + "confirmText-restore-version": "", "title-restore-version": "Obnovit verzi" }, "save-button": { @@ -5707,7 +5925,8 @@ "label-folder": "Složka", "render-footer": { "title-failed-to-save-dashboard": "Nástěnku se nepodařilo uložit" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5785,7 +6004,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Umožňuje vybrat více hodnot současně", "description-enables-option-include-variables": "Povoluje možnost zahrnout všechny hodnoty", - "description-enables-users-custom-values": "Umožňuje uživatelům přidávat vlastní hodnoty do seznamu" + "description-enables-users-custom-values": "Umožňuje uživatelům přidávat vlastní hodnoty do seznamu", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Přepnout nabídku sdílení" @@ -5805,6 +6028,9 @@ "copy-to-clipboard-failed": "Kopírování do schránky se nezdařilo" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(volitelné)", "text-options": "Možnosti textu" @@ -5828,6 +6054,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Opravdu chcete tento panel odpojit?" }, "unsaved-changes-modal": { @@ -5844,6 +6072,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Na tuto proměnnou neodkazuje žádná proměnná ani nástěnka.", "aria-label-variable-referenced-other-variables-dashboard": "Na tuto proměnnou odkazují jiné proměnné nebo nástěnka.", @@ -5853,10 +6084,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulář editoru proměnné", "back-to-list": "Zpět na seznam", + "confirmText": { + "delete-variable": "" + }, "delete": "Odstranit", "description-optional-display-name": "Nepovinné zobrazované jméno", "description-template-variable-characters": "Název proměnné šablony. (max. 50 znaků)", "general": "Obecné", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Popisný text", "placeholder-label-name": "Název štítku", "placeholder-variable-name": "Název proměnné", @@ -5871,13 +6108,25 @@ "variable": "Proměnná" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Odstranit proměnnou", "tooltip-duplicate-variable": "Duplikovat proměnnou", "tooltip-remove-variable": "Odebrat proměnnou" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Skrýt" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Zobrazuje se použití pro: {{variableId}}", "tooltip-show-usages": "Zobrazit použití" @@ -5904,6 +6153,7 @@ "version-history-table": { "aria-label-toggle-selection": "Přepnout výběr verze {{version}}", "date": "Datum", + "name-latest": "", "notes": "Poznámky", "restore": "Obnovit", "updated-by": "Aktualizoval/a", @@ -6291,7 +6541,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Nahrát" @@ -6329,6 +6580,7 @@ }, "label-limit": "Limit", "label-value": "Hodnota", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6337,9 +6589,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max.", "label-min": "Min.", - "label-value": "Hodnota" + "label-value": "Hodnota", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6911,6 +7169,8 @@ "aria-label-select-service-name-operator": "Vyberte operátor názvu služby", "aria-label-select-span-name": "Vyberte název rozsahu", "aria-label-select-span-name-operator": "Vyberte operátor názvu rozsahu", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtry rozsahu", "label-duration": "Doba trvání", "label-service-name": "Název služby", @@ -6981,6 +7241,8 @@ "split-widen": "Rozšířit podokno" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Poskytnout zpětnou vazbu", "label-copied": "Zkopírováno!", "label-export": "Exportovat", @@ -7088,6 +7350,7 @@ "when": "KDY" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Činnost" }, "math": { @@ -7118,6 +7381,7 @@ }, "folder-filter": { "clear-folder-button": "Vymazat složky", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtr složky", "select-placeholder": "Filtrovat podle složky" }, @@ -7186,7 +7450,53 @@ "incomplete-request-error": "Je mi líto, ale vaši žádost se mi nepodařilo dokončit. Zkuste to znovu.", "send-custom-feedback": "Odeslat" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Zeměpisná šířka", "label-longitude": "Zeměpisná délka" @@ -7195,6 +7505,14 @@ "center": "Střed:", "zoom": "Přiblížení:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Vrstva" @@ -7217,6 +7535,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Přidat pravidlo stylu geomapy" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Přidat vrstvu", "no-layers": "Žádné vrstvy?" @@ -7227,16 +7553,38 @@ "label-zoom": "Přiblížení", "use-current-map-settings": "Použít aktuální nastavení mapy" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Symbol" }, "measure-overlay": { "tooltip-show-measure-tools": "Zobrazit nástroje pro měření" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Vrstvu základní mapy konfiguruje správce serveru." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Zarovnat", "label-baseline": "Základní linie", "label-color": "Barva", @@ -7250,7 +7598,14 @@ "label-symbol-vertical-align": "Svislé zarovnání symbolu", "label-text-label": "Štítek textu", "label-x-offset": "Posun X", - "label-y-offset": "Posun Y" + "label-y-offset": "Posun Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operátor porovnání", @@ -7261,6 +7616,15 @@ "placeholder-feature-property": "Oblíbená vlastnost", "placeholder-numeric-value": "Číselná hodnota", "placeholder-value": "hodnota" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7510,7 +7874,8 @@ "aria-label-selected-color": "{{colorLabel}} barva" }, "confirm-button": { - "cancel": "Zrušit" + "cancel": "Zrušit", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Napište „{{confirmPromptText}}“ pro potvrzení" @@ -7692,6 +8057,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "přepnout sbalení panelu", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Zrušit dotaz", "tooltip-cancel-loading": "Zrušit dotaz", "tooltip-stop-streaming": "Zastavit streamování", @@ -7758,6 +8125,9 @@ "filter-popup-match-case": "Rozlišovat malá a velká písmena", "inspect-drawer-title": "Zkontrolovat hodnotu", "inspect-menu-label": "Zkontrolovat hodnotu", + "nested-table": { + "no-data": "" + }, "no-values-label": "Žádné hodnoty", "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} z {{numRows}} řádků" }, @@ -7766,6 +8136,7 @@ }, "tags-input": { "add": "Přidat", + "placeholder-new-tag": "", "remove": "Odebrat tag: {{name}}" }, "time-sync-button": { @@ -7859,6 +8230,12 @@ "footer-click-to-action": "Klikněte pro {{actionTitle}}", "footer-click-to-navigate": "Klikněte pro otevření {{linkTitle}}", "timestamp": "Časová známka" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8241,6 +8618,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Vytvořit panel knihovny" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Chcete tento panel odstranit?" }, @@ -8266,7 +8647,7 @@ "usage-count_other": "Použito na {{count}} nástěnkách" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Hledat podle názvu nebo popisu" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Načítání panelu knihovny…" @@ -8320,9 +8701,7 @@ "share-panel": { "config-description": "Vytvořte personalizovaný přímý odkaz pro sdílení panelu v rámci vaší organizace s následujícími možnostmi přizpůsobení:", "download-image": "Stáhnout obrázek", - "render-image": "Generovat obrázek", - "render-image-error": "Obrázek panelu se nepodařilo vykreslit", - "render-image-error-description": "Při generování obrázku došlo k chybě" + "render-image": "Generovat obrázek" } }, "live": { @@ -8408,6 +8787,24 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_few": "", + "indexed-label_many": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_few": "", + "parsedl-label_many": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_few": "", + "structured-metadata_many": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8431,7 +8828,6 @@ "label-wrap-lines": "Zalomit řádky" }, "log-details": { - "close": "Zavřít podrobnosti protokolu", "fields": "Pole", "links": "Odkazy", "log-line": "Řádek protokolu", @@ -8457,6 +8853,42 @@ "show-more": "zobrazit více", "tooltip-error": "Chyba: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Kopírovat odkaz na řádek protokolu", "copy-log": "Kopírovat řádek protokolu", @@ -8557,6 +8989,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8583,6 +9020,7 @@ "un-themed-log-details": { "aria-label-data-links": "Propojení dat", "aria-label-fields": "Pole", + "aria-label-line": "", "aria-label-log-level": "Úroveň protokolu", "aria-label-no-details": "Žádné podrobnosti" }, @@ -8650,6 +9088,8 @@ "updated-on": "Aktualizováno dne" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Odstranit" }, "unthemed-dashboard-import": { @@ -8659,8 +9099,17 @@ "text": { "import-dashboard": "Importovat nástěnku" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Tento nástroj může migrovat některé zdroje z této instalace do cloudové sady. Chcete-li začít, vytvořte snímek této instalace. Vytvoření snímku obvykle trvá méně než dvě minuty. Snímek je uložen spolu s touto instalací Grafana.", @@ -8809,6 +9258,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Nástěnku už zajišťuje a spravuje Grafana v cloudové instanci. Doporučujeme používat zajištěnou nástěnku. Pokud si přesto přejete zkopírovat nástěnku do cloudové instance, změňte ID nástěnky v JSON nástěnky, uložte nový snímek a nahrajte ji znovu.", "datasource-already-managed": "Zdroj dat už zajišťuje a spravuje Grafana v cloudové instanci. Pokud se jedná o jiný zdroj, nastavte jiné UID a zkuste to znovu.", "datasource-invalid-url": "Existuje zdroj dat s neplatnou adresou URL. Zadejte platnou adresu URL a zkuste to znovu.", @@ -9359,7 +9810,7 @@ "marker": { "100-node-count": ">100 uzlů", "aria-label-hidden-marker": "Značka skrytých uzlů: {{marker}}", - "node-count_one": "{{count}} uzel", + "node-count_one": "Uzly: {{count}}", "node-count_few": "Uzly: {{count}}", "node-count_many": "Uzly: {{count}}", "node-count_other": "Uzly: {{count}}" @@ -9372,11 +9823,11 @@ "aria-label-nodes-hidden-warning": "Varování na skryté uzly", "computing-layout": "Výpočet rozvržení", "no-data": "Žádná data", - "hidden-nodes_one": "<0> {{count}} uzel je skrytý z důvodu výkonu.", + "hidden-nodes_one": "<0>Některé uzly ({{count}}) jsou skryté z důvodu výkonu.", "hidden-nodes_few": "<0>Některé uzly ({{count}}) jsou skryté z důvodu výkonu.", "hidden-nodes_many": "<0>Některé uzly ({{count}}) jsou skryté z důvodu výkonu.", "hidden-nodes_other": "<0>Některé uzly ({{count}}) jsou skryté z důvodu výkonu.", - "processed-nodes_one": "<0> Vrstvené rozložení může být pomalé s {{count}} uzlem.", + "processed-nodes_one": "<0> Vrstvené rozložení může být pomalé s více uzly ({{count}}).", "processed-nodes_few": "<0> Vrstvené rozložení může být pomalé s více uzly ({{count}}).", "processed-nodes_many": "<0> Vrstvené rozložení může být pomalé s více uzly ({{count}}).", "processed-nodes_other": "<0> Vrstvené rozložení může být pomalé s více uzly ({{count}})." @@ -9505,6 +9956,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Vyberte organizaci" }, "page": { @@ -9727,6 +10179,7 @@ "permission": "Nemáte oprávnění pro zobrazení této stránky.", "title-access-denied": "Přístup nebyl povolen" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Nebyla nalezena žádná kořenová komponenta stránky aplikace" }, "browse": { @@ -9770,8 +10223,7 @@ "update-status-text": "pluginy byly aktualizovány" }, "versions": { - "confirmation-text-1": "Opravdu chcete přejít na nižší verzi", - "confirmation-text-2": "Normálně byste to neměli dělat", + "confirmation-text": "", "downgrade-confirm": "Přejít na nižší verzi", "downgrade-title": "Přejít na nižší verzi pluginu" } @@ -9825,6 +10277,10 @@ "empty-state": { "message": "Nebyly nalezeny žádné pluginy" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9858,7 +10314,11 @@ "updating": "Probíhá aktualizace" }, "install-controls-button": { - "title-uninstall-modal": "Odinstalovat plugin {{plugin}}" + "title-uninstall-modal": "Odinstalovat plugin {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Tento plugin není publikován na <2>grafana.com/plugins a nelze ho spravovat prostřednictvím katalogu.", @@ -10029,6 +10489,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Tato funkce je v současné době v aktivním vývoji. Pro nejlepší zážitek a nejnovější vylepšení doporučujeme použít <2>noční sestavení Grafany." @@ -10904,6 +11373,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Volič účtu služby", "select-placeholder": "Začněte psát a vyhledejte účty služeb" }, @@ -10949,6 +11419,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Přidat token účtu služby", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Odstranit účet služby", "disable-service-account": "Zakázat účet služby", "enable-service-account": "Povolit účet služby", @@ -10975,6 +11449,7 @@ "used-by": "Používá" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Upravit" }, "service-account-role-row": { @@ -10988,10 +11463,18 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Přidat účet služby", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Hledat účet služby podle názvu", "sub-title": "Účty služeb a jejich tokeny lze použít k autentizaci pomocí API Grafany. Další informace najdete v naší <2>dokumentaci.", "title-delete-service-account": "Odstranit účet služby", - "title-disable-service-account": "Zakázat účet služby" + "title-disable-service-account": "Zakázat účet služby", + "body-delete_one": "", + "body-delete_few": "", + "body-delete_many": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Platnost tokenu vypršela", @@ -11005,6 +11488,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exportovat jako JSON", "share-externally-title": "Sdílet externě", "share-internally-title": "Sdílet interně", @@ -11040,6 +11524,19 @@ "view-button": "Zobrazit JSON", "view-button-yaml": "Zobrazit YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Vytvořit panel knihovny." }, @@ -11048,7 +11545,7 @@ "info-text": "Vytvořte přímý odkaz na tuto nástěnku nebo panel přizpůsobený možnostmi dole.", "link-url": "Odkaz adresy URL", "render-alert": "Plugin pro renderování obrázků není nainstalován", - "render-instructions": "Chcete-li vyrenderovat obrázek panelu, musíte si nainstalovat <2>plugin Grafana pro renderování obrázků. Chcete-li nainstalovat plugin, obraťte se na správce Grafany.", + "render-instructions": "", "rendered-image": "Přímý odkaz na vyrenderovaný obrázek", "save-alert": "Nástěnka není uložena", "save-dashboard": "Chcete-li vyrenderovat obrázek panelu, musíte nejprve uložit nástěnku.", @@ -11116,6 +11613,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Náhled panelu" }, @@ -11383,7 +11886,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Příliš mnoho bodů pro správnou vizualizaci. <1>Aktualizujte dotaz pro získání méně bodů. <3>({{count}} bod byl přijat)", + "too-many-points_one": "Příliš mnoho bodů pro správnou vizualizaci. <1>Aktualizujte dotaz pro získání méně bodů. <3>(přijaté body: {{count}})", "too-many-points_few": "Příliš mnoho bodů pro správnou vizualizaci. <1>Aktualizujte dotaz pro získání méně bodů. <3>(přijaté body: {{count}})", "too-many-points_many": "Příliš mnoho bodů pro správnou vizualizaci. <1>Aktualizujte dotaz pro získání méně bodů. <3>(přijaté body: {{count}})", "too-many-points_other": "Příliš mnoho bodů pro správnou vizualizaci. <1>Aktualizujte dotaz pro získání méně bodů. <3>(přijaté body: {{count}})" @@ -11458,6 +11961,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Alternativní text, který se zobrazí, pokud nelze zobrazit obrázek, nebo pro uživatele, kteří používají čtečku obrazovky", "description-title-text": "Text, který se zobrazí po najetí kurzoru na obrázek", @@ -11478,6 +11982,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11528,6 +12033,7 @@ "tag-option-label": "Možnost tagu" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Volič týmu", "select-placeholder": "Vyberte tým" }, @@ -11829,6 +12335,18 @@ "placeholder-fields-or-number": "Pole nebo číslo" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Přezdívka", "label-mode": "Režim", "label-replace-all-fields": "Nahradit všechna pole" @@ -11843,6 +12361,13 @@ "concatenate-transformer-editor": { "label-label": "Štítek", "label-name": "Název", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Obrazový rám" }, "config-from-query-transformer-editor": { @@ -11853,6 +12378,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Přidat transformátor typu pole konverze", "aria-label-remove-convert-field-type-transformer": "Odebrat transformátor typu pole konverze", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Prohlížeč", "utc": "UTC" @@ -11863,7 +12392,13 @@ "label-input-format": "Vstupní formát", "label-join-with": "Propojit s", "label-set-timezone": "Nastavit časové pásmo", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Typ", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Použít explicitní oddělovač při spojování hodnot polí", "tooltip-input-format": "Zadejte formát vstupního pole, aby Grafana mohla správně analyzovat řetězec data.", "tooltip-specify-the-output-format": "Zadejte výstupní formát.", @@ -11885,6 +12420,19 @@ "desc": "", "off": "Vypnuto" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Přidat hodnotu výčtu", "generate-enum-values-from-data": "Generovat hodnoty výčtu z dat" @@ -11895,11 +12443,22 @@ "remove-enum-row-tooltip-delete": "Odstranit" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Oddělovač", "label-format": "Formát", "label-keep-time": "Ponechat čas", "label-replace-all-fields": "Nahradit všechna pole", "label-source": "Zdroj", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automaticky", "placeholder-select-delimiter": "Vyberte oddělovač…", "tooltip-regexp": "Příklad: {{regexExample}}" @@ -11908,8 +12467,22 @@ "aria-label-threshold-color": "Barva prahové hodnoty" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Pole", - "label-lookup": "Vyhledávání" + "label-lookup": "Vyhledávání", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Další nastavení", @@ -11918,12 +12491,26 @@ "use-as": "Použít jako" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Z proměnné", "label-identifier": "Identifikátor", "label-use-variable": "Použít proměnnou", "label-variable": "Proměnná", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Vzor regulárního výrazu" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Pole", "label-match": "Shoda", @@ -11933,14 +12520,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Přidejte podmínku", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Podmínky", - "label-filter-type": "Typ filtru" + "label-filter-type": "Typ filtru", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Pole", "label-format": "Formát", "label-substring-range": "Rozsah dílčího řetězce" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formát", "label-set-timezone": "Nastavit časové pásmo", @@ -11948,6 +12569,70 @@ "tooltip-format": "Výstupní formát pro pole zadané jako <2>řetězec formátu Moment.js.", "tooltip-timezone-manually": "Nastavit časové pásmo data manuálně" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Prohlížeč", @@ -11975,22 +12660,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorováno", "placeholder-select-stats": "Vyberte statistiky" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Výpočty nebudou mít vliv, pokud nebudou seskupena žádná pole" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Pokud je povoleno, vnořené tabulky zobrazí názvy polí jako záhlaví tabulky", "label-show-field-names-in-nested-tables": "Zobrazit názvy polí ve vnořených tabulkách", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Výpočty nebudou mít vliv, pokud nebudou seskupena žádná pole." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Hodnota buňky", "label-column": "Sloupec", "label-empty-value": "Prázdná hodnota", "label-row": "Řádek", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Booleovská nepravdivá hodnota", @@ -12009,6 +12718,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automaticky", "placeholder-default": "Výchozí hodnota: 30", "placeholder-none": "Žádné" @@ -12017,7 +12732,16 @@ "label-as-percentile": "Jako percentil", "tooltip-transform-index-percentile": "Transformovat index řádku jako percentil." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Vstup neobsahuje žádné štítky", "no-input-or-labels-found": "Nebyl nalezen žádný vstup (nebo štítky)", @@ -12039,10 +12763,30 @@ "label-labels": "Štítky", "label-mode": "Režim", "label-value-field-name": "Název pole hodnoty", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "Vybrat štítek (nepovinné)", "tooltip-replace-value-field-label": "Nahradit název pole hodnoty štítkem" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Hodnota nebo proměnná" }, "long-info": { @@ -12053,7 +12797,13 @@ "string-values-separate": "Hodnoty řetězců jsou reprezentovány jako samostatná pole místo štítků" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Sloučení nemá žádný vliv při použití na jeden snímek." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Sloučení nemá žádný vliv při použití na jeden snímek.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Všechny hodnoty jsou číselné", @@ -12063,10 +12813,16 @@ "time-in-ascending-order": "Čas ve vzestupném pořadí" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automaticky", "field-order-manual": "", - "first-frame-warning": "Uspořádání polí funguje jenom s jedním snímkem. Nejprve zvažte použití transformace spojení nebo filtrování vstupu." + "first-frame-warning": "Uspořádání polí funguje jenom s jedním snímkem. Nejprve zvažte použití transformace spojení nebo filtrování vstupu.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Nastavuje způsob zobrazování názvů vybraných polí. Název snímku je obvykle lepší pro tabulková data" @@ -12091,9 +12847,41 @@ "partition-values-works-single-frame": "Rozdělení podle hodnot funguje jenom s jedním snímkem.", "tooltip-keeps-partition-fields-frames": "Udržuje pole oddílů ve snímcích" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formát", - "label-info": "Info" + "label-info": "Info", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "a", @@ -12105,6 +12893,9 @@ "label-operation": "Činnost" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Výpočty", "label-include-time": "Zahrnout čas", "label-labels-to-fields": "Štítky k polím", @@ -12119,12 +12910,23 @@ "series-to-rows": "Řady na řádky" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Vyberte statistiku" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Hodnota nebo proměnná", "variable-regex-documentation": "Formátuje vícehodnotovou proměnnou na řetězec regex" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Krychlový", @@ -12146,6 +12948,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Počet bodů X, Y k předpovědi" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Shoda", "label-replace": "Vyměnit", @@ -12154,11 +12964,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Pole", - "label-mode": "Režim" + "label-mode": "Režim", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Pole", "label-reverse": "Dozadu", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Vyberte pole" }, "supplier": { @@ -12178,6 +13014,17 @@ "transform": "Transformovat" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Statistika", "label-time-field": "Pole času", @@ -12191,10 +13038,34 @@ "placeholder-value": "Hodnota", "tooltip-name-for-value-fields": "Název polí hodnot" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Činnost", "placeholder-field": "Pole" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Pole s více hodnotami libovolného typu", "single-frame": "Jeden snímek", @@ -12247,6 +13118,7 @@ "title": "Organizace" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Volič uživatelů", "select-placeholder": "Začněte psát a vyhledejte uživatele" }, @@ -12332,6 +13204,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Odstranit proměnnou" }, "create-ad-hoc-variable-adapter": { @@ -12380,9 +13254,34 @@ "label-refresh": "Obnovit" }, "query-variable-sort-select": { - "description-values-variable": "Jak třídit hodnoty této proměnné" + "description-values-variable": "Jak třídit hodnoty této proměnné", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "výchozí hodnota, pokud existuje", "text-options": "Možnosti textu" }, @@ -12411,6 +13310,8 @@ "description-optional-display-name": "Nepovinné zobrazované jméno", "description-template-variable-characters": "Název proměnné šablony. (max. 50 znaků)", "general": "Obecné", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Popisný text", "placeholder-label-name": "Název štítku", "placeholder-variable-name": "Název proměnné", @@ -12425,9 +13326,15 @@ "tooltip-duplicate-variable": "Duplikovat proměnnou", "tooltip-remove-variable": "Odebrat proměnnou" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Přepnout všechny hodnoty" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Zobrazit použití" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 5ce02f8a0d8..1fda3b8218e 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Einige Funktionen sind stabil (GA) und standardmäßig aktiviert, während andere zurzeit in ihrer vorläufigen Beta-Phase sind und für eine frühzeitige Anwendung verfügbar sind.", "confirm-modal-body-2": "Wir raten Ihnen, die Auswirkungen jeder Funktionsänderung zu kennen, bevor Sie Änderungen vornehmen.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Allgemeine Verfügbarkeit", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Organisation löschen", + "confirmText-delete": "", "title-delete": "Löschen" }, "anon-users": { "not-found": "Keine anonymen Benutzer gefunden." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Abmeldung von allen Geräten erzwingen" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Sie sind nicht berechtigt, Benutzer in dieser Organisation anzuzeigen. Wenden Sie sich an Ihren Serveradministrator, um diese Organisation zu aktualisieren.", "heading": "Organisation bearbeiten", @@ -208,9 +216,11 @@ "not-editable": "Die Rolle dieses Benutzers kann nicht bearbeitet werden, da sie von Ihrem Authentifizierungsanbieter synchronisiert wird. Weitere Informationen finden Sie in den <1>Grafana-Authentifizierungsdokumenten." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Rolle" }, + "confirmText-delete": "", "delete-aria-label": "Nutzer löschen: {{name}}", "title-delete": "Löschen" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Diese Systemeinstellungen werden in grafana.ini oder custom.ini definiert (oder in ENV-Variablen überschrieben). Um diese zu ändern, müssen Sie Grafana jetzt neu starten." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Enterprise-Lizenz" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Ändern", + "confirmText-change": "", "grafana-admin-key": "Grafana-Admin", "grafana-admin-no": "Nein", "grafana-admin-yes": "Ja", "title": "Berechtigungen" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Benutzer löschen", "disable-button": "Benutzer deaktivieren", "edit-button": "Bearbeiten", @@ -312,6 +330,9 @@ "title-delete-user": "Benutzer löschen", "title-disable-user": "Nutzer deaktivieren" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Browser und Betriebssystem", "force-logout-all-button": "Abmeldung von allen Geräten erzwingen", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Aktive Zeitvorgaben" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Weitere hinzufügen" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Stummschaltung, Gruppierung und Zeitsteuerung (optional)", "title-muting-grouping-and-timings": "Stummschaltung, Gruppierung und Zeitsteuerung" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Link kopieren", "duplicate": "Duplikat", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML bearbeiten", @@ -546,6 +577,7 @@ "view-configuration": "Konfiguration anzeigen" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Die interne Grafana-Alertmanager-Konfiguration kann nicht manuell geändert werden. Bearbeiten Sie die individuellen Ressourcen über die Benutzeroberfläche, um diese Konfiguration zu ändern.", "gma-manual-configuration-is-not-supported": "Manuelle Konfigurationsänderungen werden nicht unterstützt", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Alertmanager-Konfiguration wird zurückgesetzt" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Vergleichen", "restore": "Wiederherstellen", "text-latest": "Neueste" }, + "confirmText-yes-restore-configuration": "", "loading": "Wird geladen ...", "no-previous-configurations": "Keine vorherigen Konfigurationen", "this-might-take-a-while": "Dies kann eine Weile dauern ...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Weitere Aktionen für Kontaktpunkt „{{contactPointName}}“", + "ariaLabel-delete": "", "button-edit": "Bearbeiten", "button-view": "Anzeigen", + "export-ariaLabel-export": "", "export-label-export": "Exportieren", "label-delete": "Löschen", "label-manage-permissions": "Berechtigungen verwalten", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Nachricht zur Behebung deaktivieren" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Muss eine positive ganze Zahl sein.", "must-enter-a-group-name": "Es muss ein Gruppenname eingegeben werden" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Fehler beim Laden der Datenquellendetails", "edit": "Bearbeiten", - "evaluations-to-fire": "Auszulösende Evaluierungszyklen", "export": "Exportieren", "folder": "Ordner", "group-loading-error": "Fehler beim Laden der Gruppe", "interval": "Intervall", "namespace": "Namensraum", - "pending-period": "Wartezeit", - "recording": "Aufzeichnung", - "rule-name": "Regelname" + "new": "" }, "group-edit": { "ds-error": "Fehler beim Laden der Datenquellendetails", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Erste Schritte", "label-insights": "Insights", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Meldungen" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Andere Datenquellen" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Deaktiviert", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "Der Benachrichtigungsrichtlinienbaum wurde von einem anderen Benutzer aktualisiert.", "error-code": "Fehlermeldung: „{{error}}“", - "fallback": "Beim Aktualisieren Ihrer Benachrichtigungsrichtlinien ist ein Fehler aufgetreten.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Bitte aktualisieren Sie die Seite und versuchen Sie es erneut.", - "title": "Fehler beim Speichern der Benachrichtigungsrichtlinie" + "title": "" }, "n-more-policies_one": "{{count}} zusätzliche Richtlinie", "n-more-policies_other": "{{count}} zusätzliche Richtlinien" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Abfrage hinzufügen", "body-queries-expressions-configured": "Erstellen Sie mindestens eine Abfrage oder einen Ausdruck für eine Warnung", + "confirmText-deactivate": "", "expressions": "Ausdrücke", "loading-data-sources": "Datenquellen werden geladen ...", "manipulate-returned-queries-other-operations": "Manipulieren Sie Daten, die von Abfragen mit mathematischen und anderen Operationen zurückgegeben werden.", @@ -2184,6 +2237,9 @@ "label-export-all": "Alles exportieren" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Der Abfrage-Editor konnte nicht geladen werden wegen: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Sie müssen eine neue Bewertungsgruppe für die kopierte Regel festlegen, da die ursprüngliche bereitgestellt wurde und nicht für Regeln genutzt werden kann, die in der Benutzeroberfläche erstellt werden.", "body-not-provisioned": "Die neue Regel wird <1>nicht als bereitgestellte Regel gekennzeichnet.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Bereitgestellte Warnregel kopieren" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Warnregel prüfen" }, "rule-list": { - "cannot-find-rule-details-for": "Regeldetails für UID {{uid}} können nicht gefunden werden", - "cannot-load-rule-details-for": "Regeldetails für UID {{uid}} können nicht geladen werden", "configure-datasource": "Konfigurieren", "draft-new-rule": "Neue Regel entwerfen", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Sie finden Ihre gewünschte Datenquelle nicht? Einige Datenquellen werden für Warnungen nicht unterstützt. Klicken Sie für weitere Informationen auf das Symbol." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Neuen Alertmanager hinzufügen", "builtin-alertmanager": "Integrierter Alertmanager", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Benachrichtigungsvorlage auswählen", "loading": "Wird geladen ...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Benachrichtigungsvorlage auswählen" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Aktionen", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Keine Vorlagen festgelegt.", "template-group": "Vorlagengruppe", "title-delete-template-group": "Vorlagengruppe löschen" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "(vor {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Der Zielordner ist nicht leer, einige Regeln können überschrieben oder entfernt werden. Sind Sie sicher, dass Sie diese Warnregeln zu von Grafana verwalteten Regeln importieren möchten?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Kontaktpunkt löschen" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Benachrichtigungsrichtlinie löschen" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Anmerkung", "first-value": "Erster Wert", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Anmerkungsabfrage hinzufügen", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Weitere Informationen finden Sie in der <2>Anmerkungs-Dokumentation.", "title": "Es wurden noch keine benutzerdefinierten Anmerkungsabfragen hinzugefügt" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} Ereignisse (von {{numFields}} Feldern)", "no-events-found": "Keine Ereignisse gefunden", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Erlaubte Team-IDs festlegen", "display-name-description": "Wird auf der Anmeldeseite als „Anmelden mit …“ angezeigt. Hilfreich, wenn Sie mehr als einen Identitätsanbieter oder SSO-Protokolle nutzen.", "display-name-label": "Anzeigename", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Name des Keys, der für die Nutzer-E-Mail-Suche in der Attributzuordnung des OAuth2-ID-Tokens verwendet wird.", "email-attribute-name-label": "E-Mail-Attributname", "email-attribute-path-description": "JMESPath-Ausdruck, der für die Nutzer-E-Mail-Suche aus den Nutzerinformationen verwendet wird.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Integer-Liste der Team-IDs.", "team-ids-label": "Team-IDs", "team-ids-numbers": "Team-IDs müssen Zahlen sein.", - "team-ids-other": "String-Liste der Team-IDs.", + "team-ids-other": "", "team-ids-placeholder": "Geben Sie Team-IDs ein und drücken Sie zum Hinzufügen die Eingabetaste", "teams-url-description": "Die URL, die für die Abfrage von Team-IDs verwendet wird. Wenn dies nicht festgelegt ist, lautet der Standardwert /teams.", "teams-url-description-oauth": "Wenn Sie „{{ teamsURLLabel }}“ konfigurieren, müssen Sie auch „{{ teamIDsAttributePathLabel }}“ konfigurieren.", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Auf Standardwerte zurücksetzen" }, + "confirmText-reset": "", "disable": "Deaktivieren", "disabling": "Wird deaktiviert …", "discard": "Verwerfen", @@ -3473,11 +3563,11 @@ "button-create": "Erstellen", "button-creating": "Wird erstellt ...", "cancel": "Abbrechen", - "error-invalid-folder-name": "Ungültiger Ordnername", "error-required": "Ordnername erforderlich", "folder-name-input-placeholder-enter-folder-name": "Ordnernamen eingeben", "label-folder-name": "Ordnername", "text-pull-request-created": "Ein Pull-Request wurde erstellt, mit Änderungen an diesem Ordner:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull-Request erstellt", "title-this-repository-is-read-only": "Dieses Repository ist schreibgeschützt" }, @@ -3895,6 +3985,7 @@ "tooltip": "Schließen" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Beim Laden der Optionen ist ein Fehler aufgetreten." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Berechnet dynamisch das Intervall, indem der Zeitbereich durch die angegebene Anzahl dividiert wird" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Weiter zu externer Seite?" } }, "render-left-actions": { - "text-public": "Öffentlich", "tooltip-view-as-scene": "Als Szene anzeigen" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Dashboard konnte nicht geladen werden" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Dashboard konnte nicht abgerufen werden" @@ -4540,6 +4641,13 @@ "editable": "Editierbar", "readonly": "Schreibgeschützt" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Daten transformieren" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Sind Sie sicher, dass Sie das Dashboard auf die Version {{version}} zurücksetzen möchten? Alle nicht gespeicherten Änderungen gehen verloren.", + "confirmText-restore-version": "", "title-restore-version": "Version wiederherstellen" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Dieser Titel ist nicht eindeutig" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Speichern unter" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Ein Dashboard mit demselben Namen im ausgewählten Ordner existiert bereits.<1><2>Möchten Sie dieses Dashboard trotzdem speichern?", "body-version-mismatch": "Eine andere Person hat dieses Dashboard aktualisiert<1><2>Möchten Sie dieses Dashboard trotzdem speichern?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Widerspruch", "title-version-mismatch": "Widerspruch" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Zum nächsten Dashboard", "playlist-previous": "Zum vorherigen Dashboard", "playlist-stop": "Wiedergabeliste stoppen", - "public-dashboard": "Öffentlich", "refresh": "Dashboard aktualisieren", "save": "Dashboard speichern", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Transformation anwenden auf" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Debuggen", "title-disable-transformation": "Transformation deaktivieren", "title-filter": "Filter", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Nach Transformation suchen", "show-images": "Bilder anzeigen", - "title-add-another-transformation": "Weitere Transformation hinzufügen" + "title-add-another-transformation": "Weitere Transformation hinzufügen", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Weitere Transformation hinzufügen" }, + "confirmText-delete-all": "", "delete-all-transformations": "Alle Transformationen löschen", "title-delete-all-transformations": "Alle Transformationen löschen?", "tooltip-clear-search": "Suche löschen", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Auswahl der Version {{version}} umschalten", "date": "Datum", + "name-latest": "", "notes": "Anmerkungen", "restore": "Wiederherstellen", "updated-by": "Aktualisiert von", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Ermöglicht Nutzern, individuelle Werte zur Liste hinzuzufügen", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Dimensionen als CSV angeben: {{name}}, {{value}}", "label-data-source": "Datenquelle", - "label-use-static-key-dimensions": "Statische Key-Dimensionen verwenden" + "label-use-static-key-dimensions": "Statische Key-Dimensionen verwenden", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Öffentliche URL widerrufen" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Benutzerdefinierte Optionen", + "name-values-separated-comma": "", "selection-options": "Auswahloptionen" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Typ", "label-url": "URL", "label-with-tags": "Mit Tags", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Dashboard öffnen" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Der Wert ist noch nicht in der Grafana-Datenbank gespeichert", "not-yet-saved": "Der Wert wird nicht in der Grafana-Datenbank gespeichert", - "open-pull-request-in-git-hub": "Pull-Anfrage in GitHub öffnen", - "title-dashboard-loaded-branch-git-hub": "Dieses Dashboard wird von einem Branch in GitHub geladen.", "title-dashboard-loaded-external-repository": "Dieses Dashboard wird von einem externen Repository geladen", - "title-dashboard-loaded-request-git-hub": "Dieses Dashboard wird von einer Pull-Anfrage in GitHub geladen.", - "title-error-loading-dashboard": "Fehler beim Laden des Dashboards", - "value-not-saved": "Der Wert ist noch nicht in der Grafana-Datenbank gespeichert", - "view-pull-request-in-git-hub": "Pull-Anfrage in GitHub anzeigen" + "title-error-loading-dashboard": "Fehler beim Laden des Dashboards" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Datenquellenoptionen", "description-instance-name-filter": "Regex-Filter, für den Datenquelleninstanzen aus der Liste der Variablenwerte ausgewählt werden sollen. Für alle leer lassen.", "example-instance-name-filter": "Beispiel: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Auswahloptionen" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Transformation hinzufügen" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Spaltenoptionen", "row-height-options": "Optionen für die Zeilenhöhe" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Dimensionen als CSV angeben: {{name}}, {{value}}", "group-by-options": "Nach Optionen gruppieren", "label-data-source": "Datenquelle", - "label-use-static-group-by-dimensions": "Statische Gruppen-Dimensionen verwenden" + "label-use-static-group-by-dimensions": "Statische Gruppen-Dimensionen verwenden", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "In die Zwischenablage kopieren", @@ -5521,9 +5710,14 @@ "apply": "Anwenden" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Der berechnete Wert wird diesen Schwellenwert nicht unterschreiten", "description-step-count": "Wie oft der aktuelle Zeitbereich zur Berechnung des Werts geteilt werden soll", - "interval-options": "Intervalloptionen" + "interval-options": "Intervalloptionen", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Verwendet bei {{count}} Dashboards", "usage-count_other": "Verwendet bei {{count}} Dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Ein Dashboard mit demselben Namen im ausgewählten Ordner existiert bereits. Möchten Sie dieses Dashboard trotzdem speichern?", "title-name-already-exists": "Der Name existiert bereits" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Weiter zu externer Seite?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Weitere Transformation hinzufügen", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Alle Transformationen löschen", "title-delete-all-transformations": "Alle Transformationen löschen?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Mithilfe von benannten Erfassungsgruppen können der Anzeigetext und der Wert getrennt werden (<1>siehe Beispiele).", "description-optional": "Optional, wenn Sie einen Teil eines Reihennamens oder eines metrischen Knotensegments extrahieren möchten.", "label-data-source": "Datenquelle", + "label-static-options-sort": "", "label-target-data-source": "Zieldatenquelle", + "label-use-static-options": "", + "name-regex": "", "query-options": "Abfrageoptionen", - "selection-options": "Auswahloptionen" + "selection-options": "Auswahloptionen", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Sind Sie sicher, dass Sie das Dashboard auf die Version {{version}} zurücksetzen möchten? Alle nicht gespeicherten Änderungen gehen verloren.", + "confirmText-restore-version": "", "title-restore-version": "Version wiederherstellen" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Ordner", "render-footer": { "title-failed-to-save-dashboard": "Dashboard konnte nicht gespeichert werden" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Ermöglicht die gleichzeitige Auswahl mehrerer Werte", "description-enables-option-include-variables": "Aktiviert eine Option, mit der alle Werte einbezogen werden", - "description-enables-users-custom-values": "Ermöglicht Nutzern, individuelle Werte zur Liste hinzuzufügen" + "description-enables-users-custom-values": "Ermöglicht Nutzern, individuelle Werte zur Liste hinzuzufügen", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Freigabe-Menü umschalten" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Kopieren in die Zwischenablage fehlgeschlagen" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(optional)", "text-options": "Textoptionen" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Möchten Sie die Verknüpfung dieses Panels wirklich aufheben?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Diese Variable wird von keiner Variable bzw. keinem Dashboard referenziert.", "aria-label-variable-referenced-other-variables-dashboard": "Diese Variable wird von anderen Variablen oder Dashboards referenziert.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Variablen-Editor-Formular", "back-to-list": "Zurück zur Liste", + "confirmText": { + "delete-variable": "" + }, "delete": "Löschen", "description-optional-display-name": "Optionaler Anzeigename", "description-template-variable-characters": "Der Name der Vorlagenvariable. (Max. 50 Zeichen)", "general": "Allgemein", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Beschreibender Text", "placeholder-label-name": "Label-Name", "placeholder-variable-name": "Variablenname", @@ -5829,13 +6066,25 @@ "variable": "Variable" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Variable löschen", "tooltip-duplicate-variable": "Variable duplizieren", "tooltip-remove-variable": "Variable entfernen" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Ausblenden" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Nutzungen angezeigt für: {{variableId}}", "tooltip-show-usages": "Nutzungen anzeigen" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Auswahl der Version {{version}} umschalten", "date": "Datum", + "name-latest": "", "notes": "Anmerkungen", "restore": "Wiederherstellen", "updated-by": "Aktualisiert von", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Hochladen" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limit", "label-value": "Wert", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max.", "label-min": "Min.", - "label-value": "Wert" + "label-value": "Wert", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Dienstnamen-Operator auswählen", "aria-label-select-span-name": "Spannen-Name auswählen", "aria-label-select-span-name-operator": "Spannen-Namen-Operator auswählen", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Spannenfilter", "label-duration": "Dauer", "label-service-name": "Dienstname", @@ -6939,6 +7199,8 @@ "split-widen": "Bereich verbreitern" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Feedback geben", "label-copied": "Kopiert!", "label-export": "Exportieren", @@ -7046,6 +7308,7 @@ "when": "WANN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operation" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Ordner löschen", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Ordnerfilter", "select-placeholder": "Nach Ordner filtern" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Leider konnte ich Ihre Anfrage nicht abschließen. Bitte versuchen Sie es erneut.", "send-custom-feedback": "Senden" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Breite", "label-longitude": "Länge" @@ -7153,6 +7463,14 @@ "center": "Mitte:", "zoom": "Zoom:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Layer" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Geomap-Stilregel hinzufügen" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Layer hinzufügen", "no-layers": "Keine Layer?" @@ -7185,16 +7511,38 @@ "label-zoom": "Zoom", "use-current-map-settings": "Aktuelle Karteneinstellungen verwenden" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Symbol" }, "measure-overlay": { "tooltip-show-measure-tools": "Messtools anzeigen" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Der Basiskarten-Layer wird vom Serveradministrator konfiguriert." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Ausrichten", "label-baseline": "Basislinie", "label-color": "Farbe", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Symbol vertikal ausrichten", "label-text-label": "Text-Label", "label-x-offset": "X-Versatz", - "label-y-offset": "Y-Versatz" + "label-y-offset": "Y-Versatz", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Vergleichsoperator", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Funktionseigenschaft", "placeholder-numeric-value": "Numerischer Wert", "placeholder-value": "Wert" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "{{colorLabel}} Farbe" }, "confirm-button": { - "cancel": "Abbrechen" + "cancel": "Abbrechen", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Geben Sie zur Bestätigung „{{confirmPromptText}}“ ein" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "Einklappen des Panels einschalten", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Abfrage abbrechen", "tooltip-cancel-loading": "Abfrage abbrechen", "tooltip-stop-streaming": "Streaming anhalten", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Groß-/Kleinschreibung beachten", "inspect-drawer-title": "Wert prüfen", "inspect-menu-label": "Wert prüfen", + "nested-table": { + "no-data": "" + }, "no-values-label": "Keine Werte", "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} von {{numRows}} Zeilen" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Hinzufügen", + "placeholder-new-tag": "", "remove": "Tag entfernen: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Klicken, um zu {{actionTitle}}", "footer-click-to-navigate": "Zum Öffnen von {{linkTitle}} klicken", "timestamp": "Zeitstempel" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Bibliotheksleiste erstellen" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Möchten Sie dieses Panel löschen?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Verwendet bei {{count}} Dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Suche nach Name oder Beschreibung" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Bibliotheks-Panel wird geladen ..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Erstellen Sie einen personalisierten, direkten Link, um Ihr Panel innerhalb Ihrer Organisation mit den folgenden Anpassungseinstellungen zu teilen:", "download-image": "Bild herunterladen", - "render-image": "Bild erstellen", - "render-image-error": "Das Panel-Bild konnte nicht gerendert werden", - "render-image-error-description": "Beim Generieren des Bildes ist ein Fehler aufgetreten" + "render-image": "Bild erstellen" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Zeilen umbrechen" }, "log-details": { - "close": "Logdetails schließen", "fields": "Felder", "links": "Links", "log-line": "Protokollzeile", @@ -8405,6 +8795,42 @@ "show-more": "mehr anzeigen", "tooltip-error": "Fehler: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Link zur Protokollzeile kopieren", "copy-log": "Protokollzeile kopieren", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Datenlinks", "aria-label-fields": "Felder", + "aria-label-line": "", "aria-label-log-level": "Log-Stufe", "aria-label-no-details": "Keine Angaben" }, @@ -8598,6 +9030,8 @@ "updated-on": "Aktualisiert am" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Löschen" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Dashboard importieren" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Dieses Tool kann Ressourcen von dieser Installation zu Ihrem Cloud-Stack migrieren. Erstellen Sie einen Snapshot dieser Installation, um zu beginnen. Das Erstellen eines Snapshots dauert in der Regel weniger als zwei Minuten. Der Snapshot wird neben dieser Grafana-Installation gespeichert.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Das Dashboard wird bereits von Grafana in der Cloud-Instanz bereitgestellt und verwaltet. Wir empfehlen, in Zukunft das bereitgestellte Dashboard zu verwenden. Wenn Sie das Dashboard weiterhin in die Cloud-Instanz kopieren möchten, ändern Sie bitte die Dashboard-ID im Dashboard-JSON, speichern Sie einen neuen Snapshot und laden Sie ihn erneut hoch.", "datasource-already-managed": "Die Datenquelle wird bereits von Grafana in der Cloud-Instanz bereitgestellt und verwaltet. Wenn es sich dabei um eine andere Ressource handelt, legen Sie bitte eine andere UID fest und versuchen Sie es erneut.", "datasource-invalid-url": "Es gibt eine Datenquelle mit einer ungültigen URL. Geben Sie eine gültige URL an und versuchen Sie es erneut.", @@ -9318,7 +9763,7 @@ "aria-label-nodes-hidden-warning": "Warnung bei ausgeblendeten Knoten", "computing-layout": "Layout berechnen", "no-data": "Keine Daten", - "hidden-nodes_one": "<0> {{count}} Knoten ist aus Leistungsgründen ausgeblendet.", + "hidden-nodes_one": "<0> {{count}} Knoten sind aus Leistungsgründen ausgeblendet.", "hidden-nodes_other": "<0> {{count}} Knoten sind aus Leistungsgründen ausgeblendet.", "processed-nodes_one": "<0> Das geschichtete Layout kann mit {{count}} Knoten langsam sein.", "processed-nodes_other": "<0> Das geschichtete Layout kann mit {{count}} Knoten langsam sein." @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Organisation auswählen" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Sie haben keine Berechtigung, diese Seite anzuzeigen.", "title-access-denied": "Zugriff verweigert" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Keine Root-App-Seitenkomponente gefunden" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "Plugins aktualisiert" }, "versions": { - "confirmation-text-1": "Möchten Sie wirklich auf die Version folgende herabstufen:", - "confirmation-text-2": "Normalerweise sollten Sie das nicht tun", + "confirmation-text": "", "downgrade-confirm": "Herabstufen", "downgrade-title": "Plugin-Version herabstufen" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Keine Plugins gefunden" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Wird aktualisiert" }, "install-controls-button": { - "title-uninstall-modal": "Deinstallieren von {{plugin}}" + "title-uninstall-modal": "Deinstallieren von {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Dieses Plugin wird nicht bei <2>grafana.com/plugins veröffentlicht und kann nicht über den Katalog verwaltet werden.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Diese Funktion befindet sich momentan in der aktiven Entwicklung. Für die bestmögliche Nutzererfahrung und die neuesten Verbesserungen empfehlen wir die Nutzung von <2>Nightly Build von Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Dienstkontoauswahl", "select-placeholder": "Tippen Sie, um nach Dienstkonten zu suchen" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Dienstkonto-Token hinzufügen", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Dienstkonto löschen", "disable-service-account": "Dienstkonto deaktivieren", "enable-service-account": "Dienstkonto aktivieren", @@ -10907,6 +11375,7 @@ "used-by": "Genutzt von" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Bearbeiten" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Dienstkonto hinzufügen", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Dienstkonto nach Namen suchen", "sub-title": "Dienstkonten und ihre Token können für die Authentifizierung gegenüber der Grafana-API verwendet werden. Erfahren Sie mehr in unserer <2>Dokumentation.", "title-delete-service-account": "Dienstkonto löschen", - "title-disable-service-account": "Dienstkonto deaktivieren" + "title-disable-service-account": "Dienstkonto deaktivieren", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Dieser Token ist abgelaufen", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Als JSON exportieren", "share-externally-title": "Extern teilen", "share-internally-title": "Intern teilen", @@ -10972,6 +11448,19 @@ "view-button": "JSON anzeigen", "view-button-yaml": "YAML anzeigen" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Bibliotheks-Panel erstellen." }, @@ -10980,7 +11469,7 @@ "info-text": "Erstelle einen direkten Link zu diesem Dashboard oder Panel mit den folgenden angepassten Optionen.", "link-url": "Link-URL", "render-alert": "Bild-Render-Plug-in nicht installiert", - "render-instructions": "Um ein Panel-Bild zu rendern, müssen Sie das <2>Bild-Renderer-Plugin von Grafana installieren. Bitte wenden Sie sich an Ihren Grafana-Administrator, um das Plugin zu installieren.", + "render-instructions": "", "rendered-image": "Direktlink zum gerenderten Bild", "save-alert": "Dashboard nicht gespeichert", "save-dashboard": "Um ein Panel-Bild zu rendern, muss zuerst das Dashboard gespeichert werden.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Panel-Vorschau" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Zu viele Punkte, um sie richtig darzustellen. <1>Aktualisieren Sie die Abfrage, damit weniger Punkte zurückgegeben werden. <3>({{count}} Punkt erhalten)", + "too-many-points_one": "Zu viele Punkte, um sie richtig darzustellen. <1>Aktualisieren Sie die Abfrage, damit weniger Punkte zurückgegeben werden. <3>({{count}} Punkte erhalten)", "too-many-points_other": "Zu viele Punkte, um sie richtig darzustellen. <1>Aktualisieren Sie die Abfrage, damit weniger Punkte zurückgegeben werden. <3>({{count}} Punkte erhalten)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Alternativer Text, der angezeigt wird, wenn ein Bild nicht angezeigt werden kann, oder für Nutzer, die einen Screenreader verwenden", "description-title-text": "Text, der angezeigt wird, wenn über dem Bild der Mauszeiger bewegt wird", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Tag-Option" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Team-Picker", "select-placeholder": "Wählen Sie ein Team" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Feld(er) oder Nummer" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Modus", "label-replace-all-fields": "Alle Felder ersetzen" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Label", "label-name": "Name", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Frame" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Transformator für „Feldtyp konvertieren“ hinzufügen", "aria-label-remove-convert-field-type-transformer": "Transformator für „Feldtyp konvertieren“ entfernen", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Browser", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Eingabeformat", "label-join-with": "Verbinden mit", "label-set-timezone": "Zeitzone festlegen", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Typ", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Verwenden Sie ein explizites Trennzeichen, wenn Sie Array-Werte verbinden", "tooltip-input-format": "Geben Sie das Format des Eingabefelds an, damit Grafana den Datumsstring korrekt parsen kann.", "tooltip-specify-the-output-format": "Geben Sie das Ausgabeformat an.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Aus" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Aufzählungswert hinzufügen", "generate-enum-values-from-data": "Aufzählungswerte aus Daten generieren" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Löschen" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Trennzeichen", "label-format": "Format", "label-keep-time": "Zeit beibehalten", "label-replace-all-fields": "Alle Felder ersetzen", "label-source": "Quelle", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Trennzeichen wählen ...", "tooltip-regexp": "Beispiel: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Schwellenwert-Farbe" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Feld", - "label-lookup": "Suche" + "label-lookup": "Suche", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Zusätzliche Einstellungen", @@ -11848,12 +12413,26 @@ "use-as": "Verwenden als" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Von Variable", "label-identifier": "Kennung", "label-use-variable": "Variable verwenden", "label-variable": "Variable", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Reguläres Ausdrucksmuster" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Feld", "label-match": "Übereinstimmung", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Bedingung hinzufügen", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Bedingungen", - "label-filter-type": "Filtertyp" + "label-filter-type": "Filtertyp", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Feld", "label-format": "Format", "label-substring-range": "Teilstring-Bereich" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Zeitzone festlegen", @@ -11878,6 +12491,70 @@ "tooltip-format": "Das Ausgabeformat für das Feld, das als <2>Moment.js-Format-String angegeben ist.", "tooltip-timezone-manually": "Stellen Sie die Zeitzone des Datums manuell ein" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Browser", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignoriert", "placeholder-select-stats": "Statistiken auswählen" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Berechnungen haben keine Auswirkung, wenn keine Felder gruppiert werden" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Wenn dies aktiviert ist, werden verschachtelte Tabellen Feldnamen als Tabellenüberschrift anzeigen", "label-show-field-names-in-nested-tables": "Feldnamen in verschachtelten Tabellen anzeigen", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Berechnungen haben keine Auswirkung, wenn keine Felder gruppiert werden." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Zellenwert", "label-column": "Spalte", "label-empty-value": "Leerer Wert", "label-row": "Zeile", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Boolescher falscher Wert", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Auto", "placeholder-default": "Standard: 30", "placeholder-none": "Keine" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Als Perzentil", "tooltip-transform-index-percentile": "Zeilenindex als Perzentil umwandeln." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Die Eingabe enthält keine Labels", "no-input-or-labels-found": "Keine Eingabe (oder Labels) gefunden", @@ -11969,10 +12685,30 @@ "label-labels": "Labels", "label-mode": "Modus", "label-value-field-name": "Wertefeldname", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Optional) Label auswählen", "tooltip-replace-value-field-label": "Ersetzen Sie den Namen des Wertefelds durch ein Label" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Wert oder Variable" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "String-Werte werden als separate Felder und nicht als Labels dargestellt" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Zusammenführen hat keine Auswirkung bei Anwendung auf einen einzelnen Frame." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Zusammenführen hat keine Auswirkung bei Anwendung auf einen einzelnen Frame.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Alle Werte sind numerisch", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Zeit in aufsteigender Reihenfolge" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Auto", "field-order-manual": "", - "first-frame-warning": "Felder organisieren funktioniert nur mit einem einzelnen Frame. Überlegen Sie, zunächst eine Join-Transformation anzuwenden oder die Eingabe zu filtern." + "first-frame-warning": "Felder organisieren funktioniert nur mit einem einzelnen Frame. Überlegen Sie, zunächst eine Join-Transformation anzuwenden oder die Eingabe zu filtern.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Bestimmt, wie die Namen der ausgewählten Felder angezeigt werden. Da der Frame-Name normalerweise für tabellarische Daten besser ist" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Partitionieren nach Werten funktioniert nur mit einem einzelnen Frame.", "tooltip-keeps-partition-fields-frames": "Behält die Partitionsfelder in den Frames" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Info" + "label-info": "Info", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "und", @@ -12035,6 +12815,9 @@ "label-operation": "Operation" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Berechnungen", "label-include-time": "Zeit einbeziehen", "label-labels-to-fields": "Labels zu Feldern", @@ -12049,12 +12832,23 @@ "series-to-rows": "Reihen zu Zeilen" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Statistik auswählen" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Wert oder Variable", "variable-regex-documentation": "Formatiert eine Variable mit mehreren Werten in einen Regex-String" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Kubisch", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Anzahl der zu prognostizierenden X-, Y-Punkte" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Übereinstimmung", "label-replace": "Ersetzen", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Feld", - "label-mode": "Modus" + "label-mode": "Modus", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Feld", "label-reverse": "Rückwärts", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Feld auswählen" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transformieren" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat.", "label-time-field": "Zeitfeld", @@ -12121,10 +12960,34 @@ "placeholder-value": "Wert", "tooltip-name-for-value-fields": "Name für Wertefelder" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Operation", "placeholder-field": "Feld" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Mehrere Wertefelder jeder Art", "single-frame": "Einzelner Frame", @@ -12177,6 +13040,7 @@ "title": "Organisationen" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Nutzerauswahl", "select-placeholder": "Tippen Sie, um nach einem Nutzer zu suchen" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Variable löschen" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Aktualisieren" }, "query-variable-sort-select": { - "description-values-variable": "Wie die Werte dieser Variable sortiert werden" + "description-values-variable": "Wie die Werte dieser Variable sortiert werden", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "Standardwert, falls vorhanden", "text-options": "Textoptionen" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Optionaler Anzeigename", "description-template-variable-characters": "Der Name der Vorlagenvariable. (Max. 50 Zeichen)", "general": "Allgemein", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Beschreibender Text", "placeholder-label-name": "Label-Name", "placeholder-variable-name": "Variablenname", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Variable duplizieren", "tooltip-remove-variable": "Variable entfernen" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Zwischen allen Werten schalten" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Nutzungen anzeigen" }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f9b3241693b..804e7c186c3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Some features are stable (GA) and enabled by default, whereas some are currently in their preliminary Beta phase, available for early adoption.", "confirm-modal-body-2": "We advise understanding the implications of each feature change before making modifications.", + "confirmText-save-changes": "Save changes", "get-stage-cell": { "beta": "Beta", "content-general-availability": "General availability", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Delete org", + "confirmText-delete": "Delete", "title-delete": "Delete" }, "anon-users": { "not-found": "No anonymous users found." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "Are you sure you want to force logout from all devices?", + "confirmText-confirm-logout": "Confirm logout", + "confirmText-force-logout": "Force logout", "title-force-logout-from-all-devices": "Force logout from all devices" }, + "change-org-button": { + "confirmText-save": "Save" + }, "edit-org": { "access-denied": "You do not have permission to see users in this organization. To update this organization, contact your server administrator.", "heading": "Edit Organization", @@ -208,9 +216,11 @@ "not-editable": "This user's role is not editable because it is synchronized from your auth provider. Refer to the <1>Grafana authentication docs for details." }, "org-users-table": { + "body-delete": "Are you sure you want to delete user {{user}}?", "columns": { "aria-label-role": "Role" }, + "confirmText-delete": "Delete", "delete-aria-label": "Delete user: {{name}}", "title-delete": "Delete" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "These system settings are defined in grafana.ini or custom.ini (or overridden in ENV variables). To change these you currently need to restart Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "Confirm removal" + }, "upgrade-info": { "title": "Enterprise license" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Change", + "confirmText-change": "Change", "grafana-admin-key": "Grafana Admin", "grafana-admin-no": "No", "grafana-admin-yes": "Yes", "title": "Permissions" }, "user-profile": { + "body-delete": "Are you sure you want to delete this user?", + "body-disable": "Are you sure you want to disable this user?", + "confirmText-delete-user": "Delete user", + "confirmText-disable-user": "Disable user", "delete-button": "Delete user", "disable-button": "Disable user", "edit-button": "Edit", @@ -312,6 +330,9 @@ "title-delete-user": "Delete user", "title-disable-user": "Disable user" }, + "user-profile-row": { + "confirmText-save": "Save" + }, "user-sessions": { "browser-column": "Browser and OS", "force-logout-all-button": "Force logout from all devices", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Active timings" }, + "ad": { + "close": "Close", + "learn-more": "Learn more" + }, "add-button": { "add-more": "Add more" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Muting, grouping and timings (optional)", "title-muting-grouping-and-timings": "Muting, grouping, and timings" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "No datasources found" + }, "alert-menu": { "copy-link": "Copy link", "duplicate": "Duplicate", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "Alert rule" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Edit YAML", @@ -546,6 +577,7 @@ "view-configuration": "View configuration" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "Yes, reset configuration", "gma-manual-configuration-description": "The internal Grafana Alertmanager configuration cannot be manually changed. To change this configuration, edit the individual resources through the UI.", "gma-manual-configuration-is-not-supported": "Manual configuration changes not supported", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Resetting Alertmanager configuration" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "Are you sure you want to restore the configuration to this version? All unsaved changes will be lost.", "columns": { "compare": "Compare", "restore": "Restore", "text-latest": "Latest" }, + "confirmText-yes-restore-configuration": "Yes, restore configuration", "loading": "Loading...", "no-previous-configurations": "No previous configurations", "this-might-take-a-while": "This might take a while...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "More actions for contact point \"{{contactPointName}}\"", + "ariaLabel-delete": "Delete", "button-edit": "Edit", "button-view": "View", + "export-ariaLabel-export": "Export", "export-label-export": "Export", "label-delete": "Delete", "label-manage-permissions": "Manage permissions", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Disable resolved message" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "Loading groups...", "message": { "must-be-a-positive-integer": "Must be a positive integer.", "must-enter-a-group-name": "Must enter a group name" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Error loading data source details", "edit": "Edit", - "evaluations-to-fire": "Evaluation cycles to fire", "export": "Export", "folder": "Folder", "group-loading-error": "Error loading the group", "interval": "Interval", "namespace": "Namespace", - "pending-period": "Pending period", - "recording": "Recording", - "rule-name": "Rule name" + "new": "New" }, "group-edit": { "ds-error": "Error loading data source details", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "Unify on-call, alerting, and incident response with Grafana Cloud IRM.", + "irm-card-item-1": "Manage on-call schedules with your calendar or Terraform.", + "irm-card-item-2": "Respond to incidents via web, app, Slack, or other channels.", + "irm-card-item-3": "Pinpoint root causes with AI-powered Grafana SIFT.", + "irm-card-item-4": "Analyze past incidents to improve response and resilience.", + "irm-card-title": "Incident response and management", "label-get-started": "Get started", "label-insights": "Insights", + "synthetic-monitoring-card-description": "Monitor critical user flows, websites, and APIs externally, from global locations.", + "synthetic-monitoring-card-item-1": "Simulate end-to-end user journeys with browser checks.", + "synthetic-monitoring-card-item-2": "Run ping, DNS, HTTP/S, and TCP checks at every network layer.", + "synthetic-monitoring-card-item-3": "Use 20+ global probes or private probes behind your firewall.", + "synthetic-monitoring-card-item-4": "Track SLOs with built-in Prometheus-style alerts — right from the UI.", + "synthetic-monitoring-card-title": "Synthetic Monitoring", "title-alerting": "Alerting" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Other data sources" } } - } + }, + "noOptionsMessage-no-datasources-found": "No datasources found" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "Are you sure you would like to delete \"{{muteTiming}}\"?" }, "mute-timing-actions-buttons": { "text-disabled": "Disabled", @@ -2079,9 +2129,11 @@ "update-errors": { "conflict": "The notification policy tree has been updated by another user.", "error-code": "Error message: \"{{error}}\"", - "fallback": "Something went wrong when updating your notification policies.", + "routes": { + "conflictingMatchers": "Cannot add or update route: matchers conflict with an external routing tree if we merged matchers {{-matchers}}. This would make the route unreachable." + }, "suffix": "Please refresh the page and try again.", - "title": "Error saving notification policy" + "title": "Failed to add or update notification policy" } }, "policy": { @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Add query", "body-queries-expressions-configured": "Create at least one query or expression to be alerted on", + "confirmText-deactivate": "Deactivate", "expressions": "Expressions", "loading-data-sources": "Loading data sources...", "manipulate-returned-queries-other-operations": "Manipulate data returned from queries with math and other operations.", @@ -2184,6 +2237,9 @@ "label-export-all": "Export all" } }, + "recording-rule": { + "term": "Recording rule" + }, "recording-rule-editor": { "error-no-query-editor": "Could not load query editor due to: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "You will need to set a new evaluation group for the copied rule because the original one has been provisioned and cannot be used for rules created in the UI.", "body-not-provisioned": "The new rule will <1>not be marked as a provisioned rule.", + "confirmText-copy": "Copy", "title-copy-provisioned-alert-rule": "Copy provisioned alert rule" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Inspect Alert rule" }, "rule-list": { - "cannot-find-rule-details-for": "Cannot find rule details for UID {{uid}}", - "cannot-load-rule-details-for": "Cannot load rule details for UID {{uid}}", "configure-datasource": "Configure", "draft-new-rule": "Draft a new rule", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Not finding the data source you want? Some data sources are not supported for alerting. Click on the icon for more information." }, + "settings": { + "tabs": { + "alert-managers": "Alert managers" + } + }, "settings-content": { "add-new-alertmanager": "Add new Alertmanager", "builtin-alertmanager": "Built-in Alertmanager", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Choose notification template", "loading": "Loading...", "template-options": { + "ariaLabel": { + "select-notification-template": "Select notification template" + }, "label": { "select-notification-template": "Select notification template" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Actions", + "body-delete-template-group": "Are you sure you want to delete template group \"{{template}}\"?", + "confirmText-yes-delete": "Yes, delete", "no-templates-defined": "No templates defined.", "template-group": "Template group", "title-delete-template-group": "Delete template group" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} ago)" }, + "titles": { + "group-view": { + "subtitle": "Manage alert rules, recording rules and evaluation interval" + } + }, "to-gma": { "confirm-modal": { "body": "The target folder is not empty, some rules may be overwritten or removed. Are you sure you want to import these alert rules to Grafana-managed rules?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Delete contact point" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "Yes, delete" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Delete notification policy" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Annotation", "first-value": "First value", - "from": "From" + "from": "From", + "noOptionsMessage-unknown-field-names": "Unknown field names" }, "empty-state": { "button-title": "Add annotation query", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Checkout the <2>Annotations documentation for more information.", "title": "There are no custom annotation queries added yet" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "When this field is defined, the annotation will be treated as a range", + "results-split-on-comma": "The results will be split on comma (,)" + }, + "placeholder": { + "text-or-the-first-field": "{{defaultField}}, or the first text field", + "time-or-the-first-field": "{{defaultField}}, or the first time field" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} events (from {{numFields}} fields)", "no-events-found": "No events found", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Define allowed teams IDs", "display-name-description": "Will be displayed on the login page as \"Sign in with ...\". Helpful if you use more than one identity providers or SSO protocols.", "display-name-label": "Display name", + "domain-hint-description": "Parameter to indicate the realm of the user in the Azure AD/Entra ID tenant and streamline the login process.", + "domain-hint-label": "Domain hint", + "domain-hint-valid-domain": "This field must be a valid domain.", "email-attribute-name-description": "Name of the key to use for user email lookup within the attributes map of OAuth2 ID token.", "email-attribute-name-label": "Email attribute name", "email-attribute-path-description": "JMESPath expression to use for user email lookup from the user information.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Integer list of Team IDs.", "team-ids-label": "Team IDs", "team-ids-numbers": "Team IDs must be numbers.", - "team-ids-other": "String list of Team Ids.", + "team-ids-other": "String list of Team IDs.", "team-ids-placeholder": "Enter Team IDs and press Enter to add", "teams-url-description": "The URL used to query for Team IDs. If not set, the default value is /teams.", "teams-url-description-oauth": "If you configure \"{{ teamsURLLabel }}\", you must also configure \"{{ teamIDsAttributePathLabel }}\".", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Reset to default values" }, + "confirmText-reset": "Reset", "disable": "Disable", "disabling": "Disabling...", "discard": "Discard", @@ -3473,11 +3563,11 @@ "button-create": "Create", "button-creating": "Creating...", "cancel": "Cancel", - "error-invalid-folder-name": "Invalid folder name", "error-required": "Folder name is required", "folder-name-input-placeholder-enter-folder-name": "Enter folder name", "label-folder-name": "Folder name", "text-pull-request-created": "A pull request has been created with changes to this folder:", + "text-your-folder-will-be-created-as": "Your folder will be created as {{folderName}}", "title-pull-request-created": "Pull request created", "title-this-repository-is-read-only": "This repository is read only" }, @@ -3894,7 +3984,8 @@ "close-button": { "tooltip": "Close" }, - "cloud-enterprise-feature-badge": "Cloud & Enterprise ", + "cloud-enterprise-feature-badge": "Cloud & Enterprise", + "cloud-feature-badge": "Cloud", "combobox": { "async": { "error": "An error occurred while loading options." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Dynamically calculates interval by dividing time range by the count specified" + "variable-editor-form": { + "run-query": "Run query" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "Proceed" + }, "title": { "proceed-to-external-site": "Proceed to external site?" } }, "render-left-actions": { - "text-public": "Public", "tooltip-view-as-scene": "View as Scene" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Failed to load dashboard" }, + "export": { + "button": { + "label": "Export dashboard" + }, + "menu": { + "label": "Export dashboard menu" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Failed to fetch dashboard" @@ -4540,6 +4641,13 @@ "editable": "Editable", "readonly": "Read-only" } + }, + "graph_tooltip_options": { + "label": { + "default": "Default", + "shared-crosshair": "Shared crosshair", + "shared-tooltip": "Shared tooltip" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Transform data" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "Do not send annotation data to the following panels", + "annotation-panels-support-annotations": "Send the annotation data to all panels that support annotations", + "annotations-explicitly-listed-panels": "Send the annotations to the explicitly listed panels" + }, + "label": { + "all-panels": "All panels", + "all-panels-except": "All panels except", + "selected-panels": "Selected panels" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Are you sure you want to restore the dashboard to version {{version}}? All unsaved changes will be lost.", + "confirmText-restore-version": "Yes, restore to version {{version}}", "title-restore-version": "Restore version" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "This title is not unique" } }, + "save-dashboard": { + "message-dashboard-saved": "Dashboard saved" + }, "save-dashboard-as-button": { "save-as": "Save as" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "A dashboard with the same name in selected folder already exists.<1><2>Would you still like to save this dashboard?", "body-version-mismatch": "Someone else has updated this dashboard<1><2>Would you still like to save this dashboard?", + "confirmText-save-and-overwrite": "Save and overwrite", "title-name-exists": "Conflict", "title-version-mismatch": "Conflict" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Go to next dashboard", "playlist-previous": "Go to previous dashboard", "playlist-stop": "Stop playlist", - "public-dashboard": "Public", "refresh": "Refresh dashboard", "save": "Save dashboard", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Apply transformation to" }, "transformation-operation-row": { + "body-delete": "Note that removing one transformation may break others. If there is only a single transformation, you will go back to the main selection screen.", "render-actions": { + "confirmText-delete": "Delete", "title-debug": "Debug", "title-disable-transformation": "Disable transformation", "title-filter": "Filter", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Search for transformation", "show-images": "Show images", - "title-add-another-transformation": "Add another transformation" + "title-add-another-transformation": "Add another transformation", + "view-all": "View all" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "By deleting all transformations, you will go back to the main selection screen." }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Add another transformation" }, + "confirmText-delete-all": "Delete all", "delete-all-transformations": "Delete all transformations", "title-delete-all-transformations": "Delete all transformations?", "tooltip-clear-search": "Clear search", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Toggle selection of version {{version}}", "date": "Date", + "name-latest": "Latest", "notes": "Notes", "restore": "Restore", "updated-by": "Updated by", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Enables users to add custom values to the list", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Provide dimensions as CSV: {{name}}, {{value}}", "label-data-source": "Data source", - "label-use-static-key-dimensions": "Use static key dimensions" + "label-use-static-key-dimensions": "Use static key dimensions", + "name-allow-custom-values": "Allow custom values" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "Revoke public URL" + }, "title": { "revoke-public-url": "Revoke public URL" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Custom options", + "name-values-separated-comma": "Values separated by comma", "selection-options": "Selection options" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Type", "label-url": "URL", "label-with-tags": "With tags", + "link-type-options": { + "label": { + "dashboards": "Dashboards", + "link": "Link" + } + }, "placeholder-open-dashboard": "Open dashboard" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Type" }, "dashboard-preview-banner": { - "not-saved": "The value is not yet saved in the Grafana database", "not-yet-saved": "The value is not saved in the Grafana database", - "open-pull-request-in-git-hub": "Open pull request in GitHub", - "title-dashboard-loaded-branch-git-hub": "This dashboard is loaded from a branch in GitHub.", "title-dashboard-loaded-external-repository": "This dashboard is loaded from an external repository", - "title-dashboard-loaded-request-git-hub": "This dashboard is loaded from a pull request in GitHub.", - "title-error-loading-dashboard": "Error loading dashboard", - "value-not-saved": "The value is not yet saved in the Grafana database", - "view-pull-request-in-git-hub": "View pull request in GitHub" + "title-error-loading-dashboard": "Error loading dashboard" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Data source options", "description-instance-name-filter": "Regex filter for which data source instances to choose from in the variable value list. Leave empty for all.", "example-instance-name-filter": "Example: ", + "name-instance-name-filter": "Instance name filter", + "name-type": "Type", "selection-options": "Selection options" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Add transformation" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "Editable", + "readonly": "Read-only" + } + }, + "graph_tooltip_options": { + "label": { + "default": "Default", + "shared-crosshair": "Shared crosshair", + "shared-tooltip": "Shared tooltip" + } + } + }, "get-edit-options": { "title": { "column-options": "Column options", "row-height-options": "Row height options" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "Add key/value filters on the fly", + "dynamically-switch-source-multiple-panels": "Dynamically switch the data source for multiple panels", + "group": "Add keys to group by on the fly", + "hidden-constant-variable": "A hidden constant variable, useful for metric prefixes in dashboards you want to share", + "users-enter-arbitrary-strings-textbox": "Users can enter any arbitrary strings in a textbox", + "values-are-static-and-defined-manually": "Values are static and defined manually", + "values-fetched-source-query": "Values are fetched from a data source query", + "values-timespans": "Values are timespans, ex 1m, 1h, 1d" + }, + "name": { + "ad-hoc-filters": "Ad hoc filters", + "constant": "Constant", + "custom": "Custom", + "data-source": "Data source", + "group-by": "Group by", + "interval": "Interval", + "query": "Query", + "textbox": "Textbox" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "Do not send annotation data to the following panels", + "annotation-panels-support-annotations": "Send the annotation data to all panels that support annotations", + "annotations-explicitly-listed-panels": "Send the annotations to the explicitly listed panels" + }, + "label": { + "all-panels": "All panels", + "all-panels-except": "All panels except", + "selected-panels": "Selected panels" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Provide dimensions as CSV: {{name}}, {{value}}", "group-by-options": "Group by options", "label-data-source": "Data source", - "label-use-static-group-by-dimensions": "Use static group dimensions" + "label-use-static-group-by-dimensions": "Use static group dimensions", + "name-allow-custom-values": "Allow custom values" }, "help-wizard": { "copy-to-clipboard": "Copy to clipboard", @@ -5521,9 +5710,14 @@ "apply": "Apply" }, "interval-variable-form": { + "description-auto-option": "Dynamically calculates interval by dividing time range by the count specified", "description-calculated-value-below-threshold": "The calculated value will not go below this threshold", "description-step-count": "How many times the current time range should be divided to calculate the value", - "interval-options": "Interval options" + "interval-options": "Interval options", + "name-auto-option": "Auto option", + "name-min-interval": "Min interval", + "name-step-count": "Step count", + "name-values": "Values" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Used on {{count}} dashboards", "usage-count_other": "Used on {{count}} dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "Failed to move dashboard", + "cancel-action": "Cancel", + "current-file-not-found": "Current dashboard file could not be found", + "drawer-title": "Move Provisioned Dashboard", + "file-load-error": "Error loading dashboard", + "loading-file-data": "Loading dashboard data", + "move-action": "Move dashboard", + "move-read-only-message": "This dashboard cannot be moved directly from Grafana because the repository is read-only. To move this dashboard, please move the file in your Git repository.", + "moving": "Moving...", + "partial-failure-warning": "Dashboard was created at new location but could not be deleted from original location. Please manually remove the old file.", + "target-path-label": "Target path", + "title-this-repository-is-read-only": "This repository is read only" + }, "name-already-exists-error": { "body-name-already-exists": "A dashboard with the same name in selected folder already exists. Would you still like to save this dashboard?", "title-name-already-exists": "Name already exists" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "Proceed" + }, "title": { "proceed-to-external-site": "Proceed to external site?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Add another transformation", + "body-delete-all-transformations": "By deleting all transformations, you will go back to the main selection screen.", + "confirmText-delete-all": "Delete all", "delete-all-transformations": "Delete all transformations", "title-delete-all-transformations": "Delete all transformations?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples).", "description-optional": "Optional, if you want to extract part of a series name or metric node segment.", "label-data-source": "Data source", + "label-static-options-sort": "Static options sort", "label-target-data-source": "Target data source", + "label-use-static-options": "Use static options", + "name-regex": "Regex", "query-options": "Query options", - "selection-options": "Selection options" + "selection-options": "Selection options", + "static-options-legend": "Static options" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Are you sure you want to restore the dashboard to version {{version}}? All unsaved changes will be lost.", + "confirmText-restore-version": "Yes, restore to version {{version}}", "title-restore-version": "Restore version" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Folder", "render-footer": { "title-failed-to-save-dashboard": "Failed to save dashboard" - } + }, + "required": "Required" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Enables multiple values to be selected at the same time", "description-enables-option-include-variables": "Enables an option to include all values", - "description-enables-users-custom-values": "Enables users to add custom values to the list" + "description-enables-users-custom-values": "Enables users to add custom values to the list", + "name-allow-custom-values": "Allow custom values", + "name-custom-all-value": "Custom all value", + "name-include-all-option": "Include All option", + "name-multi-value": "Multi-value" }, "share-button": { "aria-label-sharedropdownmenu": "Toggle share menu" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Copy to clipboard failed" } }, + "text-box-variable": { + "name-default-value": "Default value" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(optional)", "text-options": "Text options" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "If you unlink this panel, you will be able to edit it without affecting any other dashboards. However, once you make a change you will not be able to revert to its original reusable panel.", + "confirmText-yes-unlink": "Yes, unlink", "title-really-unlink-panel": "Do you really want to unlink this panel?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "Dashboard saved" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "This variable is not referenced by any variable or dashboard.", "aria-label-variable-referenced-other-variables-dashboard": "This variable is referenced by other variables or dashboard.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Variable editor form", "back-to-list": "Back to list", + "confirmText": { + "delete-variable": "Delete variable" + }, "delete": "Delete", "description-optional-display-name": "Optional display name", "description-template-variable-characters": "The name of the template variable. (Max. 50 characters)", "general": "General", + "name-description": "Description", + "name-label": "Label", + "name-name": "Name", "placeholder-descriptive-text": "Descriptive text", "placeholder-label-name": "Label name", "placeholder-variable-name": "Variable name", @@ -5829,13 +6066,25 @@ "variable": "Variable" }, "variable-editor-list-row": { + "body-delete-variable": "Are you sure you want to delete: {{variable}}?", + "confirmText-delete-variable": "Delete variable", "title-delete-variable": "Delete variable", "tooltip-duplicate-variable": "Duplicate variable", "tooltip-remove-variable": "Remove variable" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "Label", + "nothing": "Nothing", + "variable": "Variable" + } + }, "label": "Hide" }, + "variable-type-select": { + "name-variable-type": "Variable type" + }, "variable-usages-button": { "title-show-usages": "Showing usages for: {{variableId}}", "tooltip-show-usages": "Show usages" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Toggle selection of version {{version}}", "date": "Date", + "name-latest": "Latest", "notes": "Notes", "restore": "Restore", "updated-by": "Updated by", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "Fixed color" + "label-fixed-color": "Fixed color", + "noOptionsMessage-no-fields-found": "No fields found" }, "file-dropzone-custom-children": { "upload": "Upload" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limit", "label-value": "Value", + "noOptionsMessage-no-fields-found": "No fields found", "scalar-options": { "description-clamped": "Use field values, clamped to max and min", "description-mod": "Use field values, mod from max", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "Fixed value" + } + }, "label-max": "Max", "label-min": "Min", - "label-value": "Value" + "label-value": "Value", + "noOptionsMessage-no-fields-found": "No fields found" }, "text-dimension-editor": { "description-field": "Display field value", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Select service name operator", "aria-label-select-span-name": "Select span name", "aria-label-select-span-name-operator": "Select span name operator", + "ariaLabel-select-max-span-duration": "Select max span duration", + "ariaLabel-select-min-span-duration": "Select min span duration", "label-collapse": "Span Filters", "label-duration": "Duration", "label-service-name": "Service name", @@ -6939,6 +7199,8 @@ "split-widen": "Widen pane" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "Copy Trace ID", + "ariaLabel-export-trace": "Export Trace", "give-feedback": "Give feedback", "label-copied": "Copied!", "label-export": "Export", @@ -7046,6 +7308,7 @@ "when": "WHEN" }, "expression-query-editor": { + "helper-text-sql": "Run MySQL-dialect SQL against the tables returned from your data sources. Data source queries (ie \"A\", \"B\") are available as tables and referenced by query-name. Fields are available as columns, as returned from the data source.", "label-operation": "Operation" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Clear folders", + "noOptionsMessage-no-folders-found": "No folders found", "select-aria-label": "Folder filter", "select-placeholder": "Filter by folder" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Sorry, I was unable to complete your request. Please try again.", "send-custom-feedback": "Send" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "Gazetteer not found", + "warning-no-geometry": "unable to find geometry", + "warning-select-geohash": "Select geohash field", + "warning-select-lat-long": "Select latitude/longitude fields", + "warning-select-lookup": "Select lookup field", + "warning-unable-to-find": "Unable to find location fields" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "No strings fields found" + }, + "latitude-field": { + "no-fields-message": "No numeric fields found" + }, + "longitude-field": { + "no-fields-message": "No numeric fields found" + }, + "lookup-field": { + "no-fields-message": "No strings fields found" + }, + "name-gazetteer": "Gazetteer", + "name-geohash-field": "Geohash field", + "name-latitude-field": "Latitude field", + "name-location-mode": "Location Mode", + "name-longitude-field": "Longitude field", + "name-lookup-field": "Lookup field" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "Automatically identify location data based on default field names", + "description-coords": "Specify latitude and longitude fields", + "description-geohash": "Specify geohash field", + "description-lookup": "Specify Gazetteer and lookup field", + "label-auto": "Auto", + "label-coords": "Coords", + "label-geohash": "Geohash", + "label-lookup": "Lookup" + } + } + }, "geomap": { + "category-basemap-layer": "Basemap layer", + "category-map-controls": "Map controls", + "category-map-layers": "Map layers", + "category-map-view": "Map view", "coordinates-map-view-editor": { "label-latitude": "Latitude", "label-longitude": "Longitude" @@ -7153,6 +7463,14 @@ "center": "Center:", "zoom": "Zoom:" }, + "description-initial-view": "This location will show when the panel first loads.", + "description-mouse-wheel-zoom": "Enable zoom control via mouse wheel", + "description-share-view": "Use the same view across multiple panels. Note: this may require a dashboard reload.", + "description-show-attribution": "Show the map source attribution info in the lower right", + "description-show-debug": "Show map info", + "description-show-measure": "Show tools for making measurements on the map", + "description-show-scale": "Indicate map scale", + "description-show-zoom": "Show zoom control buttons in the upper left corner", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Layer" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Add geomap style rule" }, + "layer-editor": { + "category-base-layer": "Base layer", + "description-display-tooltip": "Show the tooltip for layer", + "name-data": "Data", + "name-display-tooltip": "Display tooltip", + "name-layer-type": "Layer type", + "name-opacity": "Opacity" + }, "layers-editor": { "label-add-layer": "Add layer", "no-layers": "No layers?" @@ -7185,16 +7511,38 @@ "label-zoom": "Zoom", "use-current-map-settings": "Use current map settings" }, + "markers-layer": { + "description-show-legend": "Show map legend", + "name-show-legend": "Show legend", + "name-styles": "Styles" + }, "markers-legend": { "title-symbol": "Symbol" }, "measure-overlay": { "tooltip-show-measure-tools": "Show measure tools" }, + "name-initial-view": "Initial view", + "name-mouse-wheel-zoom": "Mouse wheel zoom", + "name-share-view": "Share view", + "name-show-attribution": "Show attribution", + "name-show-debug": "Show debug", + "name-show-measure": "Show measure tools", + "name-show-scale": "Show scale", + "name-show-zoom": "Show zoom control", + "name-tooltip": "Tooltip", + "photos-layer": { + "noFieldsMessage-no-string-fields": "No string fields found" + }, "plugin": { "basemap-layer-configured-server-admin": "The basemap layer is configured by the server admin." }, "style-editor": { + "horizontal-align-options": { + "label-center": "Center", + "label-left": "Left", + "label-right": "Right" + }, "label-align": "Align", "label-baseline": "Baseline", "label-color": "Color", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Symbol vertical align", "label-text-label": "Text label", "label-x-offset": "X offset", - "label-y-offset": "Y offset" + "label-y-offset": "Y offset", + "placeholderText-select-symbol": "Select a symbol", + "placeholderText-select-symbol-or-add-text": "Select a symbol or add a text label", + "vertical-align-options": { + "label-bottom": "Bottom", + "label-center": "Center", + "label-top": "Top" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Comparison operator", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Feature property", "placeholder-numeric-value": "Numeric value", "placeholder-value": "value" + }, + "tooltip-options": { + "description-details": "Show popup on hover", + "description-none": "Show contents on click, not hover", + "label-details": "Details", + "label-none": "None" + }, + "utils": { + "get-next-layer-name": "Layer {{name}}" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "{{colorLabel}} color" }, "confirm-button": { - "cancel": "Cancel" + "cancel": "Cancel", + "confirmText-delete": "Delete" }, "confirm-content": { "placeholder": "Type \"{{confirmPromptText}}\" to confirm" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "toggle collapse panel", + "ariaLabel-panel-loading": "Panel loading bar", + "ariaLabel-panel-status": "Panel status", "tooltip-cancel": "Cancel query", "tooltip-cancel-loading": "Cancel query", "tooltip-stop-streaming": "Stop streaming", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Match case", "inspect-drawer-title": "Inspect value", "inspect-menu-label": "Inspect value", + "nested-table": { + "no-data": "No data" + }, "no-values-label": "No values", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Add", + "placeholder-new-tag": "New tag (enter key to add)", "remove": "Remove tag: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Click to {{actionTitle}}", "footer-click-to-navigate": "Click to open {{linkTitle}}", "timestamp": "Timestamp" + }, + "week-start-picker": { + "weekStarts-label-default": "Default", + "weekStarts-label-monday": "Monday", + "weekStarts-label-saturday": "Saturday", + "weekStarts-label-sunday": "Sunday" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Create library panel" }, + "change-library-panel-modal": { + "confirmText-change": "Change", + "confirmText-replace": "Replace" + }, "confirm": { "delete-panel": "Do you want to delete this panel?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Used on {{count}} dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Search by name or description" + "placeholder-search-by-name-or-description": "Search by name, description or folder name" }, "loading-indicator": { "loading-library-panel": "Loading library panel..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Create a personalized, direct link to share your panel within your organization, with the following customization settings:", "download-image": "Download image", - "render-image": "Generate image", - "render-image-error": "Failed to render panel image", - "render-image-error-description": "An error occurred when generating the image" + "render-image": "Generate image" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "Experimental. Request more results by scrolling to the bottom of the logs list.", "description-enable-syntax-highlighting": "Use a predefined syntax coloring grammar to highlight relevant parts of the log lines", "description-show-controls": "Display controls to jump to the last or first log line, and filters by log level", + "fields": { + "type": { + "loki": { + "indexed-label_one": "Indexed label", + "indexed-label_other": "Indexed labels", + "parsedl-label_one": "Parsed field", + "parsedl-label_other": "Parsed fields", + "structured-metadata_one": "Structured metadata", + "structured-metadata_other": "Structured metadata" + } + } + }, "font-size-options": { "label-default": "Default", "label-small": "Small" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Wrap lines" }, "log-details": { - "close": "Close log details", "fields": "Fields", "links": "Links", "log-line": "Log line", @@ -8405,6 +8795,42 @@ "show-more": "show more", "tooltip-error": "Error: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "Clear", + "close": "Close log details", + "copy-shortlink": "Copy shortlink", + "copy-to-clipboard": "Copy to clipboard", + "displayed-fields-section": "Organize displayed fields", + "fields": { + "adhoc-statistics": "Ad-hoc statistics", + "copy-value-to-clipboard": "Copy value to clipboard", + "filter-out": "Filter out value", + "filter-out-query": "Filter out value in query {{query}}", + "toggle-field-button": { + "field-instead-message": "Show this field instead of the message", + "hide-this-field": "Hide this field" + } + }, + "fields-section": "Fields", + "hide-log-line": "Hide log line", + "inline-mode": "Display inline", + "links-section": "Links", + "log-line-field": "Log line", + "log-line-section": "Log line", + "move-displayed-field-down": "Move down", + "move-displayed-field-up": "Move up", + "no-details": "No fields to display.", + "pin-line": "Pin log", + "remove-displayed-field": "Remove field", + "search": { + "no-results": "No results to display." + }, + "search-placeholder": "Search field names and values", + "show-context": "Show context", + "show-log-line": "Show log line", + "sidebar-mode": "Anchor to the right", + "unpin-line": "Unpin log" + }, "log-line-menu": { "copy-link": "Copy link to log line", "copy-log": "Copy log line", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "Common labels", "name-deduplication": "Deduplication", + "name-details-mode": "Log Details panel mode", + "name-details-options": { + "label-inline": "Inline", + "label-sidebar": "Sidebar" + }, "name-enable-infinite-scrolling": "Enable infinite scrolling", "name-enable-log-details": "Enable log details", "name-enable-syntax-highlighting": "Enable syntax highlighting", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Data links", "aria-label-fields": "Fields", + "aria-label-line": "Log line", "aria-label-log-level": "Log level", "aria-label-no-details": "No details" }, @@ -8598,6 +9030,8 @@ "updated-on": "Updated on" }, "snapshot-list-table": { + "body-delete": "Are you sure you want to delete '{{snapshotToRemove}}'?", + "confirmText-delete": "Delete", "title-delete": "Delete" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Import dashboard" } + }, + "validation-srv": { + "message-name-required": "Name is required", + "message-reserved-name": "This is a reserved name and cannot be used for a folder.", + "message-same-name": "A dashboard or a folder with the same name already exists", + "message-same-name-general": "A folder or dashboard in the general folder with the same name already exists" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "No options found" + }, "migrate-to-cloud": { "build-snapshot": { "description": "This tool can migrate some resources from this installation to your cloud stack. To get started, you'll need to create a snapshot of this installation. Creating a snapshot typically takes less than two minutes. The snapshot is stored alongside this Grafana installation.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "Maximum number of alert rule groups reached: Delete some alert rule groups or upgrade your plan and try again.", + "alert-rules-quota-reached": "Maximum number of alert rules reached: Delete some alert rules or upgrade your plan and try again.", "dashboard-already-managed": "Dashboard is already provisioned and managed by Grafana in the cloud instance. We recommend using the provisioned dashboard going forward. If you still wish to copy the dashboard to the cloud instance, then change the dashboard ID in the dashboard JSON, save a new snapshot and upload again.", "datasource-already-managed": "Data source is already provisioned and managed by Grafana in the cloud instance. If this is a different resource, set another UID and try again.", "datasource-invalid-url": "There is a data source which has an invalid URL. Provide a valid URL and try again.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": ">100 nodes", "aria-label-hidden-marker": "Hidden nodes marker: {{marker}}", - "node-count_one": "{{count}} node", + "node-count_one": "{{count}} nodes", "node-count_other": "{{count}} nodes" }, "node": { @@ -9317,10 +9762,10 @@ "aria-label-layered-layout-performance-warning": "Layered layout performance warning", "aria-label-nodes-hidden-warning": "Nodes hidden warning", "computing-layout": "Computing layout", - "hidden-nodes_one": "<0> {{count}} node is hidden for performance reasons.", + "hidden-nodes_one": "<0> {{count}} nodes are hidden for performance reasons.", "hidden-nodes_other": "<0> {{count}} nodes are hidden for performance reasons.", "no-data": "No data", - "processed-nodes_one": "<0> Layered layout may be slow with {{count}} node.", + "processed-nodes_one": "<0> Layered layout may be slow with {{count}} nodes.", "processed-nodes_other": "<0> Layered layout may be slow with {{count}} nodes." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "No organizations found", "select-placeholder": "Select organization" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "You do not have permission to see this page.", "title-access-denied": "Access denied" }, + "error-loading-plugin": "Plugin failed to load", "no-root-app-page-component-found": "No root app page component found" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "plugins updated" }, "versions": { - "confirmation-text-1": "Are you really sure you want to downgrade to version", - "confirmation-text-2": "You should normally not be doing this", + "confirmation-text": "Are you really sure you want to downgrade to version {{version}}? You should normally not be doing this", "downgrade-confirm": "Downgrade", "downgrade-title": "Downgrade plugin version" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "No plugins found" }, + "extensions": { + "extension-error-alert-description": "Check the console for more details on the error.", + "extension-error-alert-title": "Extension failed to load: \"{{pluginId}}/{{extensionTitle}}\"" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Updating" }, "install-controls-button": { - "title-uninstall-modal": "Uninstall {{plugin}}" + "title-uninstall-modal": "Uninstall {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "Are you sure you want to uninstall this plugin?", + "confirmText-confirm": "Confirm" + } }, "install-controls-warning": { "body-not-published": "This plugin is not published to <2>grafana.com/plugins and can't be managed via the catalog.", @@ -9971,6 +10425,15 @@ "label-workflow": "Workflow" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "The value is not yet saved in the Grafana database", + "open-pull-request-in-git-hub": "Open pull request in GitHub", + "view-pull-request-in-git-hub": "View pull request in GitHub" + }, + "title-created-branch-git-hub": "A new resource has been created in a branch in GitHub.", + "title-loaded-pull-request-git-hub": "This resource is loaded from a pull request in GitHub." + }, "provisioning": { "banner": { "message": "This feature is currently under active development. For the best experience and latest improvements, we recommend using the <2>nightly build of Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "No service accounts found", "select-aria-label": "Service account picker", "select-placeholder": "Start typing to search for service accounts" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Add service account token", + "body-delete-service-account": "Are you sure you want to delete this service account?", + "body-disable-service-account": "Are you sure you want to disable this service account?", + "confirmText-delete-service-account": "Delete service account", + "confirmText-disable-service-account": "Disable service account", "delete-service-account": "Delete service account", "disable-service-account": "Disable service account", "enable-service-account": "Enable service account", @@ -10907,6 +11375,7 @@ "used-by": "Used by" }, "service-account-profile-row": { + "confirmText-save": "Save", "edit": "Edit" }, "service-account-role-row": { @@ -10920,6 +11389,12 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Add service account", + "body-delete_one": "Are you sure you want to delete {{serviceAccountName}} and {{count}} accompanying tokens?", + "body-delete_other": "Are you sure you want to delete {{serviceAccountName}} and {{count}} accompanying tokens?", + "body-delete-with-tokens": "Are you sure you want to delete {{serviceAccountName}}?", + "body-disable-service-account": "Are you sure you want to disable '{{accountToDisable}}'?", + "confirmText-delete": "Delete", + "confirmText-disable-service-account": "Disable service account", "placeholder-search-service-account-by-name": "Search service account by name", "sub-title": "Service accounts and their tokens can be used to authenticate against the Grafana API. Find out more in our <2>documentation.", "title-delete-service-account": "Delete service account", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "Export as image", "export-json-title": "Export as JSON", "share-externally-title": "Share externally", "share-internally-title": "Share internally", @@ -10972,6 +11448,19 @@ "view-button": "View JSON", "view-button-yaml": "View YAML" }, + "image": { + "actions": "Image export actions", + "cancel-button": "Cancel", + "download-button": "Download image", + "error-title": "Failed to generate image", + "generate-button": "Generate image", + "generating": "Generating image...", + "info-text": "Save this dashboard as an image", + "preview": "Preview", + "preview-aria": "Generated image preview", + "preview-region": "Image preview", + "title": "Export as image" + }, "library": { "info": "Create library panel." }, @@ -10980,7 +11469,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 <2>Grafana image renderer plugin. Please contact your Grafana administrator to install the plugin.", + "render-instructions": "To render an 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.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "Save the dashboard to enable image generation", + "download-disabled-help": "Generate an image first to enable download", + "error-title": "Failed to generate image", + "form": { + "label": "Panel image settings" + }, "preview": { "title": "Panel preview" }, @@ -11315,7 +11810,7 @@ "label-never": "Never" }, "status-history-panel": { - "too-many-points_one": "Too many points to visualize properly. <1>Update the query to return fewer points. <3>({{count}} point received)", + "too-many-points_one": "Too many points to visualize properly. <1>Update the query to return fewer points. <3>({{count}} points received)", "too-many-points_other": "Too many points to visualize properly. <1>Update the query to return fewer points. <3>({{count}} points received)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "Display a single count for all data rows", "description-fields": "Select the fields that should be calculated", "description-min-column-width": "The minimum width for column auto resizing", + "description-wrap-header-text": "Enables text wrapping for column headers", "image-cell-options-editor": { "description-alt-text": "Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader", "description-title-text": "Text that will be displayed when the image is hovered by a cursor", @@ -11408,6 +11904,7 @@ "name-min-column-width": "Minimum column width", "name-show-table-footer": "Show table footer", "name-show-table-header": "Show table header", + "name-wrap-header-text": "Wrap header text", "placeholder-column-width": "auto", "placeholder-fields": "All Numeric Fields" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Tag option" }, "team-picker": { + "noOptionsMessage-no-teams-found": "No teams found", "select-aria-label": "Team picker", "select-placeholder": "Select a team" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Field(s) or number" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "Binary operation", + "reduce-row": "Reduce row", + "row-index": "Row index", + "unary-operation": "Unary operation" + } + }, + "label": { + "cumulative-functions": "Cumulative functions", + "window-functions": "Window functions" + }, "label-alias": "Alias", "label-mode": "Mode", "label-replace-all-fields": "Replace all fields" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Label", "label-name": "Name", + "name-modes": { + "label": { + "copy-frame-name-to-field": "Copy frame name to field name", + "ignore-the-frame-name": "Ignore the frame name", + "label-frame": "Add a label with the frame name" + } + }, "placeholder-frame": "Frame" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Add a convert field type transformer", "aria-label-remove-convert-field-type-transformer": "Remove convert field type transformer", + "convert-field-type": "Convert field type", + "description": { + "convert-to-specified-field-type": "Convert a field to a specified field type." + }, "label": { "browser": "Browser", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Input format", "label-join-with": "Join with", "label-set-timezone": "Set timezone", + "name": { + "convert-field-type": "Convert field type" + }, "placeholder-type": "Type", + "tags": { + "format-field": "Format field" + }, "tooltip-explicit-separator-joining-array-values": "Use an explicit separator when joining array values", "tooltip-input-format": "Specify the format of the input field so Grafana can parse the date string correctly.", "tooltip-specify-the-output-format": "Specify the output format.", @@ -11815,6 +12342,19 @@ "desc": "DESC", "off": "Off" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "Combine all fields into a single frame." + }, + "name": { + "concatenate-fields": "Concatenate fields" + }, + "tags": { + "combine": "Combine" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Add enum value", "generate-enum-values-from-data": "Generate enum values from data" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Delete" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "Parse fields from content (JSON, labels, etc)." + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "Select field" + } + }, "label-delimiter": "Delimiter", "label-format": "Format", "label-keep-time": "Keep time", "label-replace-all-fields": "Replace all fields", "label-source": "Source", + "name": { + "extract-fields": "Extract fields" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Select delimiter...", "tooltip-regexp": "Example: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Threshold color" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "Use a field value to lookup countries, states, or airports." + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "No text fields found" + }, + "placeholderText": { + "select-text-field": "Select text field" + } + }, "label-field": "Field", - "label-lookup": "Lookup" + "label-lookup": "Lookup", + "name": { + "lookup-fields-from-resource": "Lookup fields from resource" + } }, "field-to-config-mapping-editor": { "additional-settings": "Additional settings", @@ -11848,12 +12413,26 @@ "use-as": "Use as" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "Remove parts of the query results using a regex pattern." + }, "label-from-variable": "From variable", "label-identifier": "Identifier", "label-use-variable": "Use variable", "label-variable": "Variable", + "name": { + "filter-fields-by-name": "Filter fields by name" + }, "placeholder-regular-expression-pattern": "Regular expression pattern" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "Remove rows from the data based on origin query" + }, + "name": { + "filter-data-by-query": "Filter data by query" + } + }, "filter-by-value-filter-editor": { "label-field": "Field", "label-match": "Match", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Add condition", + "description": { + "remove-rows-query-results-user-defined-filters": "Remove rows from the query results using user-defined filters." + }, + "filter-match": { + "label": { + "match-all": "Match all", + "match-any": "Match any" + } + }, + "filter-types": { + "label": { + "exclude": "Exclude", + "include": "Include" + } + }, "label-conditions": "Conditions", - "label-filter-type": "Filter type" + "label-filter-type": "Filter type", + "name": { + "filter-data-by-values": "Filter data by values" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "No text fields found" + }, + "placeholderText": { + "select-text-field": "Select text field" + } + }, "label-field": "Field", "label-format": "Format", "label-substring-range": "Substring range" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "Manipulate string fields formatting." + }, + "name": { + "format-string": "Format string" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Set timezone", @@ -11878,6 +12491,70 @@ "tooltip-format": "The output format for the field specified as a <2>Moment.js format string.", "tooltip-timezone-manually": "Set the timezone of the date manually" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "Set the output format of a time field" + }, + "name": { + "format-time": "Format time" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "Use the row values to calculate a new field." + }, + "name": { + "add-field-from-calculation": "Add field from calculation" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "Set unit, min, max and more." + }, + "name": { + "config-from-query-results": "Config from query results" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "Generate heatmap data from source data." + }, + "name": { + "create-heatmap": "Create heatmap" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "Flatten labeled results into a table joined by labels." + }, + "name": { + "join-by-labels": "Join by labels" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "Split a one-frame dataset into multiple series." + }, + "name": { + "partition-by-values": "Partition by values" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "Convert each row into a field with dynamic config." + }, + "name": { + "rows-to-fields": "Rows to fields" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "Apply spatial operations to query results." + }, + "name": { + "spatial-operations": "Spatial operations" + } + }, "get-timezone-options": { "label": { "browser": "Browser", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "Calculate", + "group-by": "Group by" + } + }, "placeholder-ignored": "Ignored", "placeholder-select-stats": "Select stats" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "Group data by a field value and create aggregate data." + }, + "name": { + "group-by": "Group by" + }, "title-calc-alert": "Calculations will not have an effect if no fields are being grouped on" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "Group data by a field value and create nested tables with the grouped data." + }, "description-show-field-names": "If enabled nested tables will show field names as a table header", "label-show-field-names-in-nested-tables": "Show field names in nested tables", + "name": { + "group-to-nested-tables": "Group to nested tables" + }, "title-calc-alert": "Calculations will not have an effect if no fields are being grouped on." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "Summarize and reorganize data based on three fields." + }, "label-cell-value": "Cell value", "label-column": "Column", "label-empty-value": "Empty value", "label-row": "Row", + "name": { + "grouping-to-matrix": "Grouping to matrix" + }, "special-value-options": { "description": { "boolean-false-value": "Boolean false value", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "Calculate a histogram from input data." + }, + "name": { + "histogram": "Histogram" + }, "placeholder-auto": "Auto", "placeholder-default": "Default: 30", "placeholder-none": "None" @@ -11947,7 +12654,16 @@ "label-as-percentile": "As percentile", "tooltip-transform-index-percentile": "Transform the row index as a percentile." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "Combine rows from 2+ tables, based on a related field." + }, + "name": { + "join-by-field": "Join by field" + } + }, "join-by-labels-transformer-editor": { + "error-required": "Required", "info": { "input-contain-labels": "The input does not contain any labels", "no-input-or-labels-found": "No input (or labels) found", @@ -11969,10 +12685,30 @@ "label-labels": "Labels", "label-mode": "Mode", "label-value-field-name": "Value field name", + "modes": { + "label": { + "columns": "Columns", + "rows": "Rows" + } + }, "placeholder-optional-select-label": "(Optional) Select label", "tooltip-replace-value-field-label": "Replace the value field name with a label" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "Group series by time and return labels or tags as fields." + }, + "name": { + "labels-to-fields": "Labels to fields" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "Limit the number of items displayed." + }, + "name": { + "limit": "Limit" + }, "placeholder-value-or-variable": "Value or variable" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "String values are represented as separate fields rather than as labels" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Merge has no effect when applied on a single frame." + "description": { + "merge-multiple-series": "Merge multiple series. Values will be combined into one row." + }, + "merge-effect-applied-single-frame": "Merge has no effect when applied on a single frame.", + "name": { + "merge": "Merge series/tables" + } }, "multi-info": { "all-values-are-numeric": "All values are numeric", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Time in ascending order" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "Re-order, hide, or rename fields." + }, "field-order": "Field order", "field-order-auto": "Auto", "field-order-manual": "Manual", - "first-frame-warning": "Organize fields only works with a single frame. Consider applying a join transformation or filtering the input first." + "first-frame-warning": "Organize fields only works with a single frame. Consider applying a join transformation or filtering the input first.", + "name": { + "organize-fields": "Organize fields by name" + } }, "partion-by-values-editor": { "tooltip-naming": "Sets how the names of the selected fields are displayed. As frame name is usually better for tabular data" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Partition by values only works with a single frame.", "tooltip-keeps-partition-fields-frames": "Keeps the partition fields in the frames" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "Stretch data frames from the wide format into the long format." + }, + "name": { + "prepare-time-series": "Prepare time series" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Info" + "label-info": "Info", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "Convert each frame to long format" + }, + "label": { + "long-time-series": "Long time series" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "Creates a new frame for each time/number pair" + }, + "label": { + "multiframe-time-series": "Multi-frame time series" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "Creates a single frame joined by time" + }, + "label": { + "wide-time-series": "Wide time series" + } + } }, "range-matcher-editor": { "and": "and", @@ -12035,6 +12815,9 @@ "label-operation": "Operation" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "Reduce all rows or data points to a single value (ex. max, mean)." + }, "label-calculations": "Calculations", "label-include-time": "Include time", "label-labels-to-fields": "Labels to fields", @@ -12049,12 +12832,23 @@ "series-to-rows": "Series to rows" } }, + "name": { + "reduce": "Reduce" + }, "placeholder-choose-stat": "Choose stat" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Value or variable", "variable-regex-documentation": "Formats multi-value variable into a regex string" }, + "regression": { + "description": { + "create-new-data-frame": "Create a new data frame containing values predicted by a statistical model." + }, + "name": { + "regression-analysis": "Regression analysis" + } + }, "regression-transformer-editor": { "label": { "cubic": "Cubic", @@ -12076,6 +12870,14 @@ "regression": "regression", "tooltip-number-of-xy-points-to-predict": "Number of X,Y points to predict" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "Rename parts of the query results using a regular expression and replacement pattern." + }, + "name": { + "rename-fields-by-regex": "Rename fields by regex" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Match", "label-replace": "Replace", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Field", - "label-mode": "Mode" + "label-mode": "Mode", + "modes": { + "description": { + "combine-data-from-two-tables": "Combine data from two tables whenever there are matching values in a fields common to both tables.", + "join-on-a-field": "Join on a field value with duplicated values. Non performant outer join best used for tabular(SQL like) data.", + "keep-all-rows": "Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series." + }, + "label": { + "inner": "Inner", + "outer-tabular": "Outer (tabular)", + "outer-time-series": "Outer (time series)" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "Merge multiple series. Return time, metric and values as a row." + }, + "name": { + "series-to-rows": "Series to rows" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "Sort fields in a frame." + }, "label-field": "Field", "label-reverse": "Reverse", + "name": { + "sort-by": "Sort by" + }, "placeholder-select-field": "Select field" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transform" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "Convert time series data to table rows so that they can be viewed in tabular or sparkline format." + }, + "is-applicable-description": { + "requires-time-series-frame": "The Time series to table transformation requires at least one time series frame to function. You currently have none." + }, + "name": { + "time-series-to-table": "Time series to table" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Time field", @@ -12121,10 +12960,34 @@ "placeholder-value": "Value", "tooltip-name-for-value-fields": "Name for value fields" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "Transpose the data frame." + }, + "name": { + "transpose": "Transpose" + }, + "tags": { + "pivot": "Pivot", + "transform": "Transform", + "translate": "Translate" + } + }, "unary-operation-editor": { "label-operation": "Operation", "placeholder-field": "Field" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "Calculate new fields", + "combine": "Combine", + "create-new-visualization": "Create new visualization", + "filter": "Filter", + "perform-spatial-operations": "Perform spatial operations", + "reformat": "Reformat", + "reorder-and-rename": "Reorder and rename" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Multiple value fields of any type", "single-frame": "Single frame", @@ -12177,6 +13040,7 @@ "title": "Organizations" }, "user-picker": { + "noOptionsMessage-no-users-found": "No users found", "select-aria-label": "User picker", "select-placeholder": "Start typing to search for user" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "Are you sure you want to delete variable \"{{variableToDelete}}\"?", + "confirmText-delete": "Delete", "title-delete-variable": "Delete variable" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Refresh" }, "query-variable-sort-select": { - "description-values-variable": "How to sort the values of this variable" + "description-values-variable": "How to sort the values of this variable", + "name-sort": "Sort", + "sort_options": { + "label": { + "alphabetical-asc": "Alphabetical (asc)", + "alphabetical-caseinsensitive-asc": "Alphabetical (case-insensitive, asc)", + "alphabetical-caseinsensitive-desc": "Alphabetical (case-insensitive, desc)", + "alphabetical-desc": "Alphabetical (desc)", + "disabled": "Disabled", + "natural-asc": "Natural (asc)", + "natural-desc": "Natural (desc)", + "numerical-asc": "Numerical (asc)", + "numerical-desc": "Numerical (desc)" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "Add option", + "description": "Add custom options in addition to query results", + "label-placeholder": "display label", + "remove-option-button-label": "Remove option", + "value-placeholder": "value, default empty string" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "How to sort static options with query results" }, "text-box-variable-editor": { + "name-default-value": "Default value", "placeholder-default-value-if-any": "default value, if any", "text-options": "Text options" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Optional display name", "description-template-variable-characters": "The name of the template variable. (Max. 50 characters)", "general": "General", + "name-label": "Label", + "name-name": "Name", "placeholder-descriptive-text": "Descriptive text", "placeholder-label-name": "Label name", "placeholder-variable-name": "Variable name", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Duplicate variable", "tooltip-remove-variable": "Remove variable" }, + "variable-editor-un-connected": { + "name-description": "Description" + }, "variable-options": { "aria-label-toggle-all-values": "Toggle all values" }, + "variable-type-select": { + "name-select-variable-type": "Select variable type" + }, "variable-usages-button": { "tooltip-show-usages": "Show usages" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index fbdb3450d6e..ac320d7b251 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Algunas características son estables (GA) y están habilitadas de forma predeterminada, mientras que otras se encuentran actualmente en su fase beta preliminar, disponibles para su adopción temprana.", "confirm-modal-body-2": "Aconsejamos comprender las implicaciones de cada cambio de característica antes de realizar modificaciones.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Disponibilidad general", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Eliminar organización", + "confirmText-delete": "", "title-delete": "Eliminar" }, "anon-users": { "not-found": "No se han encontrado usuarios anónimos." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Forzar cierre de sesión en todos los dispositivos" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "No tienes permiso para ver a los usuarios de esta organización. Para actualizar esta organización, ponte en contacto con el administrador del servidor.", "heading": "Editar organización", @@ -208,9 +216,11 @@ "not-editable": "La función de este usuario no puede editarse porque está sincronizada con tu proveedor de autenticación. Consulta los <1>documentos de autenticación de Grafana para obtener más información." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Rol" }, + "confirmText-delete": "", "delete-aria-label": "Eliminar usuario: {{name}}", "title-delete": "Eliminar" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Estos ajustes del sistema están definidos en grafana.ini o custom.ini (o anulados en variables ENV). Para modificarlos, actualmente es necesario reiniciar Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licencia Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Cambiar", + "confirmText-change": "", "grafana-admin-key": "Administrador de Grafana", "grafana-admin-no": "No", "grafana-admin-yes": "Sí", "title": "Permisos" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Eliminar usuario", "disable-button": "Deshabilitar usuario", "edit-button": "Editar", @@ -312,6 +330,9 @@ "title-delete-user": "Eliminar usuario", "title-disable-user": "Deshabilitar usuario" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Navegador y sistema operativo", "force-logout-all-button": "Forzar cierre de sesión en todos los dispositivos", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Tiempos de actividad" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Añadir más" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Silencio, agrupación y temporización (opcional)", "title-muting-grouping-and-timings": "Silencio, agrupación y temporización" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Copiar enlace", "duplicate": "Duplicar", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Editar YAML", @@ -546,6 +577,7 @@ "view-configuration": "Ver configuración" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "La configuración interna de Grafana Alertmanager no se puede cambiar manualmente. Para cambiar esta configuración, edita los recursos individuales a través de la interfaz de usuario.", "gma-manual-configuration-is-not-supported": "No se admiten cambios de configuración manual", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Restableciendo la configuración de Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Comparar", "restore": "Restaurar", "text-latest": "Recientes" }, + "confirmText-yes-restore-configuration": "", "loading": "Cargando...", "no-previous-configurations": "No hay configuraciones anteriores", "this-might-take-a-while": "Este proceso puede llevar un tiempo...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Más acciones para el punto de contacto «{{contactPointName}}»", + "ariaLabel-delete": "", "button-edit": "Editar", "button-view": "Vista", + "export-ariaLabel-export": "", "export-label-export": "Exportar", "label-delete": "Eliminar", "label-manage-permissions": "Gestionar permisos", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Deshabilitar mensaje resuelto" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "El valor debe ser un número entero positivo.", "must-enter-a-group-name": "Debe introducirse un nombre del grupo" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Error al cargar los detalles de la fuente de datos", "edit": "Editar", - "evaluations-to-fire": "Ciclos de evaluación para activar", "export": "Exportar", "folder": "Carpeta", "group-loading-error": "Error al cargar el grupo", "interval": "Intervalo", "namespace": "Nombre del espacio", - "pending-period": "Periodo pendiente", - "recording": "Registro", - "rule-name": "Nombre de la regla" + "new": "" }, "group-edit": { "ds-error": "Error al cargar los detalles de la fuente de datos", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Empezar", "label-insights": "Información", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alertas" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Otras fuentes de datos" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Desactivado", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "Otro usuario ha actualizado el árbol de políticas de notificación.", "error-code": "Mensaje de error: «{{error}}»", - "fallback": "Se ha producido un error al actualizar tus políticas de notificación.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Actualiza la página y vuelve a intentarlo.", - "title": "Error al guardar la política de notificación" + "title": "" }, "n-more-policies_one": "{{count}} políticas adicionales", "n-more-policies_other": "{{count}} políticas adicionales" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Añadir consulta", "body-queries-expressions-configured": "Crea al menos una consulta o expresión sobre la que alertar", + "confirmText-deactivate": "", "expressions": "Expresiones", "loading-data-sources": "Cargando fuentes de datos...", "manipulate-returned-queries-other-operations": "Manipula los datos devueltos por las consultas con operaciones matemáticas y de otro tipo.", @@ -2184,6 +2237,9 @@ "label-export-all": "Exportar todo" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "No se ha podido cargar el editor de consultas debido a: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Deberás establecer un nuevo grupo de evaluación para la regla copiada porque el original se ha aprovisionado y no se puede usar para las reglas creadas en la interfaz de usuario.", "body-not-provisioned": "La nueva regla <1>no se marcará como una regla aprovisionada.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Copiar regla de alerta aprovisionada" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Inspeccionar regla de alerta" }, "rule-list": { - "cannot-find-rule-details-for": "No se pueden encontrar los detalles de la regla para el UID {{uid}}", - "cannot-load-rule-details-for": "No se pueden cargar los detalles de la regla para el UID {{uid}}", "configure-datasource": "Configurar", "draft-new-rule": "Redactar una nueva regla", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "¿No encuentras la fuente de datos que quieres? Algunas fuentes de datos no son compatibles con las alertas. Haz clic en el icono para obtener más información." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Añadir nuevo Alertmanager", "builtin-alertmanager": "Alertmanager integrado", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Elegir plantilla de notificación", "loading": "Cargando...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Seleccionar plantilla de notificación" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Acciones", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "No hay plantillas definidas.", "template-group": "Grupo de plantillas", "title-delete-template-group": "Eliminar grupo de plantillas" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "(hace {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "La carpeta de destino no está vacía y pueden sobrescribirse o eliminarse algunas reglas. ¿Seguro que quieres importar estas reglas de alerta a las reglas gestionadas por Grafana?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Eliminar punto de contacto" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Eliminar política de notificación" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Notas", "first-value": "Primer valor", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Añadir consulta de anotación", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Consulta la <2>documentación de anotaciones para obtener más información.", "title": "Aún no se han añadido consultas de anotaciones personalizadas" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} eventos (de {{numFields}} campos)", "no-events-found": "No se ha encontrado ningún evento", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Definir ID de equipos permitidos", "display-name-description": "Se mostrará en la página de inicio de sesión como «Iniciar sesión con...». Resulta útil si utiliza más de un proveedor de identidad o protocolos SSO.", "display-name-label": "Nombre para mostrar", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nombre de la clave que se utilizará para la búsqueda de correo electrónico del usuario dentro del mapa de atributos del token de identificación OAuth2.", "email-attribute-name-label": "Nombre del atributo de correo electrónico", "email-attribute-path-description": "Expresión JMESPath que se utilizará para la búsqueda de correo electrónico del usuario a partir de la información del usuario.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Lista de números enteros de ID de equipo.", "team-ids-label": "ID de equipo", "team-ids-numbers": "Los ID de equipo deben ser números.", - "team-ids-other": "Lista de cadenas de identificadores de equipo.", + "team-ids-other": "", "team-ids-placeholder": "Introduzca los ID de equipo y pulse Intro para añadirlos", "teams-url-description": "La URL utilizada para consultar los ID de equipo. Si no se establece, el valor predeterminado es /teams.", "teams-url-description-oauth": "Si configura «{{ teamsURLLabel }}», también debe configurar «{{ teamIDsAttributePathLabel }}».", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Restablecer a los valores predeterminados" }, + "confirmText-reset": "", "disable": "Deshabilitar", "disabling": "Deshabilitando...", "discard": "Descartar", @@ -3473,11 +3563,11 @@ "button-create": "Crear", "button-creating": "Creando...", "cancel": "Cancelar", - "error-invalid-folder-name": "Nombre de carpeta no válido", "error-required": "Nombre de la carpeta obligatorio", "folder-name-input-placeholder-enter-folder-name": "Introducir el nombre de la carpeta", "label-folder-name": "Nombre de la carpeta", "text-pull-request-created": "Se ha creado una solicitud de extracción con cambios en esta carpeta:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Solicitud de extracción creada", "title-this-repository-is-read-only": "Este repositorio es de solo lectura" }, @@ -3895,6 +3985,7 @@ "tooltip": "Cerrar" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Se ha producido un error al cargar las opciones." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Calcula dinámicamente el intervalo dividiendo el intervalo de tiempo por el recuento especificado" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "¿Dirigirse a un sitio externo?" } }, "render-left-actions": { - "text-public": "Pública", "tooltip-view-as-scene": "Ver como escena" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Error al cargar el panel de control" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Error al obtener el dashboard" @@ -4540,6 +4641,13 @@ "editable": "Editable", "readonly": "Solo lectura" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Transformar datos" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "¿Seguro que quieres restaurar el dashboard a la versión {{version}}? Todos los cambios no guardados se perderán.", + "confirmText-restore-version": "", "title-restore-version": "Restaurar versión" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Este título no es único" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Guardar como" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Ya existe un dashboard con el mismo nombre en la carpeta seleccionada.<1><2>¿Seguro que quieres guardar este dashboard?", "body-version-mismatch": "Otra persona ha actualizado este dashboard<1><2>¿Seguro que quieres guardar este dashboard?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Conflicto", "title-version-mismatch": "Conflicto" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Ir al siguiente panel de control", "playlist-previous": "Ir al panel de control anterior", "playlist-stop": "Detener la lista de reproducción", - "public-dashboard": "Pública", "refresh": "Actualizar panel de control", "save": "Guardar panel de control", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Aplicar transformación a" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Depuración", "title-disable-transformation": "Deshabilitar transformación", "title-filter": "Filtro", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Buscar transformación", "show-images": "Mostrar imágenes", - "title-add-another-transformation": "Añadir otra transformación" + "title-add-another-transformation": "Añadir otra transformación", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Añadir otra transformación" }, + "confirmText-delete-all": "", "delete-all-transformations": "Eliminar todas las transformaciones", "title-delete-all-transformations": "¿Eliminar todas las transformaciones?", "tooltip-clear-search": "Borrar búsqueda", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alternar selección de versión {{version}}", "date": "Fecha", + "name-latest": "", "notes": "Notas", "restore": "Restaurar", "updated-by": "Actualizada por", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Permite a los usuarios añadir valores personalizados a la lista", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Proporcionar dimensiones como CSV: {{name}}, {{value}}", "label-data-source": "Fuente de datos", - "label-use-static-key-dimensions": "Usar dimensiones de clave estática" + "label-use-static-key-dimensions": "Usar dimensiones de clave estática", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Revocar URL pública" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Opciones personalizadas", + "name-values-separated-comma": "", "selection-options": "Opciones de selección" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Tipo", "label-url": "URL", "label-with-tags": "Con etiquetas", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Abrir panel" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "El valor aún no se ha guardado en la base de datos de Grafana", "not-yet-saved": "El valor no se ha guardado en la base de datos de Grafana", - "open-pull-request-in-git-hub": "Abrir solicitud de extracción en GitHub", - "title-dashboard-loaded-branch-git-hub": "Este panel de control se carga desde una rama en GitHub.", "title-dashboard-loaded-external-repository": "Este panel de control se carga desde un repositorio externo", - "title-dashboard-loaded-request-git-hub": "Este panel de control se carga desde una solicitud de extracción en GitHub.", - "title-error-loading-dashboard": "Error al cargar el panel de control", - "value-not-saved": "El valor aún no se ha guardado en la base de datos de Grafana", - "view-pull-request-in-git-hub": "Ver solicitud de extracción en GitHub" + "title-error-loading-dashboard": "Error al cargar el panel de control" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Opciones de fuente de datos", "description-instance-name-filter": "Filtro Regex para elegir las instancias de origen de datos en la lista de valores de variables. Déjalo vacío para todos.", "example-instance-name-filter": "Ejemplo: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Opciones de selección" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Añadir transformación" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Opciones de columna", "row-height-options": "Opciones de altura de fila" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Proporcionar dimensiones como CSV: {{name}}, {{value}}", "group-by-options": "Agrupar por opciones", "label-data-source": "Fuente de datos", - "label-use-static-group-by-dimensions": "Usar dimensiones de grupo estático" + "label-use-static-group-by-dimensions": "Usar dimensiones de grupo estático", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Copiar al portapapeles", @@ -5521,9 +5710,14 @@ "apply": "Aplicar" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "El valor calculado no será inferior a este umbral", "description-step-count": "Cuántas veces se debe dividir el rango de tiempo actual para calcular el valor", - "interval-options": "Opciones de intervalo" + "interval-options": "Opciones de intervalo", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Utilizado en {{count}} dashboards", "usage-count_other": "Utilizado en {{count}} dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Ya existe un dashboard con el mismo nombre en la carpeta seleccionada. ¿Seguro que quieres guardar este dashboard?", "title-name-already-exists": "Este nombre ya existe" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "¿Dirigirse a un sitio externo?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Añadir otra transformación", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Eliminar todas las transformaciones", "title-delete-all-transformations": "¿Eliminar todas las transformaciones?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Los grupos de captura con nombre se pueden utilizar para separar el texto y el valor de la pantalla (<1>consulta los ejemplos).", "description-optional": "Es opcional si quieres extraer parte del nombre de una serie o segmento de nodo métrico.", "label-data-source": "Fuente de datos", + "label-static-options-sort": "", "label-target-data-source": "Fuente de datos de destino", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opciones de consulta", - "selection-options": "Opciones de selección" + "selection-options": "Opciones de selección", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "¿Seguro que quieres restaurar el dashboard a la versión {{version}}? Todos los cambios no guardados se perderán.", + "confirmText-restore-version": "", "title-restore-version": "Restaurar versión" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Carpeta", "render-footer": { "title-failed-to-save-dashboard": "Error al guardar el dashboard" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Permite seleccionar varios valores al mismo tiempo", "description-enables-option-include-variables": "Habilita una opción para incluir todos los valores", - "description-enables-users-custom-values": "Permite a los usuarios añadir valores personalizados a la lista" + "description-enables-users-custom-values": "Permite a los usuarios añadir valores personalizados a la lista", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Alternar menú de compartir" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Error al copiar al portapapeles" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opcional)", "text-options": "Opciones de texto" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "¿Seguro que quieres desvincular este panel?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Esta variable no está referenciada por ninguna variable o dashboard.", "aria-label-variable-referenced-other-variables-dashboard": "Esta variable está referenciada por otras variables o dashboards.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulario del editor de variables", "back-to-list": "Regresar a la lista", + "confirmText": { + "delete-variable": "" + }, "delete": "Eliminar", "description-optional-display-name": "Nombre para mostrar opcional", "description-template-variable-characters": "El nombre de la variable de la plantilla. (Máx. 50 caracteres)", "general": "General", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texto descriptivo", "placeholder-label-name": "Nombre de la etiqueta", "placeholder-variable-name": "Nombre de la variable", @@ -5829,13 +6066,25 @@ "variable": "Variable" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Eliminar variable", "tooltip-duplicate-variable": "Duplicar variable", "tooltip-remove-variable": "Quitar variable" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Ocultar" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Mostrando usos para: {{variableId}}", "tooltip-show-usages": "Mostrar usos" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alternar selección de versión {{version}}", "date": "Fecha", + "name-latest": "", "notes": "Notas", "restore": "Restaurar", "updated-by": "Actualizada por", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Subir" @@ -6287,6 +6538,7 @@ }, "label-limit": "Límite", "label-value": "Valor", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Máx.", "label-min": "Mín.", - "label-value": "Valor" + "label-value": "Valor", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Seleccionar operador de nombre de servicio", "aria-label-select-span-name": "Seleccionar nombre de intervalo", "aria-label-select-span-name-operator": "Seleccionar operador de nombre de intervalo", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtros de intervalo", "label-duration": "Duración", "label-service-name": "Nombre de servicio", @@ -6939,6 +7199,8 @@ "split-widen": "Agrandar panel" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Enviar comentarios", "label-copied": "¡Copiado!", "label-export": "Exportar", @@ -7046,6 +7308,7 @@ "when": "CUÁNDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operación" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Borrar carpetas", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtro de carpeta", "select-placeholder": "Filtrar por carpeta" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Lo sentimos, no hemos podido completar tu solicitud. Vuelve a intentarlo.", "send-custom-feedback": "Enviar" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Latitud", "label-longitude": "Longitud" @@ -7153,6 +7463,14 @@ "center": "Centro:", "zoom": "Escala:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Nivel" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Añadir regla de estilo de geomapa" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Añadir capa", "no-layers": "¿No hay capas?" @@ -7185,16 +7511,38 @@ "label-zoom": "Ampliar", "use-current-map-settings": "Utilizar ajustes actuales de mapa" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Símbolo" }, "measure-overlay": { "tooltip-show-measure-tools": "Mostrar herramientas de medición" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "La capa del mapa base está configurada por el administrador del servidor." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Alinear", "label-baseline": "Punto de referencia", "label-color": "Color", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Alineación vertical de símbolo", "label-text-label": "Etiqueta de texto", "label-x-offset": "Compensación X", - "label-y-offset": "Compensación Y" + "label-y-offset": "Compensación Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operador de comparación", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Propiedad destacada", "placeholder-numeric-value": "Valor numérico", "placeholder-value": "valor" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "{{colorLabel}} color" }, "confirm-button": { - "cancel": "Cancelar" + "cancel": "Cancelar", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Escribe «{{confirmPromptText}}» para confirmar" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "alternar panel para contraer", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Cancelar consulta", "tooltip-cancel-loading": "Cancelar consulta", "tooltip-stop-streaming": "Detener transmisión", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Coincidir mayúsculas/minúsculas", "inspect-drawer-title": "Inspeccionar valor", "inspect-menu-label": "Inspeccionar valor", + "nested-table": { + "no-data": "" + }, "no-values-label": "Sin valores", "pagination-summary": "{{itemsRangeStart}}-{{displayedEnd}} de {{numRows}} filas" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Añadir", + "placeholder-new-tag": "", "remove": "Eliminar etiqueta: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Haz clic para {{actionTitle}}", "footer-click-to-navigate": "Haz clic para abrir {{linkTitle}}", "timestamp": "Marca de tiempo" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Crear panel de librería" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "¿Quieres eliminar este panel?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Utilizado en {{count}} dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Buscar por nombre o descripción" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Cargando panel de la librería..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Crea un enlace personalizado y directo para compartir tu panel dentro de tu organización, con los siguientes ajustes de personalización:", "download-image": "Descargar imagen", - "render-image": "Generar imagen", - "render-image-error": "Error al representar la imagen del panel", - "render-image-error-description": "Se ha producido un error al generar la imagen" + "render-image": "Generar imagen" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Ajustar líneas" }, "log-details": { - "close": "Cerrar detalles del log", "fields": "Campos", "links": "Enlaces", "log-line": "Línea de registro", @@ -8405,6 +8795,42 @@ "show-more": "mostrar más", "tooltip-error": "Error: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Copiar enlace a la línea de registro", "copy-log": "Copiar línea de registro", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Enlaces de datos", "aria-label-fields": "Campos", + "aria-label-line": "", "aria-label-log-level": "Nivel de log", "aria-label-no-details": "Sin detalles" }, @@ -8598,6 +9030,8 @@ "updated-on": "Actualizado el" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Eliminar" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Importar panel de control" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Esta herramienta puede migrar algunos recursos de esta instalación a tu pila en la nube. Para empezar, deberás crear una instantánea de esta instalación. La creación de una instantánea suele tardar menos de dos minutos. La instantánea se almacena junto con esta instalación de Grafana.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "Aceptar", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "El panel de control ya está aprovisionado y gestionado por Grafana en la instancia en la nube. A partir de ahora, recomendamos utilizar el panel de control proporcionado. Si todavía quieres copiar el panel de control a la instancia en la nube, cambia el ID del panel de control en el JSON del panel de control, guarda una nueva instantánea y vuelve a subirla.", "datasource-already-managed": "La fuente de datos ya está aprovisionada y gestionada por Grafana en la instancia en la nube. Si se trata de un recurso distinto, configura otro UID e inténtalo de nuevo.", "datasource-invalid-url": "Hay una fuente de datos que tiene una URL no válida. Proporciona una URL válida e inténtalo de nuevo.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": ">100 nodos", "aria-label-hidden-marker": "Marcador de nodos ocultos: {{marker}}", - "node-count_one": "{{count}} nodo", + "node-count_one": "{{count}} nodos", "node-count_other": "{{count}} nodos" }, "node": { @@ -9318,9 +9763,9 @@ "aria-label-nodes-hidden-warning": "Advertencia de nodos ocultos", "computing-layout": "Calculando el diseño", "no-data": "Sin datos", - "hidden-nodes_one": "<0> {{count}} nodo está oculto por razones de rendimiento.", + "hidden-nodes_one": "<0> {{count}} nodos están ocultos por razones de rendimiento.", "hidden-nodes_other": "<0> {{count}} nodos están ocultos por razones de rendimiento.", - "processed-nodes_one": "<0> El diseño en capas puede ser lento con {{count}} nodo.", + "processed-nodes_one": "<0> El diseño en capas puede ser lento con {{count}} nodos.", "processed-nodes_other": "<0> El diseño en capas puede ser lento con {{count}} nodos." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Seleccionar organización" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "No tienes permiso para ver esta página.", "title-access-denied": "Acceso denegado" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "No se ha encontrado ningún componente de página de la aplicación raíz" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "complementos actualizados" }, "versions": { - "confirmation-text-1": "¿Seguro que quieres volver a la versión", - "confirmation-text-2": "Esta acción no es recomendable", + "confirmation-text": "", "downgrade-confirm": "Volver a versión anterior", "downgrade-title": "Volver a versión anterior del complemento" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "No se ha encontrado ningún complemento" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "Aceptar" @@ -9800,7 +10250,11 @@ "updating": "Actualizando" }, "install-controls-button": { - "title-uninstall-modal": "Desinstalar {{plugin}}" + "title-uninstall-modal": "Desinstalar {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Este plugin no está publicado en <2>grafana.com/plugins y no se puede gestionar a través del catálogo.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Esta función se encuentra actualmente en desarrollo activo. Para obtener la mejor experiencia y las últimas mejoras, recomendamos utilizar la <2>compilación nocturna de Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Selector de cuentas de servicio", "select-placeholder": "Empieza a escribir para buscar cuentas de servicio" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Añadir token de cuenta de servicio", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Eliminar cuenta de servicio", "disable-service-account": "Deshabilitar cuenta de servicio", "enable-service-account": "Habilitar cuenta de servicio", @@ -10907,6 +11375,7 @@ "used-by": "Usado por" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Editar" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Añadir cuenta de servicio", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Buscar cuenta de servicio por nombre", "sub-title": "Las cuentas de servicio y sus tokens se pueden utilizar para autenticarse en la API de Grafana. Encuentra más información en nuestra <2>documentación.", "title-delete-service-account": "Eliminar cuenta de servicio", - "title-disable-service-account": "Deshabilitar cuenta de servicio" + "title-disable-service-account": "Deshabilitar cuenta de servicio", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "El token ha caducado", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exportar como JSON", "share-externally-title": "Compartir externamente", "share-internally-title": "Compartir internamente", @@ -10972,6 +11448,19 @@ "view-button": "Ver JSON", "view-button-yaml": "Ver YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Cree un panel de librería." }, @@ -10980,7 +11469,7 @@ "info-text": "Cree un enlace directo a este panel de control o panel, personalizado con las siguientes opciones.", "link-url": "URL del enlace", "render-alert": "Complemento de renderizador de imagen no instalado", - "render-instructions": "Para representar una imagen de panel, debes instalar el <2>complemento de renderizado de imágenes de Grafana. Ponte en contacto con tu administrador de Grafana para instalar el complemento.", + "render-instructions": "", "rendered-image": "Enlace directo a la imagen renderizada", "save-alert": "El panel de control no se ha guardado", "save-dashboard": "Para renderizar una imagen de panel, primero debe guardar el panel de control.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Vista previa del panel" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Demasiados puntos para visualizar correctamente. <1>Actualice la consulta para devolver menos puntos. <3>({{count}} punto recibido)", + "too-many-points_one": "Demasiados puntos para visualizar correctamente. <1>Actualice la consulta para devolver menos puntos. <3>({{count}} puntos recibidos)", "too-many-points_other": "Demasiados puntos para visualizar correctamente. <1>Actualice la consulta para devolver menos puntos. <3>({{count}} puntos recibidos)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Texto alternativo que se mostrará si no se puede visualizar una imagen o para los usuarios que utilizan un lector de pantalla", "description-title-text": "Texto que se mostrará cuando se pase el cursor por encima de la imagen", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Opción de etiqueta" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Selector de equipo", "select-placeholder": "Seleccionar un equipo" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Campo(s) o número" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Modo", "label-replace-all-fields": "Reemplazar todos los campos" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Etiqueta", "label-name": "Nombre", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Marco" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Añadir un convertidor de tipo de campo", "aria-label-remove-convert-field-type-transformer": "Quitar convertidor de tipo de campo", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Navegador", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Formato de entrada", "label-join-with": "Unir con", "label-set-timezone": "Establecer zona horaria", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Tipo", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Utilizar un separador explícito al unir valores de matriz", "tooltip-input-format": "Especifica el formato del campo de entrada para que Grafana pueda analizar correctamente la cadena de fecha.", "tooltip-specify-the-output-format": "Especifica el formato de salida.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Apagado" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Añadir valor de enumeración", "generate-enum-values-from-data": "Generar valores de enumeración a partir de datos" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Eliminar" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Delimitador", "label-format": "Formato", "label-keep-time": "Conservar la hora", "label-replace-all-fields": "Reemplazar todos los campos", "label-source": "Fuente", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Seleccionar delimitador...", "tooltip-regexp": "Ejemplo: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Color de umbral" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", - "label-lookup": "Búsqueda" + "label-lookup": "Búsqueda", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Configuraciones adicionales", @@ -11848,12 +12413,26 @@ "use-as": "Utilizar como" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "De la variable", "label-identifier": "Identificador", "label-use-variable": "Utilizar variable", "label-variable": "Variable", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Patrón de expresión regular" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Campo", "label-match": "Coincidencia", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Añadir condición", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Condiciones", - "label-filter-type": "Tipo de filtro" + "label-filter-type": "Tipo de filtro", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", "label-format": "Formato", "label-substring-range": "Rango de subcadena" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formato", "label-set-timezone": "Establecer zona horaria", @@ -11878,6 +12491,70 @@ "tooltip-format": "El formato de salida para el campo especificado como una <2>cadena de formato Moment.js.", "tooltip-timezone-manually": "Establecer la zona horaria de la fecha manualmente" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Navegador", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorado", "placeholder-select-stats": "Seleccionar estadísticas" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Los cálculos no tendrán efecto si no se agrupan campos" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Si está habilitado, las tablas anidadas mostrarán los nombres de los campos como encabezado de la tabla", "label-show-field-names-in-nested-tables": "Mostrar nombres de campo en tablas anidadas", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Los cálculos no tendrán efecto si no se agrupan campos." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Valor de la celda", "label-column": "Columna", "label-empty-value": "Valor vacío", "label-row": "Fila", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Valor booleano falso", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Auto", "placeholder-default": "Predeterminado: 30", "placeholder-none": "Ninguno" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Como percentil", "tooltip-transform-index-percentile": "Transforma el índice de fila como un percentil." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "La entrada no contiene ninguna etiqueta", "no-input-or-labels-found": "No se han encontrado entradas (ni etiquetas)", @@ -11969,10 +12685,30 @@ "label-labels": "Etiquetas", "label-mode": "Modo", "label-value-field-name": "Nombre del campo de valor", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opcional) Seleccionar etiqueta", "tooltip-replace-value-field-label": "Reemplaza el nombre del campo de valor con una etiqueta" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Valor o variable" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Los valores de cadena se representan como campos separados en lugar de como etiquetas" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "La función Combinar no tiene ningún efecto cuando se aplica en un solo marco." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "La función Combinar no tiene ningún efecto cuando se aplica en un solo marco.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Todos los valores son numéricos", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Tiempo en orden ascendente" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Auto", "field-order-manual": "", - "first-frame-warning": "La función Organizar campos solo funciona con un solo marco. Considera la posibilidad de aplicar una transformación de unión o filtrar la entrada primero." + "first-frame-warning": "La función Organizar campos solo funciona con un solo marco. Considera la posibilidad de aplicar una transformación de unión o filtrar la entrada primero.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Establece cómo se muestran los nombres de los campos seleccionados. Como nombre de marco suele ser mejor para datos de tablas" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "La partición por valores solo funciona con un solo marco.", "tooltip-keeps-partition-fields-frames": "Mantener los campos de partición en los marcos" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formato", - "label-info": "Información" + "label-info": "Información", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "y", @@ -12035,6 +12815,9 @@ "label-operation": "Operación" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Cálculos", "label-include-time": "Incluir hora", "label-labels-to-fields": "Etiquetas a campos", @@ -12049,12 +12832,23 @@ "series-to-rows": "Series a filas" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Elegir estadística" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Valor o variable", "variable-regex-documentation": "Formatee la variable de múltiples valores en una cadena de expresión regular" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Cúbica", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Número de puntos X,Y para predecir" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Coincidencia", "label-replace": "Reemplazar", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Campo", - "label-mode": "Modo" + "label-mode": "Modo", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Campo", "label-reverse": "Reverso", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Seleccionar campo" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transformar" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Estadística", "label-time-field": "Campo de tiempo", @@ -12121,10 +12960,34 @@ "placeholder-value": "Valor", "tooltip-name-for-value-fields": "Nombre para los campos de valor" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Operación", "placeholder-field": "Campo" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Múltiples campos de valor de cualquier tipo", "single-frame": "Marco único", @@ -12177,6 +13040,7 @@ "title": "Organizaciones" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Selector de usuario", "select-placeholder": "Comienza a escribir para buscar un usuario" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Eliminar variable" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Actualizar" }, "query-variable-sort-select": { - "description-values-variable": "Cómo ordenar los valores de esta variable" + "description-values-variable": "Cómo ordenar los valores de esta variable", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "valor predeterminado, si lo hay", "text-options": "Opciones de texto" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Nombre para mostrar opcional", "description-template-variable-characters": "El nombre de la variable de la plantilla. (Máx. 50 caracteres)", "general": "General", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texto descriptivo", "placeholder-label-name": "Nombre de la etiqueta", "placeholder-variable-name": "Nombre de la variable", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Duplicar variable", "tooltip-remove-variable": "Quitar variable" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Alternar todos los valores" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Mostrar usos" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index a1254bb741a..7aeb8ec20a7 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Certaines fonctionnalités sont stables (GA) et activées par défaut, tandis que d’autres sont actuellement dans leur phase bêta préliminaire, disponibles pour une adoption précoce.", "confirm-modal-body-2": "Nous vous conseillons de comprendre les implications de chaque changement de fonctionnalité avant d’apporter des modifications.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Bêta", "content-general-availability": "Disponibilité générale", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Supprimer l’organisation", + "confirmText-delete": "", "title-delete": "Supprimer" }, "anon-users": { "not-found": "Aucun utilisateur anonyme trouvé." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Forcer la déconnexion de tous les appareils" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Vous n'avez pas l'autorisation de consulter les utilisateurs de cette organisation. Pour mettre à jour cette organisation, contactez votre administrateur serveur.", "heading": "Modifier l'organisation", @@ -208,9 +216,11 @@ "not-editable": "Le rôle de cet utilisateur n'est pas modifiable, car il est synchronisé à partir de votre fournisseur d'authentification. Reportez-vous à la <1>Documentation d'authentification de Grafana pour en savoir plus." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Rôle" }, + "confirmText-delete": "", "delete-aria-label": "Supprimer l’utilisateur : {{name}}", "title-delete": "Supprimer" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Ces paramètres système sont définis dans grafana.ini ou custom.ini (ou remplacés par des variables ENV). Pour les modifier, vous devez actuellement relancer Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licence Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Changer", + "confirmText-change": "", "grafana-admin-key": "Administrateur Grafana", "grafana-admin-no": "Non", "grafana-admin-yes": "Oui", "title": "Autorisations" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Supprimer l'utilisateur", "disable-button": "Désactiver l'utilisateur", "edit-button": "Modifier", @@ -312,6 +330,9 @@ "title-delete-user": "Supprimer l'utilisateur", "title-disable-user": "Désactiver l’utilisateur" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Navigateur et système d'exploitation", "force-logout-all-button": "Forcer la déconnexion de tous les appareils", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Horaires actifs" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Ajouter plus" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Mise en sourdine, regroupement et horaires (facultatif)", "title-muting-grouping-and-timings": "Mise en sourdine, regroupement et horaires" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Copier le lien", "duplicate": "Dupliquer", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Modifier le fichier YAML", @@ -546,6 +577,7 @@ "view-configuration": "Afficher la configuration" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "La configuration interne de Grafana Alertmanager ne peut pas être modifiée manuellement. Pour modifier cette configuration, modifiez les ressources individuelles via l’interface utilisateur.", "gma-manual-configuration-is-not-supported": "Les modifications de configuration manuelles ne sont pas prises en charge", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Réinitialisation de la configuration d’Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Comparer", "restore": "Restaurer", "text-latest": "Dernier" }, + "confirmText-yes-restore-configuration": "", "loading": "Chargement en cours...", "no-previous-configurations": "Aucune configuration précédente", "this-might-take-a-while": "Cela peut prendre un certain temps...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Plus d’actions pour le point de contact « {{contactPointName}} »", + "ariaLabel-delete": "", "button-edit": "Modifier", "button-view": "Afficher", + "export-ariaLabel-export": "", "export-label-export": "Exporter", "label-delete": "Supprimer", "label-manage-permissions": "Gérer les autorisations", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Désactiver le message résolu" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Doit être un entier positif.", "must-enter-a-group-name": "Vous devez saisir un nom de groupe" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Erreur lors du chargement des détails de la source de données", "edit": "Modifier", - "evaluations-to-fire": "Cycles d’évaluation à déclencher", "export": "Exporter", "folder": "Dossier", "group-loading-error": "Erreur lors du chargement du groupe", "interval": "Intervalle", "namespace": "Namespace", - "pending-period": "Période en attente", - "recording": "Enregistrement", - "rule-name": "Nom de la règle" + "new": "" }, "group-edit": { "ds-error": "Erreur lors du chargement des détails de la source de données", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Commencer", "label-insights": "Aperçus", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alerte" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Autres sources de données" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Désactivé", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "L'arborescence des politiques de notification a été mise à jour par un autre utilisateur.", "error-code": "Message d'erreur : « {{error}} »", - "fallback": "Une erreur s'est produite lors de la mise à jour de vos politiques de notification.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Actualisez la page, puis réessayez.", - "title": "Erreur lors de l'enregistrement de la politique de notification" + "title": "" }, "n-more-policies_one": "{{count}} politiques supplémentaires", "n-more-policies_other": "{{count}} politiques supplémentaires" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Ajouter une requête", "body-queries-expressions-configured": "Créer au moins une requête ou une expression pour laquelle une alerte doit être émise", + "confirmText-deactivate": "", "expressions": "Expressions", "loading-data-sources": "Chargement des sources de données…", "manipulate-returned-queries-other-operations": "Manipulez les données renvoyées par les requêtes avec des opérations mathématiques et autres.", @@ -2184,6 +2237,9 @@ "label-export-all": "Tout exporter" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Impossible de charger l’éditeur de requête en raison de : {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Vous devrez définir un nouveau groupe d’évaluation pour la règle copiée, car la règle d’origine a été mise en service et ne peut pas être utilisée pour les règles créées dans l’interface utilisateur.", "body-not-provisioned": "La nouvelle règle <1>ne sera pas marquée comme une règle mise en service.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Copier la règle d’alerte mise en service" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Inspecter la règle d’alerte" }, "rule-list": { - "cannot-find-rule-details-for": "Impossible de trouver les détails de la règle pour l’UID {{uid}}", - "cannot-load-rule-details-for": "Impossible de charger les détails de la règle pour l’UID {{uid}}", "configure-datasource": "Configurer", "draft-new-rule": "Rédiger une nouvelle règle", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Vous ne trouvez pas la source de données que vous voulez ? Certaines sources de données ne sont pas prises en charge pour les alertes. Cliquez sur l’icône pour en savoir plus." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Ajouter un nouvel Alertmanager", "builtin-alertmanager": "Alertmanager intégré", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Choisir un modèle de notification", "loading": "Chargement en cours...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Sélectionner un modèle de notification" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Actions", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Aucun modèle défini.", "template-group": "Groupe de modèles", "title-delete-template-group": "Supprimer le groupe de modèles" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "(il y a {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Le dossier cible n’est pas vide, certaines règles peuvent être remplacées ou supprimées. Voulez-vous vraiment importer ces règles d’alerte dans les règles gérées par Grafana ?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Supprimer le point de contact" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Supprimer la politique de notification" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Annotation", "first-value": "Première valeur", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Ajouter une requête d'annotation", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Consultez la <2>documentation sur les annotations pour en savoir plus.", "title": "Aucune requête d'annotation personnalisée n'a encore été ajoutée" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} événements (à partir de {{numFields}} champs)", "no-events-found": "Aucun événement trouvé", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Définir les ID des équipes autorisées", "display-name-description": "Sera affiché sur la page de connexion comme « Se connecter avec... ». Utile si vous utilisez plusieurs fournisseurs d’identité ou protocoles SSO.", "display-name-label": "Pseudo", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nom de la clé à utiliser pour la recherche d’e-mail de l’utilisateur dans la carte d’attributs du jeton d’identification OAuth2.", "email-attribute-name-label": "Nom d’attribut de l’e-mail", "email-attribute-path-description": "Expression JMESPath à utiliser pour la recherche d’e-mail de l’utilisateur à partir des informations de l’utilisateur.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Liste des ID d’équipe sous forme de nombres entiers.", "team-ids-label": "ID d’équipe", "team-ids-numbers": "Les ID d’équipe doivent être des nombres.", - "team-ids-other": "Liste des chaînes de caractères des identifiants d’équipe.", + "team-ids-other": "", "team-ids-placeholder": "Saisissez les ID d’équipe et appuyez sur Entrée pour ajouter", "teams-url-description": "L’URL utilisée pour interroger les ID d’équipe. Si elle n’est pas définie, la valeur par défaut est /teams.", "teams-url-description-oauth": "Si vous configurez « {{ teamsURLLabel }} », vous devez également configurer « {{ teamIDsAttributePathLabel }} ».", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Rétablir les valeurs par défaut" }, + "confirmText-reset": "", "disable": "Désactiver", "disabling": "Désactivation...", "discard": "Abandonner", @@ -3473,11 +3563,11 @@ "button-create": "Créer", "button-creating": "Création...", "cancel": "Annuler", - "error-invalid-folder-name": "Nom de dossier non valide", "error-required": "Un nom de dossier est obligatoire", "folder-name-input-placeholder-enter-folder-name": "Saisir le nom du dossier", "label-folder-name": "Nom du dossier", "text-pull-request-created": "Une demande de fusion a été créée avec des modifications dans ce dossier :", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Demande de fusion créée", "title-this-repository-is-read-only": "Ce référentiel est en lecture seule" }, @@ -3895,6 +3985,7 @@ "tooltip": "Fermer" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Une erreur s'est produite lors du chargement des options." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Calcule dynamiquement l’intervalle en divisant la plage temporelle par le nombre spécifié" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Accéder au site externe ?" } }, "render-left-actions": { - "text-public": "Public", "tooltip-view-as-scene": "Afficher en tant que scène" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Échec du chargement du tableau de bord" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Échec de la récupération du tableau de bord" @@ -4540,6 +4641,13 @@ "editable": "Modifiable", "readonly": "Lecture seule" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Transformer les données" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Voulez-vous vraiment restaurer le tableau de bord dans sa version {{version}} ? Toutes les modifications non enregistrées seront perdues.", + "confirmText-restore-version": "", "title-restore-version": "Restaurer la version" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Ce titre n’est pas unique" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Enregistrer sous" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Un tableau de bord du même nom existe déjà dans le dossier sélectionné.<1><2>Voulez-vous vraiment enregistrer ce tableau de bord ?", "body-version-mismatch": "Quelqu’un d’autre a mis à jour ce tableau de bord<1><2>Voulez-vous vraiment enregistrer ce tableau de bord ?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Conflits", "title-version-mismatch": "Conflits" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Accéder au tableau de bord suivant", "playlist-previous": "Accéder au tableau de bord précédent", "playlist-stop": "Arrêter la liste de lecture", - "public-dashboard": "Public", "refresh": "Actualiser le tableau de bord", "save": "Enregistrer le tableau de bord", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Appliquer la transformation vers" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Déboguer", "title-disable-transformation": "Désactiver la transformation", "title-filter": "Filtrer", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Rechercher une transformation", "show-images": "Afficher les images", - "title-add-another-transformation": "Ajouter une autre transformation" + "title-add-another-transformation": "Ajouter une autre transformation", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Ajouter une autre transformation" }, + "confirmText-delete-all": "", "delete-all-transformations": "Supprimer toutes les transformations", "title-delete-all-transformations": "Supprimer toutes les transformations ?", "tooltip-clear-search": "Effacer la recherche", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Basculer la sélection de la version {{version}}", "date": "Date", + "name-latest": "", "notes": "Remarques", "restore": "Restaurer", "updated-by": "Mise à jour par", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Permet aux utilisateurs d’ajouter des valeurs personnalisées à la liste", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Fournir les dimensions en tant que CSV : {{name}}, {{value}}", "label-data-source": "Source de données", - "label-use-static-key-dimensions": "Utiliser des dimensions de clé statiques" + "label-use-static-key-dimensions": "Utiliser des dimensions de clé statiques", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Désactiver l'URL publique" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Personnaliser les options", + "name-values-separated-comma": "", "selection-options": "Options de sélection" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Type", "label-url": "URL", "label-with-tags": "Avec les balises", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Ouvrir le tableau de bord" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Type" }, "dashboard-preview-banner": { - "not-saved": "La valeur n'est pas encore enregistrée dans la base de données Grafana", "not-yet-saved": "La valeur n'est pas enregistrée dans la base de données Grafana", - "open-pull-request-in-git-hub": "Ouvrir la demande de fusion dans GitHub", - "title-dashboard-loaded-branch-git-hub": "Ce tableau de bord est chargé à partir d'une branche dans GitHub.", "title-dashboard-loaded-external-repository": "Ce tableau de bord est chargé à partir d'un référentiel externe", - "title-dashboard-loaded-request-git-hub": "Ce tableau de bord est chargé à partir d'une demande de fusion dans GitHub.", - "title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord", - "value-not-saved": "La valeur n'est pas encore enregistrée dans la base de données Grafana", - "view-pull-request-in-git-hub": "Afficher la demande de fusion dans GitHub" + "title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Options de source de données", "description-instance-name-filter": "Filtre Regex pour les instances de source de données à choisir dans la liste des valeurs de variable. Laisser vide pour tout.", "example-instance-name-filter": "Exemple : ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Options de sélection" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Ajouter une transformation" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Options de colonne", "row-height-options": "Options de hauteur de ligne" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Fournir les dimensions en tant que CSV : {{name}}, {{value}}", "group-by-options": "Regrouper par options", "label-data-source": "Source de données", - "label-use-static-group-by-dimensions": "Utiliser les dimensions de groupe statiques" + "label-use-static-group-by-dimensions": "Utiliser les dimensions de groupe statiques", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Copier dans le presse-papiers", @@ -5521,9 +5710,14 @@ "apply": "Appliquer" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "La valeur calculée ne descendra pas en dessous de ce seuil", "description-step-count": "Le nombre de divisions requis de la plage temporelle actuelle pour calculer la valeur", - "interval-options": "Options d’intervalle" + "interval-options": "Options d’intervalle", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Utilisé sur {{count}} tableaux de bord", "usage-count_other": "Utilisé sur {{count}} tableaux de bord" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Un tableau de bord du même nom existe déjà dans le dossier sélectionné. Voulez-vous toujours enregistrer ce tableau de bord ?", "title-name-already-exists": "Nom déjà existant" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Accéder au site externe ?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Ajouter une autre transformation", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Supprimer toutes les transformations", "title-delete-all-transformations": "Supprimer toutes les transformations ?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Les groupes de capture nommés peuvent être utilisés pour séparer le texte d’affichage et la valeur (<1>voir les exemples).", "description-optional": "Facultatif, si vous souhaitez extraire une partie d’un nom de série ou d’un segment de nœud de métrique.", "label-data-source": "Source de données", + "label-static-options-sort": "", "label-target-data-source": "Source de données cible", + "label-use-static-options": "", + "name-regex": "", "query-options": "Options de recherche", - "selection-options": "Options de sélection" + "selection-options": "Options de sélection", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Voulez-vous vraiment restaurer le tableau de bord dans sa version {{version}} ? Toutes les modifications non enregistrées seront perdues.", + "confirmText-restore-version": "", "title-restore-version": "Restaurer la version" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Dossier", "render-footer": { "title-failed-to-save-dashboard": "Échec de l’enregistrement du tableau de bord" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Permet de sélectionner plusieurs valeurs en même temps", "description-enables-option-include-variables": "Active une option pour inclure toutes les valeurs", - "description-enables-users-custom-values": "Permet aux utilisateurs d’ajouter des valeurs personnalisées à la liste" + "description-enables-users-custom-values": "Permet aux utilisateurs d’ajouter des valeurs personnalisées à la liste", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Basculer le menu de partage" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Échec de la copie dans le presse-papiers" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(facultatif)", "text-options": "Options de texte" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Voulez-vous vraiment dissocier ce panneau ?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Cette variable n’est référencée par aucune variable ou aucun tableau de bord.", "aria-label-variable-referenced-other-variables-dashboard": "Cette variable est référencée par d’autres variables ou d’autres tableaux de bord.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulaire de l’éditeur de variables", "back-to-list": "Retour à la liste", + "confirmText": { + "delete-variable": "" + }, "delete": "Supprimer", "description-optional-display-name": "Nom d’affichage facultatif", "description-template-variable-characters": "Le nom de la variable du modèle. (50 caractères max)", "general": "Général", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texte de description", "placeholder-label-name": "Nom de l’étiquette", "placeholder-variable-name": "Nom de la variable", @@ -5829,13 +6066,25 @@ "variable": "Variable" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Supprimer la variable", "tooltip-duplicate-variable": "Dupliquer la variable", "tooltip-remove-variable": "Supprimer la variable" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Masquer" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Afficher les usages pour : {{variableId}}", "tooltip-show-usages": "Afficher les usages" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Basculer la sélection de la version {{version}}", "date": "Date", + "name-latest": "", "notes": "Remarques", "restore": "Restaurer", "updated-by": "Mise à jour par", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Charger" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limiter", "label-value": "Valeur", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max", "label-min": "Min", - "label-value": "Valeur" + "label-value": "Valeur", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Sélectionner l’opérateur de nom de service", "aria-label-select-span-name": "Sélectionner le nom de la durée", "aria-label-select-span-name-operator": "Sélectionner l’opérateur de nom de la durée", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtres de durée", "label-duration": "Durée", "label-service-name": "Nom du service", @@ -6939,6 +7199,8 @@ "split-widen": "Élargir le panneau" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Publiez votre commentaire", "label-copied": "Copié !", "label-export": "Exporter", @@ -7046,6 +7308,7 @@ "when": "QUAND" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Opération" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Effacer les dossiers", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtre de dossier", "select-placeholder": "Filtrer par dossier" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Désolé, je n'ai pas pu répondre à votre demande. Veuillez réessayer.", "send-custom-feedback": "Envoyer" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Latitude", "label-longitude": "Longitude" @@ -7153,6 +7463,14 @@ "center": "Centre :", "zoom": "Zoom :" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Couche" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Ajouter une règle de style de carte géographique" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Ajouter une couche", "no-layers": "Pas de couche ?" @@ -7185,16 +7511,38 @@ "label-zoom": "Zoom", "use-current-map-settings": "Utiliser les paramètres actuels de la carte" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Symbole" }, "measure-overlay": { "tooltip-show-measure-tools": "Afficher les outils de mesure" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "La couche de la carte de base est configurée par l’administrateur du serveur." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Aligner", "label-baseline": "Valeur de référence", "label-color": "Couleur", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Alignement vertical du symbole", "label-text-label": "Étiquette de texte", "label-x-offset": "Décalage X", - "label-y-offset": "Décalage Y" + "label-y-offset": "Décalage Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Opérateur de comparaison", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Propriété caractéristique", "placeholder-numeric-value": "Valeur numérique", "placeholder-value": "valeur" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "Couleur {{colorLabel}}" }, "confirm-button": { - "cancel": "Annuler" + "cancel": "Annuler", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Saisissez « {{confirmPromptText}} » pour confirmer" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "basculer pour réduire le panneau", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Annuler une requête", "tooltip-cancel-loading": "Annuler une requête", "tooltip-stop-streaming": "Arrêter la diffusion", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Respecter la casse", "inspect-drawer-title": "Inspecter la valeur", "inspect-menu-label": "Inspecter la valeur", + "nested-table": { + "no-data": "" + }, "no-values-label": "Aucune valeur", "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} sur {{numRows}} lignes" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Ajouter", + "placeholder-new-tag": "", "remove": "Supprimer la balise : {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Cliquez pour {{actionTitle}}", "footer-click-to-navigate": "Cliquez pour ouvrir {{linkTitle}}", "timestamp": "Horodatage" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Créer un panneau Bibliothèque" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Voulez-vous vraiment supprimer ce panneau ?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Utilisé sur {{count}} tableaux de bord" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Rechercher par nom ou par description" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Chargement du panneau de la bibliothèque..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Créez un lien direct personnalisé pour partager votre panneau au sein de votre organisation, avec les paramètres de personnalisation suivants :", "download-image": "Télécharger l'image", - "render-image": "Générer l'image", - "render-image-error": "Échec du rendu de l'image du panneau", - "render-image-error-description": "Une erreur s'est produite lors de la génération de l'image" + "render-image": "Générer l'image" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Enrouler les lignes" }, "log-details": { - "close": "Fermer les détails du journal", "fields": "Champs", "links": "Liens", "log-line": "Ligne du journal", @@ -8405,6 +8795,42 @@ "show-more": "afficher plus", "tooltip-error": "Erreur : {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Copier le lien vers la ligne de journal", "copy-log": "Copier la ligne de journal", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Données de liaison", "aria-label-fields": "Champs", + "aria-label-line": "", "aria-label-log-level": "Niveau de journal", "aria-label-no-details": "Aucune information" }, @@ -8598,6 +9030,8 @@ "updated-on": "Mise à niveau le" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Supprimer" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Importer un tableau de bord" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Cet outil permet de migrer certaines ressources de cette installation vers votre pile cloud. Pour commencer, vous devez créer un instantané de cette installation. La création d'un instantané prend généralement moins de deux minutes. L'instantané est stocké avec cette installation Grafana.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Le tableau de bord est déjà mis en service et géré par Grafana dans l'instance cloud. Nous vous recommandons d'utiliser le tableau de bord mis en service à l'avenir. Si vous souhaitez toujours copier le tableau de bord dans l'instance cloud, modifiez l'ID du tableau de bord dans le JSON du tableau de bord, puis enregistrez un nouvel instantané et téléchargez à nouveau.", "datasource-already-managed": "La source de données est déjà mise en service et gérée par Grafana dans l'instance cloud. S'il s'agit d'une ressource différente, définissez un autre UID et réessayez.", "datasource-invalid-url": "Il y a une source de données dont l'URL n'est pas valide. Fournissez une URL valide et réessayez.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": "> 100 nœuds", "aria-label-hidden-marker": "Marqueur de nœuds cachés : {{marker}}", - "node-count_one": "{{count}} nœud", + "node-count_one": "{{count}} nœuds", "node-count_other": "{{count}} nœuds" }, "node": { @@ -9318,9 +9763,9 @@ "aria-label-nodes-hidden-warning": "Avertissement de nœuds cachés", "computing-layout": "Calcul de la disposition", "no-data": "Aucune donnée", - "hidden-nodes_one": "<0> {{count}} nœud est masqué pour des raisons de performances.", + "hidden-nodes_one": "<0> {{count}} nœuds sont masqués pour des raisons de performances.", "hidden-nodes_other": "<0> {{count}} nœuds sont masqués pour des raisons de performances.", - "processed-nodes_one": "<0> La disposition en couches peut être lente avec {{count}} nœud.", + "processed-nodes_one": "<0> La disposition en couches peut être lente avec {{count}} nœuds.", "processed-nodes_other": "<0> La disposition en couches peut être lente avec {{count}} nœuds." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Sélectionner l’organisation" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Vous n’avez pas l’autorisation requise pour consulter cette page.", "title-access-denied": "Accès refusé" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Aucun composant de page d’application racine trouvé" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "mise à jour des plugins" }, "versions": { - "confirmation-text-1": "Voulez-vous vraiment rétrograder pour la version", - "confirmation-text-2": "Vous ne devriez normalement pas faire cela", + "confirmation-text": "", "downgrade-confirm": "Rétrograder de version", "downgrade-title": "Rétrograder à la version précédente du plugin" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Aucune extension trouvée" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Mise à jour en cours" }, "install-controls-button": { - "title-uninstall-modal": "Désinstaller {{plugin}}" + "title-uninstall-modal": "Désinstaller {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Ce plugin n’est pas publié sur <2>grafana.com/plugins et ne peut pas être géré via le catalogue.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Cette fonctionnalité est actuellement en cours de développement. Pour une expérience optimale et les dernières améliorations, nous vous recommandons d’utiliser la <2>compilation nocturne de Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Sélecteur de compte de service", "select-placeholder": "Commencez à saisir du texte pour rechercher des comptes de service" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Ajouter un jeton de compte de service", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Supprimer le compte de service", "disable-service-account": "Désactiver le compte de service", "enable-service-account": "Activer le compte de service", @@ -10907,6 +11375,7 @@ "used-by": "Utilisé par" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Modifier" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Ajouter un compte de service", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Rechercher un compte de service par son nom", "sub-title": "Les comptes de service et leurs jetons peuvent être utilisés pour s’authentifier auprès de l’API Grafana. Pour en savoir plus, consultez notre <2>documentation.", "title-delete-service-account": "Supprimer le compte de service", - "title-disable-service-account": "Désactiver le compte de service" + "title-disable-service-account": "Désactiver le compte de service", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Ce jeton a expiré", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exporter en tant que JSON", "share-externally-title": "Partager en externe", "share-internally-title": "Partager en interne", @@ -10972,6 +11448,19 @@ "view-button": "Afficher au format JSON", "view-button-yaml": "Afficher le fichier YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Créer un panneau de bibliothèque." }, @@ -10980,7 +11469,7 @@ "info-text": "Créez un lien direct vers ce tableau de bord ou ce panneau, personnalisé avec les options ci-dessous.", "link-url": "Lien URL", "render-alert": "Plugin de rendu d'image non installé", - "render-instructions": "Pour afficher une image de panneau, vous devez installer le <2>plugin de rendu d'image Grafana. Veuillez contacter votre administrateur Grafana pour installer le plugin.", + "render-instructions": "", "rendered-image": "Lien direct vers l'image dont il faut améliorer le rendu", "save-alert": "Le tableau de bord n'est pas enregistré", "save-dashboard": "Pour améliorer le rendu d'une image de panneau, vous devez d'abord enregistrer le tableau de bord.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Aperçu du panneau" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Trop de points pour visualiser correctement. <1>Mettez à jour la requête pour renvoyer moins de points. <3>({{count}} point reçu)", + "too-many-points_one": "Trop de points pour visualiser correctement. <1>Mettez à jour la requête pour renvoyer moins de points. <3>({{count}} points reçus)", "too-many-points_other": "Trop de points pour visualiser correctement. <1>Mettez à jour la requête pour renvoyer moins de points. <3>({{count}} points reçus)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Texte alternatif qui sera affiché si une image ne peut pas être affichée ou pour les utilisateurs qui utilisent un lecteur d’écran", "description-title-text": "Texte qui sera affiché lorsque l’image est survolée par un curseur", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Option de balise" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Sélecteur d’équipe", "select-placeholder": "Sélectionner une équipe" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Champ(s) ou numéro" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Pseudonyme", "label-mode": "Mode", "label-replace-all-fields": "Remplacer tous les champs" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Étiquette", "label-name": "Nom", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Cadre" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Ajouter un transformateur de type de champ de conversion", "aria-label-remove-convert-field-type-transformer": "Supprimer le transformateur de type de champ de conversion", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Navigateur", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Format d’entrée", "label-join-with": "Unir avec", "label-set-timezone": "Définir le fuseau horaire", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Type", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Utiliser un séparateur explicite lorsque de l’association des valeurs de tableau", "tooltip-input-format": "Spécifiez le format du champ de saisie afin que Grafana puisse analyser correctement la chaîne de date.", "tooltip-specify-the-output-format": "Spécifiez le format de sortie.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Désactivé" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Ajouter une valeur énumérée", "generate-enum-values-from-data": "Générer des valeurs énumérée à partir de données" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Supprimer" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Délimiteur", "label-format": "Format", "label-keep-time": "Conserver l’horaire", "label-replace-all-fields": "Remplacer tous les champs", "label-source": "Source", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Sélectionner le délimiteur...", "tooltip-regexp": "Exemple : {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Couleur du seuil" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Champ", - "label-lookup": "Recherche" + "label-lookup": "Recherche", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Paramètres supplémentaires", @@ -11848,12 +12413,26 @@ "use-as": "Utiliser comme" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "À partir de la variable", "label-identifier": "Identifiant", "label-use-variable": "Utiliser la variable", "label-variable": "Variable", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Modèle d’expression régulière" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Champ", "label-match": "Correspondance", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Ajouter une condition", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Conditions", - "label-filter-type": "Type de filtre" + "label-filter-type": "Type de filtre", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Champ", "label-format": "Format", "label-substring-range": "Plage de sous-chaînes de caractères" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Définir le fuseau horaire", @@ -11878,6 +12491,70 @@ "tooltip-format": "Le format de sortie pour le champ spécifié en tant que <2>chaîne de format Moment.js.", "tooltip-timezone-manually": "Définir manuellement le fuseau horaire de la date" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Navigateur", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignoré", "placeholder-select-stats": "Sélectionner les statistiques" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Les calculs n’auront aucun effet si aucun champ n’est groupé" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Si activés, les tableaux imbriqués afficheront les noms de champs comme en-tête de tableau", "label-show-field-names-in-nested-tables": "Afficher les noms de champs dans les tableaux imbriqués", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Les calculs n’auront aucun effet si aucun champ n’est groupé." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Valeur de cellule", "label-column": "Colonne", "label-empty-value": "Valeur nulle", "label-row": "Ligne", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Valeur booléenne fausse", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Auto", "placeholder-default": "Par défaut : 30", "placeholder-none": "Aucun" @@ -11947,7 +12654,16 @@ "label-as-percentile": "En tant que centile", "tooltip-transform-index-percentile": "Transformer l’index de ligne en centile." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "L’entrée ne contient aucune étiquette", "no-input-or-labels-found": "Aucune entrée (ou étiquette) trouvée", @@ -11969,10 +12685,30 @@ "label-labels": "Étiquettes", "label-mode": "Mode", "label-value-field-name": "Nom du champ de valeur", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Facultatif) Sélectionner l’étiquette", "tooltip-replace-value-field-label": "Remplacer le nom du champ de valeur par une étiquette" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Valeur ou variable" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Les valeurs des chaînes de caractères sont représentées sous forme de champs séparés plutôt que d’étiquettes" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "La fusion n’a aucun effet lorsqu’elle est appliquée sur une seule image." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "La fusion n’a aucun effet lorsqu’elle est appliquée sur une seule image.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Toutes les valeurs sont numériques", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Temps par ordre croissant" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Auto", "field-order-manual": "", - "first-frame-warning": "L’organisation des champs ne fonctionne qu’avec un seul cadre. Envisagez d’appliquer une transformation d’union ou de filtrer d’abord les données d’entrée." + "first-frame-warning": "L’organisation des champs ne fonctionne qu’avec un seul cadre. Envisagez d’appliquer une transformation d’union ou de filtrer d’abord les données d’entrée.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Définit la façon dont les noms des champs sélectionnés sont affichés. Comme le nom du cadre est généralement préférable pour les données tabulaires" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "La partition par valeurs ne fonctionne qu’avec un seul cadre.", "tooltip-keeps-partition-fields-frames": "Conserve les champs de partition dans les cadres" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Infos" + "label-info": "Infos", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "et", @@ -12035,6 +12815,9 @@ "label-operation": "Opération" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Calculs", "label-include-time": "Inclure l’heure", "label-labels-to-fields": "Étiquettes aux champs", @@ -12049,12 +12832,23 @@ "series-to-rows": "Séries en lignes" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Choisir la statistique" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Valeur ou variable", "variable-regex-documentation": "Formate la variable à valeurs multiples en une chaîne regex" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Cubique", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Nombre de points X, Y à prédire" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Correspondance", "label-replace": "Remplacer", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Champ", - "label-mode": "Mode" + "label-mode": "Mode", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Champ", "label-reverse": "Inverser", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Sélectionner le champ" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transformer" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Champ horaire", @@ -12121,10 +12960,34 @@ "placeholder-value": "Valeur", "tooltip-name-for-value-fields": "Nom des champs de valeur" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Opération", "placeholder-field": "Champ" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Champs de valeurs multiples de tout type", "single-frame": "Cadre simple", @@ -12177,6 +13040,7 @@ "title": "Organisations" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Sélecteur d’utilisateur", "select-placeholder": "Commencez à saisir du texte pour rechercher un utilisateur" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Supprimer la variable" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Actualiser" }, "query-variable-sort-select": { - "description-values-variable": "Comment trier les valeurs de cette variable" + "description-values-variable": "Comment trier les valeurs de cette variable", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "valeur par défaut, le cas échéant", "text-options": "Options de texte" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Nom d’affichage facultatif", "description-template-variable-characters": "Le nom de la variable du modèle. (50 caractères max)", "general": "Général", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texte de description", "placeholder-label-name": "Nom de l’étiquette", "placeholder-variable-name": "Nom de la variable", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Dupliquer la variable", "tooltip-remove-variable": "Supprimer la variable" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Basculer toutes les valeurs" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Afficher les usages" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index cc9c3a2967b..b6df8afa686 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Egyes funkciók stabilak (GA), és alapértelmezés szerint engedélyezve vannak, míg mások jelenleg előzetes béta fázisban vannak, és korai bevezetéshez állnak rendelkezésre.", "confirm-modal-body-2": "Javasoljuk, hogy a módosítások végrehajtása előtt ismerje meg az egyes funkciók módosításának következményeit.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Béta", "content-general-availability": "Általános elérhetőség", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Szervezet törlése", + "confirmText-delete": "", "title-delete": "Törlés" }, "anon-users": { "not-found": "Nem található névtelen felhasználó." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Kijelentkezés kényszerítése minden eszközön" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Nincs engedélye a felhasználók megtekintésére ebben a szervezetben. A szervezet frissítéséhez forduljon a kiszolgáló rendszergazdájához.", "heading": "Szervezet szerkesztése", @@ -208,9 +216,11 @@ "not-editable": "Ennek a felhasználónak a szerepköre nem szerkeszthető, mert szinkronizálva van az Ön hitelesítési szolgáltatójával. További részletekért olvassa el a <1>Grafana hitelesítési dokumentációját." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Szerepkör" }, + "confirmText-delete": "", "delete-aria-label": "Felhasználó törlése: {{name}}", "title-delete": "Törlés" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Ezek a rendszerbeállítások a grafana.ini vagy a custom.ini fájlban vannak meghatározva (vagy felülbírálva az ENV-változókban). Ezek módosításához jelenleg újra kell indítania a Grafanát." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Vállalati licenc" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Módosítás", + "confirmText-change": "", "grafana-admin-key": "Grafana-rendszergazda", "grafana-admin-no": "Nem", "grafana-admin-yes": "Igen", "title": "Engedélyek" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Felhasználó törlése", "disable-button": "Felhasználó letiltása", "edit-button": "Szerkesztés", @@ -312,6 +330,9 @@ "title-delete-user": "Felhasználó törlése", "title-disable-user": "Felhasználó letiltása" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Böngésző és operációs rendszer", "force-logout-all-button": "Kijelentkezés kényszerítése minden eszközön", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Aktív időzítések" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Továbbiak hozzáadása" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Némítás, csoportosítás és időzítés (opcionális)", "title-muting-grouping-and-timings": "Némítás, csoportosítás és időzítés" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Hivatkozás másolása", "duplicate": "Duplikálás", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML szerkesztése", @@ -546,6 +577,7 @@ "view-configuration": "Nézet konfigurációja" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "A belső Grafana riasztáskezelő-konfiguráció manuálisan nem módosítható. A konfiguráció módosításához szerkessze az egyes erőforrásokat a felhasználói felületen keresztül.", "gma-manual-configuration-is-not-supported": "A manuális konfigurációs módosítások nem támogatottak", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Riasztáskezelő konfigurációjának visszaállítása" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Összehasonlítás", "restore": "Visszaállítás", "text-latest": "Legújabb" }, + "confirmText-yes-restore-configuration": "", "loading": "Betöltés...", "no-previous-configurations": "Nincsenek előző konfigurációk", "this-might-take-a-while": "Ez hosszabb időt is igénybe vehet…", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "További műveletek a(z) „{{contactPointName}}” kapcsolattartási ponthoz", + "ariaLabel-delete": "", "button-edit": "Szerkesztés", "button-view": "Nézet", + "export-ariaLabel-export": "", "export-label-export": "Exportálás", "label-delete": "Törlés", "label-manage-permissions": "Engedélyek kezelése", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Megoldott üzenet letiltása" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Pozitív egész számnak kell lennie.", "must-enter-a-group-name": "A csoportnév megadása kötelező" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Hiba történt az adatforrás adatainak betöltésekor", "edit": "Szerkesztés", - "evaluations-to-fire": "Aktiválandó értékelési ciklusok", "export": "Exportálás", "folder": "Mappa", "group-loading-error": "Hiba a csoport betöltésekor", "interval": "Intervallum", "namespace": "Névtér", - "pending-period": "Függőben lévő időszak", - "recording": "Felvételkészítés", - "rule-name": "Szabálynév" + "new": "" }, "group-edit": { "ds-error": "Hiba történt az adatforrás adatainak betöltésekor", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Kezdés", "label-insights": "Adatok", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Riasztás" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Egyéb adatforrások" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Letiltva", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "Az értesítési házirendfáját egy másik felhasználó frissítette.", "error-code": "Hibaüzenet: „{{error}}”", - "fallback": "Valami hiba történt az értesítési házirendjei frissítése során.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Kérjük, frissítse az oldalt, és próbálkozzon újra.", - "title": "Hiba történt az értesítési házirend mentésekor" + "title": "" }, "n-more-policies_one": "{{count}} további házirend", "n-more-policies_other": "{{count}} további házirend" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Lekérdezés hozzáadása", "body-queries-expressions-configured": "Hozzon létre legalább egy lekérdezést vagy kifejezést, amelyre riasztást kap", + "confirmText-deactivate": "", "expressions": "Kifejezések", "loading-data-sources": "Adatforrások betöltése…", "manipulate-returned-queries-other-operations": "A lekérdezésekből visszaadott adatok manipulálása matematikai és egyéb műveletekkel.", @@ -2184,6 +2237,9 @@ "label-export-all": "Összes exportálása" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Nem sikerült betölteni a lekérdezésszerkesztőt a következők miatt: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "A másolt szabályhoz új értékelési csoportot kell beállítania, mert az eredeti ki van építve, és nem használható a felhasználói felületen létrehozott szabályokhoz.", "body-not-provisioned": "Az új szabály <1>nem lesz kiépített szabályként megjelölve.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Kiépített riasztási szabály másolása" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Riasztási szabály vizsgálata" }, "rule-list": { - "cannot-find-rule-details-for": "A szabály részletei nem találhatók a(z) {{uid}} UID-hez", - "cannot-load-rule-details-for": "A szabály részletei nem tölthetők be a(z) {{uid}} UID-hez", "configure-datasource": "Konfigurálás", "draft-new-rule": "Új szabály felvázolása", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Nem találja a kívánt adatforrást? Egyes adatforrások nem támogatottak a riasztáshoz. További információért kattintson az ikonra." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Új riasztáskezelő hozzáadása", "builtin-alertmanager": "Beépített riasztáskezelő", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Válasszon értesítési sablont", "loading": "Betöltés...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Értesítési sablon kiválasztása" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Műveletek", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Nincs meghatározott sablon.", "template-group": "Sabloncsoport", "title-delete-template-group": "Sabloncsoport törlése" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "(ennyi ideje: {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "A célmappa nem üres, néhány szabály felül lesz írva vagy el lesz távolítva. Biztosan importálja ezeket a riasztási szabályokat a Grafana által felügyelt szabályokba?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Kapcsolattartási pont törlése" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Értesítési házirend törlése" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Jegyzet", "first-value": "Első érték", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Jegyzetlekérdezés hozzáadása", @@ -3067,6 +3143,16 @@ "info-box-content-2": "További információért tekintse meg a <2>Jegyzetek dokumentációját.", "title": "Még nincsenek hozzáadva egyéni jegyzetlekérdezések" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} esemény ({{numFields}} mezőből)", "no-events-found": "Nem található esemény", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Engedélyezett csapatazonosítók meghatározása", "display-name-description": "A bejelentkezési oldalon „Bejelentkezés ezzel” felirattal jelenik meg. Hasznos, ha egynél több identitásszolgáltatót vagy SSO-protokollt használ.", "display-name-label": "Megjelenített név", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Az OAuth2-azonosítótoken attribútumtérképén belül a felhasználói e-mail-kereséshez használandó kulcs neve.", "email-attribute-name-label": "E-mail-attribútum neve", "email-attribute-path-description": "A felhasználói adatok alapján végzett felhasználói e-mail-kereséshez használandó JMESPath-kifejezés.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Csapatazonosítók egész számos listája.", "team-ids-label": "Csapatazonosítók", "team-ids-numbers": "A csapatazonosítóknak számoknak kell lenniük.", - "team-ids-other": "Csapatazonosítók karakterláncos listája.", + "team-ids-other": "", "team-ids-placeholder": "Adja meg a csapatazonosítókat, és nyomja le az Enter billentyűt a hozzáadáshoz", "teams-url-description": "A csapatazonosítók lekérdezéséhez használt URL-cím. Ha nincs beállítva, az alapértelmezett érték a /teams.", "teams-url-description-oauth": "Ha konfigurálja a(z) „{{ teamsURLLabel }}” elemet, konfigurálni kell a(z) „{{ teamIDsAttributePathLabel }}” elemet is.", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Visszaállítás az alapértelmezett értékekre" }, + "confirmText-reset": "", "disable": "Letiltás", "disabling": "Letiltás folyamatban…", "discard": "Elvetés", @@ -3473,11 +3563,11 @@ "button-create": "Létrehozás", "button-creating": "Létrehozás…", "cancel": "Mégse", - "error-invalid-folder-name": "Érvénytelen mappanév", "error-required": "A mappa nevének megadása kötelező", "folder-name-input-placeholder-enter-folder-name": "Mappanév megadása", "label-folder-name": "Mappa neve", "text-pull-request-created": "A lekérési kérelem ennek a mappának a módosításával lett létrehozva:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Lekérési kérelem létrehozva", "title-this-repository-is-read-only": "Ez az adattár csak olvasható" }, @@ -3895,6 +3985,7 @@ "tooltip": "Bezárás" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Hiba történt az opciók betöltése során." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Dinamikusan kiszámítja az intervallumot úgy, hogy az időtartományt elosztja a megadott számmal" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Tovább a külső webhelyre?" } }, "render-left-actions": { - "text-public": "Nyilvános", "tooltip-view-as-scene": "Megtekintés jelenetként" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Az irányítópult betöltése sikertelen volt" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Az irányítópult lekérése nem sikerült" @@ -4540,6 +4641,13 @@ "editable": "Szerkeszthető", "readonly": "Csak olvasható" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Adatok átalakítása" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Biztosan visszaállítja az irányítópultot a(z) {{version}} verzióra? Az összes nem mentett módosítás elveszik.", + "confirmText-restore-version": "", "title-restore-version": "Verzió visszaállítása" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Ez a cím nem egyedi" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Mentés másként" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Egy irányítópult már létezik ugyanazzal a névvel a kijelölt mappában.<1><2>Biztosan menti ezt az irányítópultot?", "body-version-mismatch": "Valaki más frissítette ezt az irányítópultot<1><2>Biztosan menti ezt az irányítópultot?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Ütközés", "title-version-mismatch": "Ütközés" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Ugrás a következő irányítópulthoz", "playlist-previous": "Ugrás az előző irányítópulthoz", "playlist-stop": "Lejátszási lista leállítása", - "public-dashboard": "Nyilvános", "refresh": "Irányítópult frissítése", "save": "Irányítópult mentése", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Transzformáció alkalmazása erre:" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Hibakeresés", "title-disable-transformation": "Transzformáció letiltása", "title-filter": "Szűrő", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Transzformáció keresése", "show-images": "Képek megjelenítése", - "title-add-another-transformation": "Másik transzformáció hozzáadása" + "title-add-another-transformation": "Másik transzformáció hozzáadása", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Másik transzformáció hozzáadása" }, + "confirmText-delete-all": "", "delete-all-transformations": "Összes transzformáció törlése", "title-delete-all-transformations": "Törli az összes transzformációt?", "tooltip-clear-search": "Keresés törlése", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Verzió kijelölésének ki- és bekapcsolása: {{version}}", "date": "Dátum", + "name-latest": "", "notes": "Megjegyzések", "restore": "Visszaállítás", "updated-by": "Frissítette:", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Lehetővé teszi a felhasználók számára, hogy egyéni értékeket adjanak a listához", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Méretek megadása CSV-ként: {{name}}, {{value}}", "label-data-source": "Adatforrás", - "label-use-static-key-dimensions": "Statikus kulcsméretek használata" + "label-use-static-key-dimensions": "Statikus kulcsméretek használata", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Nyilvános URL-cím visszavonása" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Egyéni opciók", + "name-values-separated-comma": "", "selection-options": "Kijelölés beállításai" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Típus", "label-url": "URL-cím", "label-with-tags": "Címkékkel", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Irányítópult megnyitása" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Típus" }, "dashboard-preview-banner": { - "not-saved": "Az érték még nincs mentve a Grafana-adatbázisban", "not-yet-saved": "Az érték nincs mentve a Grafana-adatbázisban", - "open-pull-request-in-git-hub": "Összefésülési kérelem megnyitása a GitHubon", - "title-dashboard-loaded-branch-git-hub": "Ez az irányítópult a GitHub egyik ágából töltődik be.", "title-dashboard-loaded-external-repository": "Ez az irányítópult külső adattárból töltődik be", - "title-dashboard-loaded-request-git-hub": "Ez az irányítópult a GitHub egyik összefésülési kérelméből töltődik be.", - "title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor", - "value-not-saved": "Az érték még nincs mentve a Grafana-adatbázisban", - "view-pull-request-in-git-hub": "Összefésülési kérelem megtekintése a GitHubon" + "title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Adatforrás beállításai", "description-instance-name-filter": "Reguláris kifejezéses szűrő, amelyhez adatforráspéldányokat választhat a változóérték-listában. Hagyja üresen minden esetben.", "example-instance-name-filter": "Példa: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Kijelölés beállításai" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Transzformáció hozzáadása" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Oszlopbeállítások", "row-height-options": "Sormagasság beállításai" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Méretek megadása CSV-ként: {{name}}, {{value}}", "group-by-options": "Csoportosítási beállítások", "label-data-source": "Adatforrás", - "label-use-static-group-by-dimensions": "Statikus csoportméretek használata" + "label-use-static-group-by-dimensions": "Statikus csoportméretek használata", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Másolás vágólapra", @@ -5521,9 +5710,14 @@ "apply": "Alkalmaz" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "A számított érték nem csökkenhet e küszöbérték alá", "description-step-count": "Hányszor kell az aktuális időtartományt elosztani az érték kiszámításához?", - "interval-options": "Intervallum beállításai" + "interval-options": "Intervallum beállításai", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "{{count}} irányítópulton használatos", "usage-count_other": "{{count}} irányítópulton használatos" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Egy irányítópult már létezik ugyanazzal a névvel a kijelölt mappában. Biztosan menti ezt az irányítópultot?", "title-name-already-exists": "A név már létezik" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Tovább a külső webhelyre?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Másik transzformáció hozzáadása", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Összes transzformáció törlése", "title-delete-all-transformations": "Törli az összes transzformációt?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Az elnevezett rögzítési csoportok felhasználhatók a megjelenített szöveg és érték elválasztására (<1>lásd a példákat).", "description-optional": "Opcionális, ha egy sorozatnév vagy metrikai csomópontszegmens egy részét szeretné kinyerni.", "label-data-source": "Adatforrás", + "label-static-options-sort": "", "label-target-data-source": "Céladatforrás", + "label-use-static-options": "", + "name-regex": "", "query-options": "Lekérdezési beállítások", - "selection-options": "Kijelölés beállításai" + "selection-options": "Kijelölés beállításai", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Biztosan visszaállítja az irányítópultot a(z) {{version}} verzióra? Az összes nem mentett módosítás elveszik.", + "confirmText-restore-version": "", "title-restore-version": "Verzió visszaállítása" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Mappa", "render-footer": { "title-failed-to-save-dashboard": "Az irányítópult mentése sikertelen volt" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Lehetővé teszi több érték egyidejű kijelölését", "description-enables-option-include-variables": "Lehetővé teszi az összes érték belefoglalását", - "description-enables-users-custom-values": "Lehetővé teszi a felhasználók számára, hogy egyéni értékeket adjanak a listához" + "description-enables-users-custom-values": "Lehetővé teszi a felhasználók számára, hogy egyéni értékeket adjanak a listához", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Menümegosztás ki- és bekapcsolása" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Nem sikerült a vágólapra másolás" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opcionális)", "text-options": "Szövegbeállítások" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Biztosan leválasztja ezt a panelt?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Erre a változóra egyetlen változó vagy irányítópult sem hivatkozik.", "aria-label-variable-referenced-other-variables-dashboard": "Erre a változóra más változó vagy irányítópult hivatkozik.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Változószerkesztő űrlap", "back-to-list": "Vissza a listához", + "confirmText": { + "delete-variable": "" + }, "delete": "Törlés", "description-optional-display-name": "Opcionális megjelenítendő név", "description-template-variable-characters": "A sablonváltozó neve. (Max. 50 karakter)", "general": "Általános", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Leírószöveg", "placeholder-label-name": "Címkenév", "placeholder-variable-name": "Változónév", @@ -5829,13 +6066,25 @@ "variable": "Változó" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Változó törlése", "tooltip-duplicate-variable": "Duplikált változó", "tooltip-remove-variable": "Változó eltávolítása" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Elrejtés" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Használat megjelenítése a következőhöz: {{variableId}}", "tooltip-show-usages": "Használatok megjelenítése" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Verzió kijelölésének ki- és bekapcsolása: {{version}}", "date": "Dátum", + "name-latest": "", "notes": "Megjegyzések", "restore": "Visszaállítás", "updated-by": "Frissítette:", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Feltöltés" @@ -6287,6 +6538,7 @@ }, "label-limit": "Korlát", "label-value": "Érték", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max.", "label-min": "Min.", - "label-value": "Érték" + "label-value": "Érték", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Válasszon operátort a szolgáltatásnévhez", "aria-label-select-span-name": "Válasszon terjedelemnevet", "aria-label-select-span-name-operator": "Terjedelemnév operátor kijelölése", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Terjedelemszűrők", "label-duration": "Időtartam", "label-service-name": "Szolgáltatásnév", @@ -6939,6 +7199,8 @@ "split-widen": "Ablaktábla szélesítése" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Visszajelzés küldése", "label-copied": "Kimásolva!", "label-export": "Exportálás", @@ -7046,6 +7308,7 @@ "when": "WHEN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Művelet" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Mappák törlése", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Mappaszűrő", "select-placeholder": "Szűrés mappa alapján" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Sajnos nem sikerült teljesíteni a kérését. Próbálkozzon újra.", "send-custom-feedback": "Küldés" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Szélességi fok", "label-longitude": "Hosszúsági fok" @@ -7153,6 +7463,14 @@ "center": "Középre:", "zoom": "Nagyítás:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Réteg" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Geomap-stílusszabály hozzáadása" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Réteg hozzáadása", "no-layers": "Nincs réteg?" @@ -7185,16 +7511,38 @@ "label-zoom": "Nagyítás", "use-current-map-settings": "Aktuális térképbeállítások használata" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Szimbólum" }, "measure-overlay": { "tooltip-show-measure-tools": "Mérőeszközök megjelenítése" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Az alaptérkép rétegét a kiszolgáló rendszergazdája konfigurálja." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Illesztés", "label-baseline": "Alapvonal", "label-color": "Szín", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Szimbólum függőleges igazítása", "label-text-label": "Szöveges címke", "label-x-offset": "X eltolás", - "label-y-offset": "Y eltolás" + "label-y-offset": "Y eltolás", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Összehasonlítás műveleti jele", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Funkciótulajdonság", "placeholder-numeric-value": "Számérték", "placeholder-value": "érték" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "{{colorLabel}} szín" }, "confirm-button": { - "cancel": "Mégse" + "cancel": "Mégse", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "„{{confirmPromptText}}” beírása szükséges a megerősítéshez" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "panel összecsukásának ki- és bekapcsolása", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "A lekérdezés megszakítása", "tooltip-cancel-loading": "A lekérdezés megszakítása", "tooltip-stop-streaming": "Adatfolyam leállítása", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Nagybetűérzékeny", "inspect-drawer-title": "Érték ellenőrzése", "inspect-menu-label": "Érték ellenőrzése", + "nested-table": { + "no-data": "" + }, "no-values-label": "Nincsenek értékek", "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} / {{numRows}} sor" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Hozzáadás", + "placeholder-new-tag": "", "remove": "Címke eltávolítása: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Kattintson ehhez: {{actionTitle}}", "footer-click-to-navigate": "Kattintson a következő megnyitásához: {{linkTitle}}", "timestamp": "Időbélyeg" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Könyvtárpanel létrehozása" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Biztosan törli ezt a panelt?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "{{count}} irányítópulton használatos" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Keresés név vagy leírás alapján" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Könyvtárpanel betöltése…" @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Létrehozhat egy személyre szabott, közvetlen hivatkozást a panel szervezeten belüli megosztásához, a következő testreszabási beállításokkal:", "download-image": "Kép letöltése", - "render-image": "Kép generálása", - "render-image-error": "Panel képének renderelése sikertelen", - "render-image-error-description": "Hiba történt a kép generálása során" + "render-image": "Kép generálása" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Sortörés" }, "log-details": { - "close": "Napló részleteinek bezárása", "fields": "Mezők", "links": "Hivatkozások", "log-line": "Naplósor", @@ -8405,6 +8795,42 @@ "show-more": "több megjelenítése", "tooltip-error": "Hiba: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Hivatkozás másolása a naplósorba", "copy-log": "Naplósor másolása", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Adathivatkozások", "aria-label-fields": "Mezők", + "aria-label-line": "", "aria-label-log-level": "Naplózási szint", "aria-label-no-details": "Nincsenek részletek" }, @@ -8598,6 +9030,8 @@ "updated-on": "Frissítve:" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Törlés" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Irányítópult importálása" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Ez az eszköz áttelepíthet bizonyos erőforrásokat ebből a telepített példányból a felhőstackbe. A kezdéshez létre kell hoznia egy pillanatfelvételt erről a telepített példányról. A pillanatfelvétel létrehozása általában kevesebb mint két percet vesz igénybe. A pillanatfelvételt ezen Grafana-telepítés mellett tárolja a rendszer.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Az irányítópultot a Grafana már kiépítette és felügyeli a felhőpéldányban. Javasoljuk, hogy a továbbiakban használja a kiépített irányítópultot. Ha továbbra is át szeretné másolni az irányítópultot a felhőpéldányba, akkor módosítsa az irányítópult azonosítóját az irányítópult JSON-fájljában, mentse az új pillanatképet, és töltse fel újra.", "datasource-already-managed": "Az adatforrást a Grafana már kiépítette és felügyeli a felhőpéldányban. Ha ez egy másik erőforrás, állítson be egy másik UID-t, és próbálkozzon újra.", "datasource-invalid-url": "Egy adatforrás érvénytelen URL-címmel rendelkezik. Adjon meg egy érvényes URL-címet, és próbálkozzon újra.", @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Szervezet kijelölése" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Nincs engedélye az oldal megtekintésére.", "title-access-denied": "Hozzáférés megtagadva" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Nem található a gyökéralkalmazás oldalkomponense" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "frissített bővítmény" }, "versions": { - "confirmation-text-1": "Biztosan visszalép erre a verzióra:", - "confirmation-text-2": "Általában nem lenne szabad ezt tennie", + "confirmation-text": "", "downgrade-confirm": "Visszalépés", "downgrade-title": "Visszalépés korábbi bővítményverzióra" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Nem található bővítmény" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Frissítés" }, "install-controls-button": { - "title-uninstall-modal": "{{plugin}} eltávolítása" + "title-uninstall-modal": "{{plugin}} eltávolítása", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Ez a bővítmény nincs közzétéve a <2>grafana.com/plugins weboldalon, és nem kezelhető a katalóguson keresztül.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Ez a funkció jelenleg aktív fejlesztés alatt áll. A legjobb élmény és a legújabb fejlesztések érdekében javasoljuk a Grafana <2>éjszakai buildjének használatát." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Szolgáltatási fiókválasztó", "select-placeholder": "Kezdjen el gépelni a szolgáltatási fiókok kereséséhez" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Szolgáltatási fióktoken hozzáadása", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Szolgáltatási fiók törlése", "disable-service-account": "Szolgáltatási fiók letiltása", "enable-service-account": "Szolgáltatási fiók engedélyezése", @@ -10907,6 +11375,7 @@ "used-by": "Felhasználta:" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Szerkesztés" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Szolgáltatási fiók hozzáadása", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Szolgáltatási fiók keresése név alapján", "sub-title": "A szolgáltatási fiókok és tokenjeik felhasználhatók a Grafana API-val történő hitelesítésre. További információkat a <2>dokumentációnkban talál.", "title-delete-service-account": "Szolgáltatási fiók törlése", - "title-disable-service-account": "Szolgáltatási fiók letiltása" + "title-disable-service-account": "Szolgáltatási fiók letiltása", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "A token lejárt", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exportálás JSON-fájlként", "share-externally-title": "Külső megosztás", "share-internally-title": "Belső megosztás", @@ -10972,6 +11448,19 @@ "view-button": "JSON megtekintése", "view-button-yaml": "YAML megtekintése" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Könyvtárpanel létrehozása." }, @@ -10980,7 +11469,7 @@ "info-text": "Létrehozhat egy közvetlen hivatkozást ehhez az irányítópulthoz vagy panelhez, az alábbi lehetőségekkel testreszabva.", "link-url": "Hivatkozás URL-címe", "render-alert": "Az Image Renderer bővítmény nincs telepítve", - "render-instructions": "Panelkép rendereléséhez telepítenie kell a <2>Grafana Image Renderer bővítményt. Kérjük, vegye fel a kapcsolatot Grafana-rendszergazdájával a bővítmény telepítéséhez.", + "render-instructions": "", "rendered-image": "Renderelt kép közvetlen hivatkozása", "save-alert": "Az irányítópult nincs mentve", "save-dashboard": "Panelkép rendereléséhez először mentenie kell az irányítópultot.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Panel előnézete" }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Helyettesítő szöveg, amely akkor jelenik meg, ha egy kép nem jeleníthető meg, és amely a képernyőolvasót használó felhasználóknak hasznos", "description-title-text": "Szöveg, amely akkor jelenik meg, amikor a kép fölé viszik a kurzort", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Címkézési opció" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Csapatválasztó", "select-placeholder": "Csapat kijelölése" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Mező(k) vagy szám" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Üzemmód", "label-replace-all-fields": "Összes mező cseréje" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Címke", "label-name": "Név", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Keret" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Konvertálandó mezőtípusú transzformátor hozzáadása", "aria-label-remove-convert-field-type-transformer": "Konvertálandó mezőtípusú transzformátor eltávolítása", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Böngésző", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Bemeneti formátum", "label-join-with": "Csatlakozás ezzel:", "label-set-timezone": "Időzóna beállítása", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Típus", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Explicit elválasztó használata tömbértékek egyesítésekor", "tooltip-input-format": "Adja meg a beviteli mező formátumát, hogy a Grafana helyesen tudja elemezni a dátumkarakterláncot.", "tooltip-specify-the-output-format": "Adja meg a kimeneti formátumot.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Ki" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Felsorolt érték hozzáadása", "generate-enum-values-from-data": "Felsorolt értékek generálása adatokból" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Törlés" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Elválasztójel", "label-format": "Formátum", "label-keep-time": "Megőrzési idő", "label-replace-all-fields": "Összes mező cseréje", "label-source": "Forrás", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automatikus", "placeholder-select-delimiter": "Elválasztójel kijelölése…", "tooltip-regexp": "Példa: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Küszöb színe" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Mező", - "label-lookup": "Keresés" + "label-lookup": "Keresés", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "További beállítások", @@ -11848,12 +12413,26 @@ "use-as": "Felhasználás módja:" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Változóból", "label-identifier": "Azonosító", "label-use-variable": "Változó használata", "label-variable": "Változó", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "A reguláris kifejezés mintája" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Mező", "label-match": "Egyezés", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Feltétel hozzáadása", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Feltételek", - "label-filter-type": "Szűrő típusa" + "label-filter-type": "Szűrő típusa", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Mező", "label-format": "Formátum", "label-substring-range": "Részkarakterlánc-tartomány" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formátum", "label-set-timezone": "Időzóna beállítása", @@ -11878,6 +12491,70 @@ "tooltip-format": "A <2>Moment.js formátumú karakterlánc mező kimeneti formátuma.", "tooltip-timezone-manually": "A dátum időzónájának manuális beállítása" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Böngésző", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Mellőzve", "placeholder-select-stats": "Statisztikák kijelölése" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "A számításoknak nem lesz hatásuk, ha nincsenek csoportosított mezők" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Ha engedélyezve van, a beágyazott táblázatok a mezőneveket táblázatfejlécként jelenítik meg", "label-show-field-names-in-nested-tables": "Mezőnevek megjelenítése a beágyazott táblázatokban", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "A számításoknak nem lesz hatásuk, ha a mezők nincsenek csoportosítva." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Cellaérték", "label-column": "Oszlop", "label-empty-value": "Üres érték", "label-row": "Sor", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Logikai hamis érték", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automatikus", "placeholder-default": "Alapértelmezett: 30", "placeholder-none": "Nincs" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Percentilisként", "tooltip-transform-index-percentile": "Transzformálja a sorindexet percentilisként." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "A bemenet nem tartalmaz címkéket", "no-input-or-labels-found": "Nem található bemenet (vagy címke)", @@ -11969,10 +12685,30 @@ "label-labels": "Címkék", "label-mode": "Üzemmód", "label-value-field-name": "Értékmező neve", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opcionális) Címke kijelölése", "tooltip-replace-value-field-label": "Cserélje le az értékmező nevét egy címkére" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Érték vagy változó" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "A karakterláncértékek címkék helyett különálló mezőkként jelennek meg" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Egyetlen keretre alkalmazva nincs hatása az egyesítésnek." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Egyetlen keretre alkalmazva nincs hatása az egyesítésnek.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Minden érték numerikus", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Idő növekvő sorrendben" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automatikus", "field-order-manual": "", - "first-frame-warning": "A mezők rendszerezése csak egyetlen kerettel működik. Először érdemes lehet egyesítési transzformációt alkalmazni, vagy szűrni a bemenetet." + "first-frame-warning": "A mezők rendszerezése csak egyetlen kerettel működik. Először érdemes lehet egyesítési transzformációt alkalmazni, vagy szűrni a bemenetet.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Beállítja, hogyan jelenjen meg a kijelölt mezők neve. Mivel a keret neve általában jobb a táblázatos adatokhoz" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Az értékek szerinti partíció csak egyetlen kerettel működik.", "tooltip-keeps-partition-fields-frames": "A partíciós mezőket a keretekben tartja" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formátum", - "label-info": "Információ" + "label-info": "Információ", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "és", @@ -12035,6 +12815,9 @@ "label-operation": "Művelet" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Számítások", "label-include-time": "Idővel együtt", "label-labels-to-fields": "Címkék mezőkhöz", @@ -12049,12 +12832,23 @@ "series-to-rows": "Sorozatok sorokká alakítása" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Válasszon statisztikát" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Érték vagy változó", "variable-regex-documentation": "Többértékű változót formáz egy reguláris kifejezéses karakterlánccá" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Harmadfokú", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Előre jelzendő X,Y pontok száma" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Egyezés", "label-replace": "Csere", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Mező", - "label-mode": "Üzemmód" + "label-mode": "Üzemmód", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Mező", "label-reverse": "Fordított", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Mező kijelölése" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Átalakítás" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Statisztika", "label-time-field": "Időmező", @@ -12121,10 +12960,34 @@ "placeholder-value": "Érték", "tooltip-name-for-value-fields": "Az értékmezők neve" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Művelet", "placeholder-field": "Mező" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Bármilyen típusú több értékmező", "single-frame": "Egyetlen keret", @@ -12177,6 +13040,7 @@ "title": "Szervezetek" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Felhasználóválasztó", "select-placeholder": "Kezdjen el gépelni a felhasználó kereséséhez" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Változó törlése" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Frissítés" }, "query-variable-sort-select": { - "description-values-variable": "Hogyan kell rendezni ennek a változónak az értékeit?" + "description-values-variable": "Hogyan kell rendezni ennek a változónak az értékeit?", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "alapértelmezett érték, ha van", "text-options": "Szövegbeállítások" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Opcionális megjelenítendő név", "description-template-variable-characters": "A sablonváltozó neve. (Max. 50 karakter)", "general": "Általános", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Leírószöveg", "placeholder-label-name": "Címkenév", "placeholder-variable-name": "Változónév", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Duplikált változó", "tooltip-remove-variable": "Változó eltávolítása" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Összes érték ki- és bekapcsolása" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Használatok megjelenítése" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index f63f0f6b315..146cc402af0 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Beberapa fitur stabil (GA) dan diaktifkan secara default, sedangkan beberapa fitur lainnya saat ini dalam fase Beta awal, tersedia untuk penggunaan awal.", "confirm-modal-body-2": "Sebaiknya pahami implikasi dari setiap perubahan fitur sebelum melakukan modifikasi.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Ketersediaan umum", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Hapus org", + "confirmText-delete": "", "title-delete": "Hapus" }, "anon-users": { "not-found": "Pengguna anonim tidak ditemukan." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Paksa keluar dari semua perangkat" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Anda tidak memiliki izin untuk melihat pengguna di organisasi ini. Untuk memperbarui organisasi ini, hubungi administrator server Anda.", "heading": "Edit Organisasi", @@ -208,9 +216,11 @@ "not-editable": "Peran pengguna ini tidak dapat diedit karena disinkronkan dari penyedia autentikasi Anda. Lihat <1> dokumen autentikasi Grafana untuk detailnya." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Peran" }, + "confirmText-delete": "", "delete-aria-label": "Hapus pengguna: {{name}}", "title-delete": "Hapus" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Pengaturan sistem ini ditentukan dalam grafana.ini atau custom.ini (atau ditimpa dalam variabel ENV). Untuk mengubahnya, saat ini Anda perlu memulai ulang Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Lisensi Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Ubah", + "confirmText-change": "", "grafana-admin-key": "Admin Grafana", "grafana-admin-no": "Tidak", "grafana-admin-yes": "Ya", "title": "Izin" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Hapus pengguna", "disable-button": "Nonaktifkan pengguna", "edit-button": "Edit", @@ -312,6 +330,9 @@ "title-delete-user": "Hapus pengguna", "title-disable-user": "Nonaktifkan pengguna" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Browser dan OS", "force-logout-all-button": "Paksa keluar dari semua perangkat", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Pengaturan waktu aktif" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Tambah lagi" }, @@ -453,6 +478,9 @@ "label-muting-grouping-and-timings-optional": "Pembisuan, pengelompokan, dan pengaturan waktu (opsional)", "title-muting-grouping-and-timings": "Pembisuan, pengelompokan, dan pengaturan waktu" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Salin tautan", "duplicate": "Duplikasikan", @@ -476,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Edit YAML", @@ -542,6 +573,7 @@ "view-configuration": "Lihat konfigurasi" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Konfigurasi internal Alertmanager Grafana tidak dapat diubah secara manual. Untuk mengubah konfigurasi ini, edit setiap sumber daya melalui UI.", "gma-manual-configuration-is-not-supported": "Perubahan konfigurasi manual tidak didukung", "message": { @@ -556,11 +588,13 @@ "title-resetting-alertmanager-configuration": "Mengatur ulang konfigurasi Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Bandingkan", "restore": "Pulihkan", "text-latest": "Terkini" }, + "confirmText-yes-restore-configuration": "", "loading": "Memuat...", "no-previous-configurations": "Tidak ada konfigurasi sebelumnya", "this-might-take-a-while": "Harap tunggu...", @@ -840,8 +874,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Tindakan lainnya untuk titik kontak \"{{contactPointName}}\"", + "ariaLabel-delete": "", "button-edit": "Edit", "button-view": "Lihat", + "export-ariaLabel-export": "", "export-label-export": "Ekspor", "label-delete": "Hapus", "label-manage-permissions": "Kelola izin", @@ -1374,6 +1410,7 @@ "label-disable-resolved-message": "Nonaktifkan pesan terselesaikan" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Harus berupa bilangan bulat positif.", "must-enter-a-group-name": "Harus memasukkan nama grup" @@ -1465,15 +1502,12 @@ "group-details": { "ds-features-error": "Kesalahan saat memuat sumber data", "edit": "Edit", - "evaluations-to-fire": "Siklus evaluasi yang akan dinyalakan", "export": "Ekspor", "folder": "Folder", "group-loading-error": "Kesalahan saat memuat grup", "interval": "Interval", "namespace": "Ruang nama", - "pending-period": "Periode tertunda", - "recording": "Pencatatan", - "rule-name": "Nama aturan" + "new": "" }, "group-edit": { "ds-error": "Kesalahan saat memuat sumber data", @@ -1519,8 +1553,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Mulai", "label-insights": "Wawasan", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alerting" }, "import-to-gma": { @@ -1819,7 +1865,11 @@ "other-data-sources": "Sumber data lain" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Dinonaktifkan", @@ -2069,9 +2119,11 @@ "update-errors": { "conflict": "Pohon kebijakan pemberitahuan telah diperbarui oleh pengguna lain.", "error-code": "Pesan kesalahan: \"{{error}}\"", - "fallback": "Terjadi kesalahan saat memperbarui kebijakan pemberitahuan Anda.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Muat ulang halaman untuk mencoba lagi.", - "title": "Kesalahan saat menyimpan kebijakan pemberitahuan" + "title": "" }, "n-more-policies_other": "{{count}} kebijakan tambahan" }, @@ -2126,6 +2178,7 @@ "query-and-expressions-step": { "add-query": "Tambahkan kueri", "body-queries-expressions-configured": "Buat setidaknya satu kueri atau pola untuk diperingatkan", + "confirmText-deactivate": "", "expressions": "Pola", "loading-data-sources": "Memuat sumber data...", "manipulate-returned-queries-other-operations": "Manipulasi data yang dikembalikan dari kueri dengan matematika dan operasi lainnya.", @@ -2175,6 +2228,9 @@ "label-export-all": "Ekspor semua" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Tidak dapat memuat editor kueri karena: {{errorMessage}}" }, @@ -2193,6 +2249,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Anda perlu mengatur grup evaluasi baru untuk aturan yang disalin karena grup evaluasi awal telah disediakan dan tidak dapat digunakan untuk aturan yang dibuat di UI.", "body-not-provisioned": "Aturan baru tidak <1>akan ditandai sebagai aturan yang disediakan.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Salin aturan peringatan yang disediakan" }, "redirect-to-rule-viewer": { @@ -2389,8 +2446,6 @@ "title-inspect-alert-rule": "Periksa aturan Peringatan" }, "rule-list": { - "cannot-find-rule-details-for": "Tidak dapat menemukan detail aturan untuk UID {{uid}}", - "cannot-load-rule-details-for": "Tidak dapat memuat detail aturan untuk UID {{uid}}", "configure-datasource": "Konfigurasikan", "draft-new-rule": "Buat draf aturan baru", "ds-error": { @@ -2585,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Tidak menemukan sumber data yang Anda inginkan? Beberapa sumber data tidak didukung untuk peringatan. Klik ikon untuk informasi selengkapnya." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Tambah Alertmanager baru", "builtin-alertmanager": "Alertmanager bawaan", @@ -2737,6 +2797,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Pilih templat pemberitahuan", "loading": "Memuat...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Pilih templat pemberitahuan" } @@ -2763,6 +2826,8 @@ }, "templates-table": { "actions": "Tindakan", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Tidak ada templat yang ditentukan.", "template-group": "Grup templat", "title-delete-template-group": "Hapus grup templat" @@ -2815,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}} lalu)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Folder target tidak kosong, beberapa aturan mungkin ditimpa atau dihapus. Anda yakin ingin mengimpor aturan peringatan ini ke aturan yang dikelola Grafana?", @@ -2890,6 +2960,11 @@ "title-delete-contact-point": "Hapus titik kontak" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Hapus kebijakan pemberitahuan" @@ -3046,7 +3121,8 @@ "annotation-field-mapper": { "annotation": "Anotasi", "first-value": "Nilai pertama", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Tambahkan kueri anotasi", @@ -3054,6 +3130,16 @@ "info-box-content-2": "Lihat <2>Dokumentasi anotasi untuk informasi selengkapnya.", "title": "Belum ada kueri anotasi kustom yang ditambahkan" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} peristiwa (dari {{numFields}} bidang)", "no-events-found": "Tidak ada peristiwa yang ditemukan", @@ -3129,6 +3215,9 @@ "define-allowed-teams-ids-label": "Tentukan ID tim yang diizinkan", "display-name-description": "Akan ditampilkan di halaman login sebagai \"Masuk dengan ...\". Berguna jika Anda menggunakan lebih dari satu penyedia identitas atau protokol SSO.", "display-name-label": "Nama tampilan", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nama kunci yang akan digunakan untuk pencarian email pengguna dalam peta atribut token ID OAuth2.", "email-attribute-name-label": "Nama atribut email", "email-attribute-path-description": "Ekspresi JMESPath yang digunakan untuk pencarian email pengguna dari informasi pengguna.", @@ -3180,7 +3269,7 @@ "team-ids-github": "Daftar bilangan bulat ID Tim.", "team-ids-label": "ID Tim", "team-ids-numbers": "ID Tim harus berupa angka.", - "team-ids-other": "Daftar string ID Tim.", + "team-ids-other": "", "team-ids-placeholder": "Masukkan ID Tim dan tekan Enter untuk menambahkan", "teams-url-description": "URL yang digunakan untuk kueri ID Tim. Jika tidak diatur, nilai defaultnya adalah /teams.", "teams-url-description-oauth": "Jika Anda mengonfigurasi \"{{ teamsURLLabel }}\", Anda juga harus mengonfigurasi \"{{ teamIDsAttributePathLabel }}\".", @@ -3224,6 +3313,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Atur ulang ke nilai default" }, + "confirmText-reset": "", "disable": "Nonaktifkan", "disabling": "Sedang menonaktifkan...", "discard": "Buang", @@ -3455,11 +3545,11 @@ "button-create": "Buat", "button-creating": "Membuat...", "cancel": "Batalkan", - "error-invalid-folder-name": "Nama folder tidak valid", "error-required": "Nama folder wajib diisi", "folder-name-input-placeholder-enter-folder-name": "Masukkan nama folder", "label-folder-name": "Nama folder", "text-pull-request-created": "Permintaan pull telah dibuat dengan perubahan pada folder ini:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Permintaan pull dibuat", "title-this-repository-is-read-only": "Repositori ini hanya dapat dibaca" }, @@ -3877,6 +3967,7 @@ "tooltip": "Tutup" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Terjadi kesalahan saat memuat opsi." @@ -4145,8 +4236,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Menghitung interval secara dinamis dengan membagi rentang waktu dengan jumlah yang ditentukan" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4308,12 +4399,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Lanjutkan ke situs eksternal?" } }, "render-left-actions": { - "text-public": "Publik", "tooltip-view-as-scene": "Tampilkan sebagai Scene" } }, @@ -4506,6 +4599,14 @@ "errors": { "failed-to-load": "Gagal memuat dasbor" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Gagal mengambil dasbor" @@ -4522,6 +4623,13 @@ "editable": "Dapat diedit", "readonly": "Hanya-baca" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4571,6 +4679,18 @@ "transform-data": "Ubah data" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4825,6 +4945,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Anda yakin ingin memulihkan dasbor ke versi {{version}}? Semua perubahan yang belum disimpan akan hilang.", + "confirmText-restore-version": "", "title-restore-version": "Pulihkan versi" }, "row-options-button": { @@ -4875,6 +4996,9 @@ "title-not-unique": "Judul ini tidak unik" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Simpan sebagai" }, @@ -4909,6 +5033,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Dasbor dengan nama yang sama di folder yang dipilih sudah ada.<1><2>Apa Anda masih ingin menyimpan dasbor ini?", "body-version-mismatch": "Orang lain telah memperbarui dasbor ini<1><2>Apa Anda masih ingin menyimpan dasbor ini?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Konflik", "title-version-mismatch": "Konflik" }, @@ -5074,7 +5199,6 @@ "playlist-next": "Buka dasbor berikutnya", "playlist-previous": "Buka dasbor sebelumnya", "playlist-stop": "Hentikan daftar putar", - "public-dashboard": "Publik", "refresh": "Muat ulang dasbor", "save": "Simpan dasbor", "save-dashboard": { @@ -5105,7 +5229,9 @@ "label-apply-transformation-to": "Terapkan transformasi ke" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Debug", "title-disable-transformation": "Nonaktifkan transformasi", "title-filter": "Filter", @@ -5125,12 +5251,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Cari transformasi", "show-images": "Tampilkan gambar", - "title-add-another-transformation": "Tambah transformasi lain" + "title-add-another-transformation": "Tambah transformasi lain", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Tambah transformasi lain" }, + "confirmText-delete-all": "", "delete-all-transformations": "Hapus semua transformasi", "title-delete-all-transformations": "Hapus semua transformasi?", "tooltip-clear-search": "Hapus pencarian", @@ -5167,6 +5298,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alihkan tombol pemilihan versi {{version}}", "date": "Tanggal", + "name-latest": "", "notes": "Catatan", "restore": "Pulihkan", "updated-by": "Diperbarui oleh", @@ -5243,7 +5375,8 @@ "description-enables-users-custom-values": "Memungkinkan pengguna untuk menambahkan nilai kustom ke daftar", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Berikan dimensi sebagai CSV: {{name}}, {{value}}", "label-data-source": "Sumber data", - "label-use-static-key-dimensions": "Gunakan dimensi kunci statis" + "label-use-static-key-dimensions": "Gunakan dimensi kunci statis", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5316,6 +5449,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Cabut URL publik" } @@ -5327,6 +5463,7 @@ }, "custom-variable-form": { "custom-options": "Opsi kustom", + "name-values-separated-comma": "", "selection-options": "Opsi pemilihan" }, "dashboard-edit-pane-renderer": { @@ -5345,6 +5482,12 @@ "label-type": "Jenis", "label-url": "URL", "label-with-tags": "Dengan tag", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Buka dasbor" }, "dashboard-link-list": { @@ -5357,15 +5500,9 @@ "type": "Jenis" }, "dashboard-preview-banner": { - "not-saved": "Nilai belum disimpan dalam database Grafana", "not-yet-saved": "Nilai tidak disimpan dalam database Grafana", - "open-pull-request-in-git-hub": "Buka permintaan penggabungan di GitHub", - "title-dashboard-loaded-branch-git-hub": "Dasbor ini dimuat dari cabang di GitHub.", "title-dashboard-loaded-external-repository": "Dasbor ini dimuat dari repositori eksternal", - "title-dashboard-loaded-request-git-hub": "Dasbor ini dimuat dari permintaan penggabungan di GitHub.", - "title-error-loading-dashboard": "Kesalahan saat memuat dasbor", - "value-not-saved": "Nilai belum disimpan dalam database Grafana", - "view-pull-request-in-git-hub": "Lihat permintaan penggabungan di GitHub" + "title-error-loading-dashboard": "Kesalahan saat memuat dasbor" }, "dashboard-scene": { "text": { @@ -5391,6 +5528,8 @@ "data-source-options": "Opsi sumber data", "description-instance-name-filter": "Filter regex untuk instans sumber data yang akan dipilih dalam daftar nilai variabel. Biarkan kosong untuk semua.", "example-instance-name-filter": "Contoh: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Opsi pemilihan" }, "default-grid-layout-manager": { @@ -5436,12 +5575,61 @@ "empty-transformations-message": { "add-transformation": "Tambahkan transformasi" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Opsi kolom", "row-height-options": "Opsi tinggi baris" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5466,7 +5654,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Berikan dimensi sebagai CSV: {{name}}, {{value}}", "group-by-options": "Kelompokkan berdasarkan opsi", "label-data-source": "Sumber data", - "label-use-static-group-by-dimensions": "Gunakan dimensi grup statis" + "label-use-static-group-by-dimensions": "Gunakan dimensi grup statis", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Salin ke papan klip", @@ -5502,9 +5691,14 @@ "apply": "Terapkan" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Nilai yang dihitung tidak akan berada di bawah ambang batas ini", "description-step-count": "Berapa kali rentang waktu saat ini harus dibagi untuk menghitung nilai", - "interval-options": "Opsi interval" + "interval-options": "Opsi interval", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5523,11 +5717,28 @@ "last-edited": "{{timeAgo}} oleh", "usage-count_other": "Digunakan pada {{count}} dasbor" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Dasbor dengan nama yang sama di folder yang dipilih sudah ada. Ingin tetap menyimpan dasbor ini?", "title-name-already-exists": "Nama sudah ada" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Lanjutkan ke situs eksternal?" } @@ -5563,6 +5774,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Tambah transformasi lain", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Hapus semua transformasi", "title-delete-all-transformations": "Hapus semua transformasi?" }, @@ -5615,9 +5828,13 @@ "description-examples": "Grup tangkapan bernama dapat digunakan untuk memisahkan teks tampilan dan nilai (<1>lihat contoh).", "description-optional": "Opsional, jika Anda ingin mengekstrak bagian dari nama seri atau segmen node metrik.", "label-data-source": "Sumber data", + "label-static-options-sort": "", "label-target-data-source": "Sumber data target", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opsi kueri", - "selection-options": "Opsi pemilihan" + "selection-options": "Opsi pemilihan", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5630,6 +5847,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Anda yakin ingin memulihkan dasbor ke versi {{version}}? Semua perubahan yang belum disimpan akan hilang.", + "confirmText-restore-version": "", "title-restore-version": "Pulihkan versi" }, "save-button": { @@ -5647,7 +5865,8 @@ "label-folder": "Folder", "render-footer": { "title-failed-to-save-dashboard": "Gagal menyimpan dasbor" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5722,7 +5941,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Memungkinkan beberapa nilai untuk dipilih secara bersamaan", "description-enables-option-include-variables": "Mengaktifkan opsi untuk menyertakan semua nilai", - "description-enables-users-custom-values": "Memungkinkan pengguna untuk menambahkan nilai kustom ke daftar" + "description-enables-users-custom-values": "Memungkinkan pengguna untuk menambahkan nilai kustom ke daftar", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Alihkan menu bagikan" @@ -5742,6 +5965,9 @@ "copy-to-clipboard-failed": "Gagal menyalin ke papan klip" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opsional)", "text-options": "Opsi teks" @@ -5765,6 +5991,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Anda yakin ingin memutus tautan panel ini?" }, "unsaved-changes-modal": { @@ -5781,6 +6009,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Variabel ini tidak direferensikan oleh variabel atau dasbor apa pun.", "aria-label-variable-referenced-other-variables-dashboard": "Variabel ini direferensikan oleh variabel atau dasbor lain.", @@ -5790,10 +6021,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulir editor variabel", "back-to-list": "Kembali ke daftar", + "confirmText": { + "delete-variable": "" + }, "delete": "Hapus", "description-optional-display-name": "Nama tampilan opsional", "description-template-variable-characters": "Nama variabel templat. (Maks. 50 karakter)", "general": "Umum", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Teks deskriptif", "placeholder-label-name": "Nama label", "placeholder-variable-name": "Nama variabel", @@ -5808,13 +6045,25 @@ "variable": "Variabel" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Hapus variabel", "tooltip-duplicate-variable": "Duplikatkan variabel", "tooltip-remove-variable": "Hapus variabel" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Sembunyikan" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Menampilkan penggunaan untuk: {{variableId}}", "tooltip-show-usages": "Tampilkan penggunaan" @@ -5841,6 +6090,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alihkan tombol pemilihan versi {{version}}", "date": "Tanggal", + "name-latest": "", "notes": "Catatan", "restore": "Pulihkan", "updated-by": "Diperbarui oleh", @@ -6228,7 +6478,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Unggah" @@ -6266,6 +6517,7 @@ }, "label-limit": "Batas", "label-value": "Nilai", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6274,9 +6526,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Maksimum", "label-min": "Minimum", - "label-value": "Nilai" + "label-value": "Nilai", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6848,6 +7106,8 @@ "aria-label-select-service-name-operator": "Pilih operator nama layanan", "aria-label-select-span-name": "Pilih nama rentang", "aria-label-select-span-name-operator": "Pilih operator nama rentang", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filter Rentang", "label-duration": "Durasi", "label-service-name": "Nama layanan", @@ -6918,6 +7178,8 @@ "split-widen": "Lebarkan panel" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Berikan umpan balik", "label-copied": "Disalin!", "label-export": "Ekspor", @@ -7025,6 +7287,7 @@ "when": "KAPAN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Pengoperasian" }, "math": { @@ -7055,6 +7318,7 @@ }, "folder-filter": { "clear-folder-button": "Hapus folder", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filter folder", "select-placeholder": "Filter berdasarkan folder" }, @@ -7123,7 +7387,53 @@ "incomplete-request-error": "Maaf, saya tidak dapat menyelesaikan permintaan Anda. Silakan coba lagi.", "send-custom-feedback": "Kirim" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Garis Lintang", "label-longitude": "Garis Bujur" @@ -7132,6 +7442,14 @@ "center": "Pusat:", "zoom": "Zoom:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Layer" @@ -7154,6 +7472,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Tambahkan aturan gaya geomap" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Tambahkan layer", "no-layers": "Tidak ada layer?" @@ -7164,16 +7490,38 @@ "label-zoom": "Perbesar", "use-current-map-settings": "Gunakan pengaturan peta saat ini" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Simbol" }, "measure-overlay": { "tooltip-show-measure-tools": "Tampilkan alat ukur" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Layer peta dasar dikonfigurasi oleh admin server." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Ratakan", "label-baseline": "Baseline", "label-color": "Warna", @@ -7187,7 +7535,14 @@ "label-symbol-vertical-align": "Penyelarasan vertikal simbol", "label-text-label": "Label teks", "label-x-offset": "Offset X", - "label-y-offset": "Offset Y" + "label-y-offset": "Offset Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operator perbandingan", @@ -7198,6 +7553,15 @@ "placeholder-feature-property": "Properti fitur", "placeholder-numeric-value": "Nilai numerik", "placeholder-value": "nilai" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7447,7 +7811,8 @@ "aria-label-selected-color": "warna {{colorLabel}}" }, "confirm-button": { - "cancel": "Batalkan" + "cancel": "Batalkan", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Ketik \"{{confirmPromptText}}\" untuk mengonfirmasi" @@ -7629,6 +7994,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "alihkan panel ciutkan", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Batalkan kueri", "tooltip-cancel-loading": "Batalkan kueri", "tooltip-stop-streaming": "Hentikan streaming", @@ -7695,6 +8062,9 @@ "filter-popup-match-case": "Cocok huruf besar kecil", "inspect-drawer-title": "Periksa nilai", "inspect-menu-label": "Periksa nilai", + "nested-table": { + "no-data": "" + }, "no-values-label": "Tidak ada nilai", "pagination-summary": "{{itemsRangeStart}}—{{displayedEnd}} dari {{numRows}} baris" }, @@ -7703,6 +8073,7 @@ }, "tags-input": { "add": "Tambahkan", + "placeholder-new-tag": "", "remove": "Hapus tag: {{name}}" }, "time-sync-button": { @@ -7796,6 +8167,12 @@ "footer-click-to-action": "Klik untuk {{actionTitle}} ", "footer-click-to-navigate": "Klik untuk membuka {{linkTitle}}", "timestamp": "Stempel Waktu" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8172,6 +8549,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Buat panel pustaka" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Ingin menghapus panel ini?" }, @@ -8194,7 +8575,7 @@ "usage-count_other": "Digunakan pada {{count}} dasbor" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Cari berdasarkan nama atau deskripsi" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Memuat panel pustaka..." @@ -8242,9 +8623,7 @@ "share-panel": { "config-description": "Buat tautan langsung yang dipersonalisasi untuk membagikan panel Anda dalam organisasi Anda, dengan pengaturan penyesuaian berikut:", "download-image": "Unduh gambar", - "render-image": "Buat gambar", - "render-image-error": "Gagal merender gambar panel", - "render-image-error-description": "Terjadi kesalahan saat membuat gambar" + "render-image": "Buat gambar" } }, "live": { @@ -8330,6 +8709,15 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8353,7 +8741,6 @@ "label-wrap-lines": "Terapkan wrap pada baris" }, "log-details": { - "close": "Tutup detail log", "fields": "Bidang", "links": "Tautan", "log-line": "Baris log", @@ -8379,6 +8766,42 @@ "show-more": "tampilkan lebih banyak", "tooltip-error": "Kesalahan: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Salin tautan ke baris log", "copy-log": "Salin baris log", @@ -8479,6 +8902,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8505,6 +8933,7 @@ "un-themed-log-details": { "aria-label-data-links": "Tautan data", "aria-label-fields": "Bidang", + "aria-label-line": "", "aria-label-log-level": "Level log", "aria-label-no-details": "Tidak ada detail" }, @@ -8572,6 +9001,8 @@ "updated-on": "Diperbarui pada" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Hapus" }, "unthemed-dashboard-import": { @@ -8581,8 +9012,17 @@ "text": { "import-dashboard": "Impor dasbor" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Alat ini dapat memigrasikan beberapa sumber daya dari instalasi ini ke tumpukan cloud Anda. Untuk memulai, Anda harus membuat snapshot instalasi ini. Membuat snapshot biasanya membutuhkan waktu kurang dari dua menit. Snapshot disimpan di samping instalasi Grafana ini.", @@ -8731,6 +9171,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Dasbor sudah disediakan dan dikelola oleh Grafana di instans cloud. Sebaiknya gunakan dasbor yang disediakan ke depannya. Jika Anda masih ingin menyalin dasbor ke instans cloud, ubah ID dasbor di JSON dasbor, simpan snapshot baru, lalu unggah lagi.", "datasource-already-managed": "Dasbor sudah disediakan dan dikelola oleh Grafana di instans cloud. Jika ini adalah sumber daya yang berbeda, atur UID lain dan coba lagi.", "datasource-invalid-url": "Ada sumber data yang memiliki URL tidak valid. Berikan URL yang valid dan coba lagi.", @@ -9418,6 +9860,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Pilih organisasi" }, "page": { @@ -9640,6 +10083,7 @@ "permission": "Anda tidak memiliki izin untuk melihat halaman ini.", "title-access-denied": "Akses ditolak" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Tidak ada komponen halaman aplikasi root yang ditemukan" }, "browse": { @@ -9683,8 +10127,7 @@ "update-status-text": "plugin diperbarui" }, "versions": { - "confirmation-text-1": "Apa Anda yakin ingin menurunkan ke versi", - "confirmation-text-2": "Anda biasanya tidak boleh melakukan ini", + "confirmation-text": "", "downgrade-confirm": "Turunkan", "downgrade-title": "Turunkan versi plugin" } @@ -9738,6 +10181,10 @@ "empty-state": { "message": "Tidak ada plugin yang ditemukan" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9771,7 +10218,11 @@ "updating": "Memperbarui" }, "install-controls-button": { - "title-uninstall-modal": "Hapus instalasi {{plugin}}" + "title-uninstall-modal": "Hapus instalasi {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Plugin ini tidak dipublikasikan ke <2>grafana.com/plugins dan tidak dapat dikelola melalui katalog.", @@ -9942,6 +10393,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Fitur ini saat ini sedang dalam pengembangan aktif. Untuk pengalaman terbaik dan peningkatan terbaru, kami merekomendasikan Anda untuk menggunakan <2>versi nightly Grafana." @@ -10802,6 +11262,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Pemilih akun layanan", "select-placeholder": "Mulai mengetik untuk mencari akun layanan" }, @@ -10847,6 +11308,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Tambahkan token akun layanan", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Hapus akun layanan", "disable-service-account": "Nonaktifkan akun layanan", "enable-service-account": "Aktifkan akun layanan", @@ -10873,6 +11338,7 @@ "used-by": "Digunakan oleh" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Edit" }, "service-account-role-row": { @@ -10886,10 +11352,15 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Tambahkan akun layanan", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Cari akun layanan berdasarkan nama", "sub-title": "Akun layanan dan tokennya dapat digunakan untuk mengautentikasi API Grafana. Cari tahu selengkapnya di <2>dokumentasi kami.", "title-delete-service-account": "Hapus akun layanan", - "title-disable-service-account": "Nonaktifkan akun layanan" + "title-disable-service-account": "Nonaktifkan akun layanan", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Token ini telah kedaluwarsa", @@ -10903,6 +11374,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Ekspor sebagai JSON", "share-externally-title": "Bagikan secara eksternal", "share-internally-title": "Bagikan secara internal", @@ -10938,6 +11410,19 @@ "view-button": "Lihat JSON", "view-button-yaml": "Lihat YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Buat panel pustaka." }, @@ -10946,7 +11431,7 @@ "info-text": "Buat tautan langsung ke dasbor atau panel ini, disesuaikan dengan opsi di bawah ini.", "link-url": "Tautkan URL", "render-alert": "Plugin perender gambar belum diinstal", - "render-instructions": "Untuk merender gambar panel, Anda harus menginstal <2>plugin perender gambar Grafana . Hubungi administrator Grafana Anda untuk menginstal plugin.", + "render-instructions": "", "rendered-image": "Tautan langsung gambar yang dirender", "save-alert": "Dasbor tidak disimpan", "save-dashboard": "Untuk merender gambar panel, Anda harus menyimpan dasbor terlebih dahulu.", @@ -11014,6 +11499,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Pratinjau panel" }, @@ -11353,6 +11844,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Teks alternatif yang akan ditampilkan jika gambar tidak dapat ditampilkan atau untuk pengguna yang menggunakan pembaca layar", "description-title-text": "Teks yang akan ditampilkan saat gambar diarahkan oleh kursor", @@ -11373,6 +11865,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11423,6 +11916,7 @@ "tag-option-label": "Opsi tag" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Pemilih tim", "select-placeholder": "Pilih tim" }, @@ -11724,6 +12218,18 @@ "placeholder-fields-or-number": "Bidang atau angka" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Mode", "label-replace-all-fields": "Ganti semua bidang" @@ -11738,6 +12244,13 @@ "concatenate-transformer-editor": { "label-label": "Label", "label-name": "Nama", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Frame" }, "config-from-query-transformer-editor": { @@ -11748,6 +12261,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Tambahkan transformer jenis bidang konversi", "aria-label-remove-convert-field-type-transformer": "Hapus transformer jenis bidang konversi", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Browser", "utc": "UTC" @@ -11758,7 +12275,13 @@ "label-input-format": "Format input", "label-join-with": "Gabung dengan", "label-set-timezone": "Atur zona waktu", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Jenis", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Gunakan pemisah eksplisit saat menggabungkan nilai array", "tooltip-input-format": "Tentukan format bidang input sehingga Grafana dapat memproses string tanggal dengan benar.", "tooltip-specify-the-output-format": "Tentukan format output.", @@ -11780,6 +12303,19 @@ "desc": "", "off": "Nonaktif" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Tambah nilai enum", "generate-enum-values-from-data": "Buat nilai enum dari data" @@ -11790,11 +12326,22 @@ "remove-enum-row-tooltip-delete": "Hapus" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Delimiter", "label-format": "Format", "label-keep-time": "Pertahankan waktu", "label-replace-all-fields": "Ganti semua bidang", "label-source": "Sumber", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Pilih delimiter...", "tooltip-regexp": "Contoh: {{regexExample}}" @@ -11803,8 +12350,22 @@ "aria-label-threshold-color": "Warna ambang batas" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Bidang", - "label-lookup": "Cari" + "label-lookup": "Cari", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Pengaturan tambahan", @@ -11813,12 +12374,26 @@ "use-as": "Gunakan sebagai" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Dari variabel", "label-identifier": "Pengidentifikasi", "label-use-variable": "Gunakan variabel", "label-variable": "Variabel", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Struktur pola reguler" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Bidang", "label-match": "Cocok", @@ -11828,14 +12403,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Tambah ketentuan", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Syarat", - "label-filter-type": "Jenis filter" + "label-filter-type": "Jenis filter", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Bidang", "label-format": "Format", "label-substring-range": "Rentang substring" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Atur zona waktu", @@ -11843,6 +12452,70 @@ "tooltip-format": "Format output untuk bidang yang ditentukan sebagai <2>string format Moment.js.", "tooltip-timezone-manually": "Atur zona waktu tanggal secara manual" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Browser", @@ -11870,22 +12543,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Diabaikan", "placeholder-select-stats": "Pilih statistik" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Perhitungan tidak akan berpengaruh jika tidak ada bidang yang dikelompokkan" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Jika tabel nested diaktifkan, akan menampilkan nama bidang sebagai header tabel", "label-show-field-names-in-nested-tables": "Tampilkan nama bidang dalam tabel nested", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Perhitungan tidak akan berpengaruh jika tidak ada bidang yang dikelompokkan." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Nilai cell", "label-column": "Kolom", "label-empty-value": "Nilai kosong", "label-row": "Baris", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Nilai Boolean salah", @@ -11904,6 +12601,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Auto", "placeholder-default": "Default: 30", "placeholder-none": "Tidak ada" @@ -11912,7 +12615,16 @@ "label-as-percentile": "Sebagai persentil", "tooltip-transform-index-percentile": "Ubah indeks baris menjadi persentil." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Input tidak berisi label apa pun", "no-input-or-labels-found": "Tidak ada input (atau label) yang ditemukan", @@ -11934,10 +12646,30 @@ "label-labels": "Label", "label-mode": "Mode", "label-value-field-name": "Nama bidang nilai", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opsional) Pilih label", "tooltip-replace-value-field-label": "Ganti nama bidang nilai dengan label" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Nilai atau variabel" }, "long-info": { @@ -11948,7 +12680,13 @@ "string-values-separate": "String nilai direpresentasikan sebagai bidang terpisah dan bukan sebagai label" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Penggabungan tidak berpengaruh ketika diterapkan pada single frame." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Penggabungan tidak berpengaruh ketika diterapkan pada single frame.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Semua nilai adalah numerik", @@ -11958,10 +12696,16 @@ "time-in-ascending-order": "Waktu dalam urutan naik" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Auto", "field-order-manual": "", - "first-frame-warning": "Penataan bidang hanya berfungsi dengan satu frame. Pertimbangkan untuk menerapkan transformasi gabungan atau memfilter input terlebih dahulu." + "first-frame-warning": "Penataan bidang hanya berfungsi dengan satu frame. Pertimbangkan untuk menerapkan transformasi gabungan atau memfilter input terlebih dahulu.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Mengatur tampilan nama bidang yang dipilih. Karena nama frame biasanya lebih baik untuk data tabular" @@ -11986,9 +12730,41 @@ "partition-values-works-single-frame": "Partisi berdasarkan nilai hanya berfungsi dengan satu frame.", "tooltip-keeps-partition-fields-frames": "Menyimpan bidang partisi di frame" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Info" + "label-info": "Info", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "dan", @@ -12000,6 +12776,9 @@ "label-operation": "Pengoperasian" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Perhitungan", "label-include-time": "Sertakan waktu", "label-labels-to-fields": "Label ke bidang", @@ -12014,12 +12793,23 @@ "series-to-rows": "Format deret ke format baris" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Pilih stat" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Nilai atau variabel", "variable-regex-documentation": "Memformat variabel multi-nilai menjadi regex-string" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Kubik", @@ -12041,6 +12831,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Jumlah poin X,Y untuk diprediksi" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Cocok", "label-replace": "Ganti", @@ -12049,11 +12847,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Bidang", - "label-mode": "Mode" + "label-mode": "Mode", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Bidang", "label-reverse": "Balik", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Pilih bidang" }, "supplier": { @@ -12073,6 +12897,17 @@ "transform": "Ubah" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Bidang waktu", @@ -12086,10 +12921,34 @@ "placeholder-value": "Nilai", "tooltip-name-for-value-fields": "Nama untuk bidang nilai" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Pengoperasian", "placeholder-field": "Bidang" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Beberapa bidang nilai dari jenis apa pun", "single-frame": "Single frame", @@ -12142,6 +13001,7 @@ "title": "Organisasi" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Pemilih pengguna", "select-placeholder": "Mulai mengetik untuk mencari pengguna" }, @@ -12227,6 +13087,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Hapus variabel" }, "create-ad-hoc-variable-adapter": { @@ -12275,9 +13137,34 @@ "label-refresh": "Muat ulang" }, "query-variable-sort-select": { - "description-values-variable": "Cara mengurutkan nilai variabel ini" + "description-values-variable": "Cara mengurutkan nilai variabel ini", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "nilai default, jika ada", "text-options": "Opsi teks" }, @@ -12306,6 +13193,8 @@ "description-optional-display-name": "Nama tampilan opsional", "description-template-variable-characters": "Nama variabel templat. (Maks. 50 karakter)", "general": "Umum", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Teks deskriptif", "placeholder-label-name": "Nama label", "placeholder-variable-name": "Nama variabel", @@ -12320,9 +13209,15 @@ "tooltip-duplicate-variable": "Duplikatkan variabel", "tooltip-remove-variable": "Hapus variabel" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Alihkan tombol semua nilai" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Tampilkan penggunaan" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 03f35d3bbc5..e6d2550b981 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Alcune funzionalità sono stabili (GA) e attive per impostazione predefinita, mentre altre sono ancora in fase beta preliminare e disponibili su richiesta in anteprima.", "confirm-modal-body-2": "Si consiglia di comprendere le implicazioni di ogni modifica delle funzionalità prima di applicarle.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Disponibilità generale", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Elimina org", + "confirmText-delete": "", "title-delete": "Elimina" }, "anon-users": { "not-found": "Nessun utente anonimo trovato." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Forza la disconnessione da tutti i dispositivi" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Non hai l'autorizzazione per vedere gli utenti in questa organizzazione. Per aggiornare questa organizzazione, contatta l'amministratore del server.", "heading": "Modifica organizzazione", @@ -208,9 +216,11 @@ "not-editable": "Il ruolo di questo utente non è modificabile perché è sincronizzato dal provider di autenticazione. Fai riferimento ai <1>documenti di autenticazione Grafana per i dettagli." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Ruolo" }, + "confirmText-delete": "", "delete-aria-label": "Elimina utente: {{name}}", "title-delete": "Elimina" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Queste impostazioni di sistema sono definite in grafana.ini o custom.ini (o sovrascritte nelle variabili ENV). Per modificarle, al momento è necessario riavviare Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licenza Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Modifica", + "confirmText-change": "", "grafana-admin-key": "Amministratore Grafana", "grafana-admin-no": "No", "grafana-admin-yes": "Sì", "title": "Autorizzazioni" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Elimina utente", "disable-button": "Disabilita utente", "edit-button": "Modifica", @@ -312,6 +330,9 @@ "title-delete-user": "Elimina utente", "title-disable-user": "Disabilita utente" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Browser e sistema operativo", "force-logout-all-button": "Forza la disconnessione da tutti i dispositivi", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Orari attivi" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Aggiungi altro" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Disattivazione audio, raggruppamento e tempistiche (opzionale)", "title-muting-grouping-and-timings": "Disattivazione audio, raggruppamento e orari" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Copia link", "duplicate": "Duplica", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Modifica YAML", @@ -546,6 +577,7 @@ "view-configuration": "Visualizza configurazione" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "La configurazione interna di Grafana Alertmanager non può essere modificata manualmente. Per modificare questa configurazione, modifica le singole risorse tramite l'interfaccia utente.", "gma-manual-configuration-is-not-supported": "Modifiche alla configurazione manuale non supportate", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Ripristino della configurazione di Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Confronta", "restore": "Ripristina", "text-latest": "Più recente" }, + "confirmText-yes-restore-configuration": "", "loading": "Caricamento in corso...", "no-previous-configurations": "Nessuna configurazione precedente", "this-might-take-a-while": "Potrebbe volerci un po' di tempo...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Altre azioni per il punto di contatto \"{{contactPointName}}\"", + "ariaLabel-delete": "", "button-edit": "Modifica", "button-view": "Visualizza", + "export-ariaLabel-export": "", "export-label-export": "Esporta", "label-delete": "Elimina", "label-manage-permissions": "Gestisci autorizzazioni", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Disabilita messaggio risolto" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "", "must-enter-a-group-name": "" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Errore durante il caricamento dei dettagli dell'origine dati", "edit": "Modifica", - "evaluations-to-fire": "Cicli di valutazione da attivare", "export": "Esporta", "folder": "Cartella", "group-loading-error": "Errore durante il caricamento del gruppo", "interval": "Intervallo", "namespace": "Namespace", - "pending-period": "Periodo di attesa", - "recording": "Registrazione", - "rule-name": "Nome regola" + "new": "" }, "group-edit": { "ds-error": "Errore durante il caricamento dei dettagli dell'origine dati", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Inizia subito", "label-insights": "Approfondimenti", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Avvisi" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Disabilitato", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "L'albero di accesso alle notifiche è stato aggiornato da un altro utente.", "error-code": "Messaggio di errore: \"{{error}}\"", - "fallback": "Si è verificato un errore durante l'aggiornamento dei criteri di notifica.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Aggiorna la pagina e riprova.", - "title": "Errore durante il salvataggio dei criteri di notifica" + "title": "" }, "n-more-policies_one": "{{count}} criterio aggiuntivo", "n-more-policies_other": "{{count}} criteri aggiuntivi" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Aggiungi query", "body-queries-expressions-configured": "Crea almeno una query o un'espressione su cui ricevere un avviso", + "confirmText-deactivate": "", "expressions": "Espressioni", "loading-data-sources": "Caricamento delle origini dei dati in corso...", "manipulate-returned-queries-other-operations": "Modifica i dati che trovi nelle query usando operazioni matematiche e di altro tipo.", @@ -2184,6 +2237,9 @@ "label-export-all": "Esporta tutto" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Impossibile caricare l'editor di query a causa di: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Sarà necessario impostare un nuovo gruppo di valutazione per la regola copiata perché quella originale è stata sottoposta a provisioning e non può essere utilizzata per le regole create nell'interfaccia utente.", "body-not-provisioned": "La nuova regola <1>non sarà contrassegnata come regola con provisioning.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Copia la regola di avviso fornita" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Ispeziona la regola di avviso" }, "rule-list": { - "cannot-find-rule-details-for": "Impossibile trovare i dettagli della regola per l'UID {{uid}}", - "cannot-load-rule-details-for": "Impossibile caricare i dettagli della regola per l'UID {{uid}}", "configure-datasource": "Configura", "draft-new-rule": "Scrivi una nuova regola", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Non trovi l'origine dei dati che desideri? Alcune origini dei dati non sono supportate per gli avvisi. Fai clic sull'icona per ulteriori informazioni." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Aggiungi nuovo Alertmanager", "builtin-alertmanager": "Alertmanager integrato", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Scegli il modello di notifica", "loading": "Caricamento in corso...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Azioni", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Nessun modello definito.", "template-group": "Gruppo modelli", "title-delete-template-group": "Elimina gruppo di modelli" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} fa)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "La cartella di destinazione non è vuota, alcune regole potrebbero essere sovrascritte o rimosse. Desideri davvero importare queste regole di avviso nelle regole gestite da Grafana?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Elimina punto di contatto" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Elimina criterio di notifica" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Annotazioni", "first-value": "Primo valore", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Aggiungi query di annotazione", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Consulta la <2>Documentazione delle annotazioni per ulteriori informazioni.", "title": "Non sono ancora state aggiunte query di annotazioni personalizzate" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} eventi (da {{numFields}} campi)", "no-events-found": "Nessun evento trovato", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Definisci gli ID dei team consentiti", "display-name-description": "Verrà visualizzato nella pagina di accesso come \"Accedi con...\". Utile se utilizzi più di un provider di identità o protocolli SSO.", "display-name-label": "Visualizza nome", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nome della chiave da utilizzare per la ricerca dell'e-mail dell'utente all'interno della mappa degli attributi del token ID di OAuth2.", "email-attribute-name-label": "Nome attributo e-mail", "email-attribute-path-description": "Espressione JMESPath da utilizzare per la ricerca dell'e-mail dell'utente dalle informazioni dell'utente.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Elenco intero di ID team.", "team-ids-label": "ID team", "team-ids-numbers": "Gli ID dei team devono essere numeri.", - "team-ids-other": "Elenco stringa di ID team.", + "team-ids-other": "", "team-ids-placeholder": "Inserisci gli ID dei team e premi Invio per aggiungerli", "teams-url-description": "L'URL utilizzato per eseguire query per gli ID dei team. Se non impostato, il valore predefinito è /teams.", "teams-url-description-oauth": "Se configuri \"{{ teamsURLLabel }}\", devi configurare anche \"{{ teamIDsAttributePathLabel }}\".", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Ripristina valori predefiniti" }, + "confirmText-reset": "", "disable": "Disabilita", "disabling": "Disabilitazione in corso...", "discard": "Annulla", @@ -3473,11 +3563,11 @@ "button-create": "Crea", "button-creating": "Creazione in corso...", "cancel": "Annulla", - "error-invalid-folder-name": "Nome cartella non valido", "error-required": "Il nome della cartella è obbligatorio", "folder-name-input-placeholder-enter-folder-name": "Inserisci nome cartella", "label-folder-name": "Nome cartella", "text-pull-request-created": "È stata creata una richiesta di pull con modifiche a questa cartella:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Richiesta di pull creata", "title-this-repository-is-read-only": "Questo repository è di sola lettura" }, @@ -3895,6 +3985,7 @@ "tooltip": "Chiudi" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Si è verificato un errore nel caricamento delle opzioni." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Calcola dinamicamente l'intervallo dividendo l'intervallo di tempo per il conteggio specificato" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "" } }, "render-left-actions": { - "text-public": "Pubblico", "tooltip-view-as-scene": "Visualizza come Scena" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Impossibile caricare il dashboard" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "" @@ -4540,6 +4641,13 @@ "editable": "Modificabile", "readonly": "" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Desideri davvero ripristinare la dashboard alla versione {{version}}? Tutte le modifiche non salvate andranno perse.", + "confirmText-restore-version": "", "title-restore-version": "Ripristina versione" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Questo titolo non è univoco" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Salva con nome" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Esiste già una dashboard con lo stesso nome nella cartella selezionata.<1><2>Desideri comunque salvare questa dashboard?", "body-version-mismatch": "Qualcun altro ha aggiornato questa dashboard<1><2>Desideri comunque salvare questa dashboard?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Conflitto", "title-version-mismatch": "Conflitto" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Vai al dashboard successivo", "playlist-previous": "Vai al dashboard precedente", "playlist-stop": "Interrompi playlist", - "public-dashboard": "Pubblico", "refresh": "Aggiorna dashboard", "save": "Salva dashboard", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Applica la trasformazione a" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Esegui il debug", "title-disable-transformation": "Disabilita trasformazione", "title-filter": "Filtro", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Cerca trasformazione", "show-images": "Mostra immagini", - "title-add-another-transformation": "Aggiungi un'altra trasformazione" + "title-add-another-transformation": "Aggiungi un'altra trasformazione", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Aggiungi un'altra trasformazione" }, + "confirmText-delete-all": "", "delete-all-transformations": "Elimina tutte le trasformazioni", "title-delete-all-transformations": "Eliminare tutte le trasformazioni?", "tooltip-clear-search": "Azzera ricerca", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Toggle di selezione della versione {{version}}", "date": "Data", + "name-latest": "", "notes": "Note", "restore": "Ripristina", "updated-by": "Aggiornato da", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Consente agli utenti di aggiungere valori personalizzati all'elenco", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Fornisci le dimensioni come CSV: {{name}}, {{value}}", "label-data-source": "Sorgente dati", - "label-use-static-key-dimensions": "Usa dimensioni chiave statiche" + "label-use-static-key-dimensions": "Usa dimensioni chiave statiche", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Revoca URL pubblico" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Opzioni personalizzate", + "name-values-separated-comma": "", "selection-options": "Seleziona opzioni" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Tipo", "label-url": "URL", "label-with-tags": "Con tag", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Apri dashboard" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "Il valore non è ancora stato salvato nel database di Grafana", "not-yet-saved": "Il valore non è stato salvato nel database di Grafana", - "open-pull-request-in-git-hub": "Apri richiesta pull in GitHub", - "title-dashboard-loaded-branch-git-hub": "Questo dashboard viene caricato da un ramo in GitHub.", "title-dashboard-loaded-external-repository": "Questo dashboard viene caricato da un repository esterno", - "title-dashboard-loaded-request-git-hub": "Questo dashboard viene caricato da una richiesta pull in GitHub.", - "title-error-loading-dashboard": "Errore durante il caricamento del dashboard", - "value-not-saved": "Il valore non è ancora stato salvato nel database di Grafana", - "view-pull-request-in-git-hub": "Visualizza richiesta pull in GitHub" + "title-error-loading-dashboard": "Errore durante il caricamento del dashboard" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Opzioni dell'origine dei dati", "description-instance-name-filter": "Filtro Regex per le istanze di origine dei dati tra cui scegliere nell'elenco dei valori delle variabili. Lascia vuoto per tutti.", "example-instance-name-filter": "Esempio: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Seleziona opzioni" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Aggiungi trasformazione" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "", "row-height-options": "" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Fornisci le dimensioni come CSV: {{name}}, {{value}}", "group-by-options": "Raggruppa per opzioni", "label-data-source": "Sorgente dati", - "label-use-static-group-by-dimensions": "Usa dimensioni di gruppo statiche" + "label-use-static-group-by-dimensions": "Usa dimensioni di gruppo statiche", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Copia negli appunti", @@ -5521,9 +5710,14 @@ "apply": "Applica" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Il valore calcolato non andrà al di sotto di questa soglia", "description-step-count": "Quante volte deve essere diviso l'intervallo di tempo corrente per calcolare il valore", - "interval-options": "Opzioni di intervallo" + "interval-options": "Opzioni di intervallo", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Utilizzato su {{count}} dashboard", "usage-count_other": "Utilizzato su {{count}} dashboard" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Esiste già una dashboard con lo stesso nome nella cartella selezionata. Desideri comunque salvare questa dashboard?", "title-name-already-exists": "Nome già in uso" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Aggiungi un'altra trasformazione", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Elimina tutte le trasformazioni", "title-delete-all-transformations": "Eliminare tutte le trasformazioni?" }, @@ -5635,9 +5848,13 @@ "description-examples": "I gruppi di acquisizione con nome possono essere utilizzati per separare il testo e il valore di visualizzazione (<1>vedi esempi).", "description-optional": "Facoltativo, se desideri estrarre parte di un nome di serie o di un segmento di nodo della metrica.", "label-data-source": "Sorgente dati", + "label-static-options-sort": "", "label-target-data-source": "Origine dati di destinazione", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opzioni query", - "selection-options": "Seleziona opzioni" + "selection-options": "Seleziona opzioni", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Desideri davvero ripristinare la dashboard alla versione {{version}}? Tutte le modifiche non salvate andranno perse.", + "confirmText-restore-version": "", "title-restore-version": "Ripristina versione" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Cartella", "render-footer": { "title-failed-to-save-dashboard": "Impossibile salvare la dashboard" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Consente di selezionare più valori contemporaneamente", "description-enables-option-include-variables": "Abilita un'opzione per includere tutti i valori", - "description-enables-users-custom-values": "Consente agli utenti di aggiungere valori personalizzati all'elenco" + "description-enables-users-custom-values": "Consente agli utenti di aggiungere valori personalizzati all'elenco", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Attiva/disattiva il menu di condivisione" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opzionale)", "text-options": "Opzioni testo" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Desideri davvero scollegare questo pannello?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Questa variabile non è referenziata da nessuna variabile o dashboard.", "aria-label-variable-referenced-other-variables-dashboard": "Questa variabile è referenziata da altre variabili o dashboard.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Modulo di modifica delle variabili", "back-to-list": "Torna all'elenco", + "confirmText": { + "delete-variable": "" + }, "delete": "Elimina", "description-optional-display-name": "Nome visualizzato facoltativo", "description-template-variable-characters": "Il nome della variabile del modello. (Massimo 50 caratteri)", "general": "Generale", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Testo descrittivo", "placeholder-label-name": "Nome etichetta", "placeholder-variable-name": "Nome variabile", @@ -5829,13 +6066,25 @@ "variable": "Variabile" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Elimina variabile", "tooltip-duplicate-variable": "Duplica variabile", "tooltip-remove-variable": "Rimuovi variabile" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Nascondi" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Mostra utilizzi per: {{variableId}}", "tooltip-show-usages": "Mostra utilizzi" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Toggle di selezione della versione {{version}}", "date": "Data", + "name-latest": "", "notes": "Note", "restore": "Ripristina", "updated-by": "Aggiornato da", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Carica" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limite", "label-value": "Valore", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max", "label-min": "Min", - "label-value": "Valore" + "label-value": "Valore", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Seleziona operatore nome servizio", "aria-label-select-span-name": "Seleziona nome intervallo", "aria-label-select-span-name-operator": "Seleziona l'operatore del nome dell'intervallo", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtri intervallo", "label-duration": "Durata", "label-service-name": "Nome del servizio", @@ -6939,6 +7199,8 @@ "split-widen": "Allarga riquadro" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Lascia un feedback", "label-copied": "Copiato!", "label-export": "Esporta", @@ -7046,6 +7308,7 @@ "when": "QUANDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operazione" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Cancella cartelle", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtro cartelle", "select-placeholder": "Filtra per cartella" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Siamo spiacenti, non è stato possibile completare la richiesta. Riprova.", "send-custom-feedback": "Invia" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "", "label-longitude": "" @@ -7153,6 +7463,14 @@ "center": "", "zoom": "" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "", "no-layers": "" @@ -7185,16 +7511,38 @@ "label-zoom": "Zoom", "use-current-map-settings": "" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Simbolo" }, "measure-overlay": { "tooltip-show-measure-tools": "" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "" }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "", "label-baseline": "", "label-color": "Colore", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "", "label-text-label": "", "label-x-offset": "", - "label-y-offset": "" + "label-y-offset": "", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operatore di confronto", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "", "placeholder-numeric-value": "", "placeholder-value": "valore" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "Colore {{colorLabel}}" }, "confirm-button": { - "cancel": "Annulla" + "cancel": "Annulla", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Digita \"{{confirmPromptText}}\" per confermare" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "attiva/disattiva il pannello di riduzione", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Annulla query", "tooltip-cancel-loading": "Annulla query", "tooltip-stop-streaming": "Interrompi streaming", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Corrispondenza", "inspect-drawer-title": "Ispeziona valore", "inspect-menu-label": "Ispeziona valore", + "nested-table": { + "no-data": "" + }, "no-values-label": "Nessun valore", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} di {{numRows}} righe" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Aggiungi", + "placeholder-new-tag": "", "remove": "Rimuovi tag: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Fai clic per {{actionTitle}}", "footer-click-to-navigate": "Fai clic per aprire {{linkTitle}}", "timestamp": "Indicazione data/ora" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Crea pannello della libreria" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Desideri eliminare questo pannello?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Utilizzato su {{count}} dashboard" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Cerca per nome o descrizione" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Caricamento del pannello della libreria in corso..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Crea un link diretto e personalizzato per condividere il pannello all'interno della tua organizzazione, con le seguenti impostazioni di personalizzazione:", "download-image": "Scarica immagine", - "render-image": "Genera immagine", - "render-image-error": "Impossibile eseguire il rendering dell'immagine del pannello", - "render-image-error-description": "Si è verificato un errore durante la generazione dell'immagine" + "render-image": "Genera immagine" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "A capo" }, "log-details": { - "close": "Chiudi i dettagli del registro", "fields": "Campi", "links": "Link", "log-line": "Riga registro", @@ -8405,6 +8795,42 @@ "show-more": "mostra altro", "tooltip-error": "Errore: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Copia il link alla riga del registro", "copy-log": "Copia la riga del registro", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Collegamenti dati", "aria-label-fields": "Campi", + "aria-label-line": "", "aria-label-log-level": "Livello registro", "aria-label-no-details": "Nessun dettaglio" }, @@ -8598,6 +9030,8 @@ "updated-on": "Aggiornato il" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Elimina" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Importa dashboard" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Questo strumento può migrare alcune risorse da questa installazione al tuo stack cloud. Per iniziare, è necessario creare un'istantanea di questa installazione. La creazione di un'istantanea richiede in genere meno di due minuti. L'istantanea viene archiviata insieme a questa installazione di Grafana.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Il dashboard è già stato predisposto e viene gestito da Grafana nell'istanza cloud. In futuro, ti consigliamo di utilizzare il dashboard con provisioning. Se desideri comunque copiare il dashboard nell'istanza cloud, modifica l'ID del dashboard nel JSON del dashboard, salva una nuova istantanea e carica di nuovo.", "datasource-already-managed": "L'origine dati è già stata predisposta e viene gestita da Grafana nell'istanza cloud. Se si tratta di una risorsa diversa, imposta un altro UID e riprova.", "datasource-invalid-url": "È presente un'origine dati con un URL non valido. Fornisci un URL valido e riprova.", @@ -9307,8 +9752,8 @@ "marker": { "100-node-count": "", "aria-label-hidden-marker": "", - "node-count_one": "", - "node-count_other": "" + "node-count_one": "{{count}} nodi", + "node-count_other": "{{count}} nodi" }, "node": { "aria-label-node-title": "" @@ -9318,10 +9763,10 @@ "aria-label-nodes-hidden-warning": "", "computing-layout": "", "no-data": "Nessun dato", - "hidden-nodes_one": "", - "hidden-nodes_other": "", - "processed-nodes_one": "", - "processed-nodes_other": "" + "hidden-nodes_one": "<0> {{count}} nodi sono nascosti per motivi di prestazioni.", + "hidden-nodes_other": "<0> {{count}} nodi sono nascosti per motivi di prestazioni.", + "processed-nodes_one": "<0> Il layout a livelli potrebbe essere lento con {{count}} nodi.", + "processed-nodes_other": "<0> Il layout a livelli potrebbe essere lento con {{count}} nodi." }, "node-graph-panel": { "no-data-found-in-response": "" @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Seleziona organizzazione" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Non disponi dell'autorizzazione per visualizzare questa pagina.", "title-access-denied": "Accesso negato" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Non è stato trovato alcun componente della pagina dell'app principale" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "plug-in aggiornati" }, "versions": { - "confirmation-text-1": "Vuoi davvero effettuare il downgrade alla versione", - "confirmation-text-2": "Normalmente non dovresti farlo", + "confirmation-text": "", "downgrade-confirm": "Effettua il downgrade", "downgrade-title": "Effettua il downgrade della versione del plug-in" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Nessun plug-in trovato" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Aggiornamento in corso" }, "install-controls-button": { - "title-uninstall-modal": "Disinstalla {{plugin}}" + "title-uninstall-modal": "Disinstalla {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Questo componente aggiuntivo non è pubblicato su <2>grafana.com/plugins e non può essere gestito tramite il catalogo.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Questa funzione è attualmente in fase di sviluppo attivo. Per un'esperienza ottimale e gli ultimi miglioramenti, consigliamo di utilizzare la <2>nightly build di Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Selettore account di servizio", "select-placeholder": "Inizia a digitare per cercare gli account di servizio" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Aggiungi token dell'account di servizio", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Elimina account di servizio", "disable-service-account": "Disabilita account di servizio", "enable-service-account": "Abilita account di servizio", @@ -10907,6 +11375,7 @@ "used-by": "Utilizzato da" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Modifica" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Aggiungi account di servizio", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Cerca account di servizio per nome", "sub-title": "Gli account di servizio e i relativi token possono essere utilizzati per autenticarsi con l'API Grafana. Scopri di più nella nostra <2>documentazione.", "title-delete-service-account": "Elimina account di servizio", - "title-disable-service-account": "Disabilita account di servizio" + "title-disable-service-account": "Disabilita account di servizio", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Questo token è scaduto", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Esporta in formato JSON", "share-externally-title": "Condividi esternamente", "share-internally-title": "Condividi internamente", @@ -10972,6 +11448,19 @@ "view-button": "Visualizza JSON", "view-button-yaml": "Visualizza YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Crea il pannello della libreria." }, @@ -10980,7 +11469,7 @@ "info-text": "Crea un link diretto a questo dashboard o pannello, personalizzato con le opzioni seguenti.", "link-url": "Collega URL", "render-alert": "Plug-in del renderer di immagini non installato", - "render-instructions": "Per eseguire il rendering di un'immagine del pannello, è necessario installare il <2>plug-in del renderer di immagini Grafana. Contatta l'amministratore di Grafana per installare il plug-in.", + "render-instructions": "", "rendered-image": "Immagine con rendering del collegamento diretto", "save-alert": "Il dashboard non è stato salvato", "save-dashboard": "Per eseguire il rendering di un'immagine del pannello, è necessario prima salvare il dashboard.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Anteprima pannello" }, @@ -11315,8 +11810,8 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "", - "too-many-points_other": "" + "too-many-points_one": "Troppi punti da visualizzare correttamente. <1>Aggiorna la query per restituire meno punti <3>({{count}} punti ricevuti)", + "too-many-points_other": "Troppi punti da visualizzare correttamente. <1>Aggiorna la query per restituire meno punti <3>({{count}} punti ricevuti)" } }, "support-bundles": { @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "", "description-title-text": "", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Opzione tag" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Selettore team", "select-placeholder": "Seleziona un team" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Campo/i o numero" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Nome alternativo", "label-mode": "Modalità", "label-replace-all-fields": "Sostituisci tutti i campi" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Etichetta", "label-name": "Nome", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Fotogramma" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Aggiungi un trasformatore del tipo di campo di conversione", "aria-label-remove-convert-field-type-transformer": "Rimuovi il trasformatore del tipo di campo di conversione", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Browser", "utc": "" @@ -11793,7 +12314,13 @@ "label-input-format": "Formato di inserimento", "label-join-with": "Unisci con", "label-set-timezone": "Imposta fuso orario", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Tipo", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Utilizza un separatore esplicito quando unisci i valori di un array", "tooltip-input-format": "Specifica il formato del campo di input in modo che Grafana possa analizzare correttamente la stringa della data.", "tooltip-specify-the-output-format": "Specifica il formato di uscita.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Off" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Aggiungi valore enum", "generate-enum-values-from-data": "Genera valori enum dai dati" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Elimina" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Delimitatore", "label-format": "Formato", "label-keep-time": "Mantieni orario", "label-replace-all-fields": "Sostituisci tutti i campi", "label-source": "Origine", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automatico", "placeholder-select-delimiter": "Seleziona delimitatore...", "tooltip-regexp": "Esempio: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Colore soglia" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", - "label-lookup": "Ricerca" + "label-lookup": "Ricerca", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Impostazioni aggiuntive", @@ -11848,12 +12413,26 @@ "use-as": "Usa come" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Dalla variabile", "label-identifier": "Identificatore", "label-use-variable": "Utilizza variabile", "label-variable": "Variabile", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Modello di espressione regolare" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Campo", "label-match": "Corrispondenza", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Aggiungi condizione", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Condizioni", - "label-filter-type": "Tipo di filtro" + "label-filter-type": "Tipo di filtro", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", "label-format": "Formato", "label-substring-range": "Intervallo di stringa secondaria" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formato", "label-set-timezone": "Imposta fuso orario", @@ -11878,6 +12491,70 @@ "tooltip-format": "Il formato di output per il campo specificato come <2>stringa di formato Moment.js.", "tooltip-timezone-manually": "Imposta manualmente il fuso orario della data" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Browser", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorato", "placeholder-select-stats": "Seleziona statistiche" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "I calcoli non avranno alcun effetto se non sono presenti campi da raggruppare" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Se abilitato, le tabelle nidificate mostreranno i nomi dei campi come intestazione della tabella", "label-show-field-names-in-nested-tables": "Mostra i nomi dei campi nelle tabelle nidificate", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "I calcoli non avranno alcun effetto se non sono presenti campi da raggruppare." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Valore cella", "label-column": "Colonna", "label-empty-value": "Valore vuoto", "label-row": "Riga", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automatico", "placeholder-default": "Impostazione predefinita: 30", "placeholder-none": "Nessuno" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Come percentile", "tooltip-transform-index-percentile": "Trasforma l'indice di riga in percentile." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Il dato inserito non contiene etichette", "no-input-or-labels-found": "Nessun inserimento (o etichetta) trovato", @@ -11969,10 +12685,30 @@ "label-labels": "Etichette", "label-mode": "Modalità", "label-value-field-name": "Nome campo valore", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opzionale) Seleziona etichetta", "tooltip-replace-value-field-label": "Sostituisci il nome del campo del valore con un'etichetta" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Valore o variabile" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "I valori stringa sono rappresentati come campi separati anziché come etichette" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "L'unione non ha alcun effetto se applicata a un singolo fotogramma." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "L'unione non ha alcun effetto se applicata a un singolo fotogramma.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Tutti i valori sono numerici", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Orario in ordine crescente" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automatico", "field-order-manual": "", - "first-frame-warning": "Organizza campi funziona solo con un singolo fotogramma. Valuta di applicare una trasformazione di unione o di filtrare prima l'inserimento." + "first-frame-warning": "Organizza campi funziona solo con un singolo fotogramma. Valuta di applicare una trasformazione di unione o di filtrare prima l'inserimento.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Imposta la modalità di visualizzazione dei nomi dei campi selezionati. Dato che il nome del fotogramma di solito è più adatto per i dati tabulari" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Partizione per valori funziona solo con un singolo fotogramma.", "tooltip-keeps-partition-fields-frames": "Mantiene i campi di partizione nei fotogrammi" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formato", - "label-info": "Informazioni" + "label-info": "Informazioni", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "e", @@ -12035,6 +12815,9 @@ "label-operation": "Operazione" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Calcoli", "label-include-time": "Includi l'ora", "label-labels-to-fields": "Etichette ai campi", @@ -12049,12 +12832,23 @@ "series-to-rows": "" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Scegli stat" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Valore o variabile", "variable-regex-documentation": "Formatta la variabile a più valori in una stringa regex" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Numero di punti X, Y da prevedere" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Corrispondenza", "label-replace": "Sostituisci", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Campo", - "label-mode": "Modalità" + "label-mode": "Modalità", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Campo", "label-reverse": "Inverti", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Seleziona campo" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat.", "label-time-field": "Campo dell'ora", @@ -12121,10 +12960,34 @@ "placeholder-value": "Valore", "tooltip-name-for-value-fields": "Nome per i campi valore" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Operazione", "placeholder-field": "Campo" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Campi di valori multipli di qualsiasi tipo", "single-frame": "Singolo fotogramma", @@ -12177,6 +13040,7 @@ "title": "Organizzazioni" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Selettore utente", "select-placeholder": "Inizia a digitare per cercare l'utente" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Elimina variabile" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Aggiorna" }, "query-variable-sort-select": { - "description-values-variable": "Come ordinare i valori di questa variabile" + "description-values-variable": "Come ordinare i valori di questa variabile", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "valore predefinito, se presente", "text-options": "Opzioni testo" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Nome visualizzato facoltativo", "description-template-variable-characters": "Il nome della variabile del modello. (Massimo 50 caratteri)", "general": "Generale", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Testo descrittivo", "placeholder-label-name": "Nome etichetta", "placeholder-variable-name": "Nome variabile", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Duplica variabile", "tooltip-remove-variable": "Rimuovi variabile" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Attiva/disattiva tutti i valori" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Mostra utilizzi" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 47a76879ee1..d5b485af2a8 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "一部機能は安定版(GA)として標準で有効になっていますが、いくつかの機能は現在暫定的なベータ版の段階であり、早期導入することが可能です。", "confirm-modal-body-2": "変更する前に、各機能の変更がもたらす影響を理解することをお勧めします。", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "ベータ版", "content-general-availability": "一般提供", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "組織を削除", + "confirmText-delete": "", "title-delete": "削除" }, "anon-users": { "not-found": "匿名ユーザーは見つかりませんでした。" }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "すべてのデバイスから強制ログアウトする" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "この組織のユーザーを表示する権限がありません。この組織を更新するには、サーバー管理者に連絡してください。", "heading": "組織を編集", @@ -208,9 +216,11 @@ "not-editable": "このユーザーの役割は、認証プロバイダーから同期されているため編集できません。詳細については、<1>Grafana認証ドキュメントを参照してください。" }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "ロール" }, + "confirmText-delete": "", "delete-aria-label": "ユーザーを削除:{{name}}", "title-delete": "削除" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "これらのシステム設定は、grafana.iniまたはcustom.iniで定義されています(またはENV変数で上書きされています)。これらを変更するには、現在、Grafanaを再起動する必要があります。" }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "エンタープライズライセンス" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "変更", + "confirmText-change": "", "grafana-admin-key": "Grafana管理者", "grafana-admin-no": "いいえ", "grafana-admin-yes": "はい", "title": "権限" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "ユーザーの削除", "disable-button": "無効のユーザー", "edit-button": "編集", @@ -312,6 +330,9 @@ "title-delete-user": "ユーザを削除", "title-disable-user": "ユーザーを無効化" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "ブラウザとOS", "force-logout-all-button": "すべてのデバイスから強制ログアウト", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "アクティブタイミング" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "さらに追加" }, @@ -453,6 +478,9 @@ "label-muting-grouping-and-timings-optional": "ミュート、グループ化、タイミング(任意)", "title-muting-grouping-and-timings": "ミュート、グループ化、タイミング" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "リンクをコピー", "duplicate": "複製", @@ -476,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAMLを編集", @@ -542,6 +573,7 @@ "view-configuration": "設定を表示" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "内部Grafana Alertmanager設定は手動で変更できません。この設定を変更するには、UI経由で個々のリソースを編集してください。", "gma-manual-configuration-is-not-supported": "手動構成の変更はサポートされていません", "message": { @@ -556,11 +588,13 @@ "title-resetting-alertmanager-configuration": "Alertmanager設定のリセット" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "比較", "restore": "復元", "text-latest": "最新" }, + "confirmText-yes-restore-configuration": "", "loading": "読み込み中...", "no-previous-configurations": "以前の設定はありません", "this-might-take-a-while": "これには少し時間がかかります…", @@ -840,8 +874,10 @@ }, "contact-point-header": { "aria-label-more-actions": "連絡先「{{contactPointName}}」のその他の操作", + "ariaLabel-delete": "", "button-edit": "編集", "button-view": "表示", + "export-ariaLabel-export": "", "export-label-export": "エクスポート", "label-delete": "削除", "label-manage-permissions": "権限を管理する", @@ -1374,6 +1410,7 @@ "label-disable-resolved-message": "解決済みメッセージを無効にする" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "正の整数で入力してください。", "must-enter-a-group-name": "グループ名の入力が必要です" @@ -1465,15 +1502,12 @@ "group-details": { "ds-features-error": "データソースの読み込み中にエラーが発生しました", "edit": "編集", - "evaluations-to-fire": "発生する評価サイクル", "export": "エクスポート", "folder": "フォルダ", "group-loading-error": "グループの読み込み中にエラーが発生しました", "interval": "間隔", "namespace": "名前空間", - "pending-period": "保留期間", - "recording": "記録", - "rule-name": "ルール名" + "new": "" }, "group-edit": { "ds-error": "データソースの読み込み中にエラーが発生しました", @@ -1519,8 +1553,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "開始する", "label-insights": "インサイト", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "アラート" }, "import-to-gma": { @@ -1819,7 +1865,11 @@ "other-data-sources": "その他のデータソース" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "無効", @@ -2069,9 +2119,11 @@ "update-errors": { "conflict": "通知ポリシーツリーは別のユーザーによって更新されました。", "error-code": "エラーメッセージ:「{{error}}」", - "fallback": "通知ポリシーの更新中にエラーが発生しました。", + "routes": { + "conflictingMatchers": "" + }, "suffix": "ページを更新して、やり直してください。", - "title": "通知ポリシーの保存中にエラーが発生しました" + "title": "" }, "n-more-policies_other": "{{count}}件の追加のポリシー" }, @@ -2126,6 +2178,7 @@ "query-and-expressions-step": { "add-query": "クエリを追加", "body-queries-expressions-configured": "アラート対象となるクエリまたは式を少なくとも1つ作成してください", + "confirmText-deactivate": "", "expressions": "式", "loading-data-sources": "データソース読み込み中...", "manipulate-returned-queries-other-operations": "数式やその他の演算を使用してクエリから返されたデータを操作します。", @@ -2175,6 +2228,9 @@ "label-export-all": "すべてをエクスポート" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "次の理由によりクエリエディタを読み込めませんでした:{{errorMessage}}" }, @@ -2193,6 +2249,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "元のルールはプロビジョニングされたもので、UI上で作成したルールには使用できないため、コピーしたルールには新しい評価グループを設定する必要があります。", "body-not-provisioned": "新しいルールはプロビジョニングされたルールとしてマーク<1>されません。", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "プロビジョニングされたアラートルールをコピー" }, "redirect-to-rule-viewer": { @@ -2389,8 +2446,6 @@ "title-inspect-alert-rule": "アラートルールを検査" }, "rule-list": { - "cannot-find-rule-details-for": "UID{{uid}}のルール詳細が見つかりません ", - "cannot-load-rule-details-for": "UID{{uid}}のルール詳細を読み込めません ", "configure-datasource": "構成", "draft-new-rule": "新しいルールの下書きを作成", "ds-error": { @@ -2585,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "お探しのデータソースが見つかりませんか?一部のデータソースはアラートに対応していません。詳細についてはアイコンをクリックしてください。" }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "新しいAlertmanagerを追加", "builtin-alertmanager": "内蔵Alertmanager", @@ -2737,6 +2797,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "通知テンプレートを選択", "loading": "読み込み中...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "通知テンプレートを選択" } @@ -2763,6 +2826,8 @@ }, "templates-table": { "actions": "操作", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "テンプレートがまだ定義されていません。", "template-group": "テンプレートグループ", "title-delete-template-group": "テンプレートグループを削除" @@ -2815,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}}前)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "対象フォルダが空ではないため、一部のルールが上書きまたは削除される可能性があります。これらのアラートルールをGrafana管理ルールにインポートしてもよろしいですか?", @@ -2890,6 +2960,11 @@ "title-delete-contact-point": "連絡先を削除" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "通知ポリシーを削除" @@ -3046,7 +3121,8 @@ "annotation-field-mapper": { "annotation": "注釈", "first-value": "最初の値", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "注釈クエリを追加", @@ -3054,6 +3130,16 @@ "info-box-content-2": "詳細については、<2>注釈のドキュメントをご覧ください。", "title": "カスタム注釈クエリがまだ追加されていません" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}}件のイベント({{numFields}}フィールドから)", "no-events-found": "イベントが見つかりません", @@ -3129,6 +3215,9 @@ "define-allowed-teams-ids-label": "許可されたチームIDを定義する", "display-name-description": "ログインページに「...でサインイン」と表示されます。複数のIDプロバイダーまたはSSOプロトコルを使用する場合に役立ちます。", "display-name-label": "表示名", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "OAuth2 IDトークンの属性マップ内でユーザーメール検索に使用するキーの名前。", "email-attribute-name-label": "メール属性名", "email-attribute-path-description": "ユーザー情報からユーザーメールを検索する際に使用するJMESPath式。", @@ -3180,7 +3269,7 @@ "team-ids-github": "チームIDの整数リスト。", "team-ids-label": "チームID", "team-ids-numbers": "チームIDは数字である必要があります。", - "team-ids-other": "チームIDの文字列リスト。", + "team-ids-other": "", "team-ids-placeholder": "チームIDを入力し、Enterキーを押して追加します", "teams-url-description": "チームIDを照会するために使用するURL。設定されていない場合、デフォルト値は/teamsです。", "teams-url-description-oauth": "「{{ teamsURLLabel }}」を設定する場合は、「{{ teamIDsAttributePathLabel }}」も設定する必要があります。", @@ -3224,6 +3313,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "デフォルト値にリセット" }, + "confirmText-reset": "", "disable": "無効化", "disabling": "無効にしています...", "discard": "破棄", @@ -3455,11 +3545,11 @@ "button-create": "作成", "button-creating": "作成中...", "cancel": "キャンセル", - "error-invalid-folder-name": "無効なフォルダ名", "error-required": "フォルダ名は必須です", "folder-name-input-placeholder-enter-folder-name": "フォルダ名を入力してください", "label-folder-name": "フォルダ名", "text-pull-request-created": "このフォルダへの変更を含むプルリクエストが作成されました。", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "プルリクエストが作成されました", "title-this-repository-is-read-only": "このリポジトリは読み取り専用です" }, @@ -3877,6 +3967,7 @@ "tooltip": "閉じる" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "オプションの読み込み中にエラーが発生しました。" @@ -4145,8 +4236,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "指定した数で時間範囲を分割して、間隔を動的に計算します" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4308,12 +4399,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "外部サイトに移動しますか?" } }, "render-left-actions": { - "text-public": "パブリック", "tooltip-view-as-scene": "シーンとして表示" } }, @@ -4506,6 +4599,14 @@ "errors": { "failed-to-load": "ダッシュボードの読み込みに失敗しました" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "ダッシュボードの取得に失敗しました" @@ -4522,6 +4623,13 @@ "editable": "編集可能", "readonly": "読み取り専用" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4571,6 +4679,18 @@ "transform-data": "データを変換" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4825,6 +4945,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "ダッシュボードをバージョン{{version}}に復元してもよろしいですか?未保存の変更はすべて失われます。", + "confirmText-restore-version": "", "title-restore-version": "バージョンの復元" }, "row-options-button": { @@ -4875,6 +4996,9 @@ "title-not-unique": "このタイトルは重複しています" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "名前を付けて保存" }, @@ -4909,6 +5033,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "選択したフォルダには同じ名前のダッシュボードがすでに存在します。<1><2>このダッシュボードの保存を続行しますか?", "body-version-mismatch": "他のユーザーがこのダッシュボードを更新しました<1><2>このダッシュボードを保存しますか?", + "confirmText-save-and-overwrite": "", "title-name-exists": "競合", "title-version-mismatch": "競合" }, @@ -5074,7 +5199,6 @@ "playlist-next": "次のダッシュボードに移動", "playlist-previous": "前のダッシュボードに戻る", "playlist-stop": "プレイリストを停止", - "public-dashboard": "パブリック", "refresh": "ダッシュボードを更新", "save": "ダッシュボードを保存", "save-dashboard": { @@ -5105,7 +5229,9 @@ "label-apply-transformation-to": "変換の適用先" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "デバッグ", "title-disable-transformation": "変換を無効化", "title-filter": "フィルタリング", @@ -5125,12 +5251,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "変換を検索", "show-images": "画像を表示", - "title-add-another-transformation": "別の変換を追加" + "title-add-another-transformation": "別の変換を追加", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "別の変換を追加" }, + "confirmText-delete-all": "", "delete-all-transformations": "すべての変換を削除", "title-delete-all-transformations": "すべての変換を削除しますか?", "tooltip-clear-search": "検索結果をクリア", @@ -5167,6 +5298,7 @@ "version-history-table": { "aria-label-toggle-selection": "バージョン{{version}}の選択を切り替え", "date": "日付", + "name-latest": "", "notes": "メモ", "restore": "復元", "updated-by": "更新者", @@ -5243,7 +5375,8 @@ "description-enables-users-custom-values": "ユーザーがリストにカスタム値を追加できるようにします", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "CSV形式で次のように次元を指定:{{name}}, {{value}}", "label-data-source": "データソース", - "label-use-static-key-dimensions": "静的キー次元を使用" + "label-use-static-key-dimensions": "静的キー次元を使用", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5316,6 +5449,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "公開URLを取り消す" } @@ -5327,6 +5463,7 @@ }, "custom-variable-form": { "custom-options": "カスタムオプション", + "name-values-separated-comma": "", "selection-options": "選択オプション" }, "dashboard-edit-pane-renderer": { @@ -5345,6 +5482,12 @@ "label-type": "タイプ", "label-url": "URL", "label-with-tags": "タグ付き", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "ダッシュボードを開く" }, "dashboard-link-list": { @@ -5357,15 +5500,9 @@ "type": "タイプ" }, "dashboard-preview-banner": { - "not-saved": "値はまだGrafanaデータベースに保存されていません", "not-yet-saved": "値はGrafanaデータベースに保存されていません", - "open-pull-request-in-git-hub": "GitHubでプルリクエストを開く", - "title-dashboard-loaded-branch-git-hub": "このダッシュボードはGitHubのブランチから読み込まれます。", "title-dashboard-loaded-external-repository": "このダッシュボードは外部リポジトリから読み込まれます", - "title-dashboard-loaded-request-git-hub": "このダッシュボードは、GitHubのプルリクエストから読み込まれます。", - "title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました", - "value-not-saved": "値はまだGrafanaデータベースに保存されていません", - "view-pull-request-in-git-hub": "GitHubでプルリクエストを表示する" + "title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました" }, "dashboard-scene": { "text": { @@ -5391,6 +5528,8 @@ "data-source-options": "データソースオプション", "description-instance-name-filter": "変数値リストで選択するデータソースインスタンスの正規表現フィルター。すべての場合は、空欄にしてください。", "example-instance-name-filter": "例:", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "選択オプション" }, "default-grid-layout-manager": { @@ -5436,12 +5575,61 @@ "empty-transformations-message": { "add-transformation": "変換を追加" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "列オプション", "row-height-options": "行の高さオプション" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5466,7 +5654,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "CSV形式で次のように次元を指定:{{name}}, {{value}}", "group-by-options": "グループ化オプション", "label-data-source": "データソース", - "label-use-static-group-by-dimensions": "静的グループ次元を使用" + "label-use-static-group-by-dimensions": "静的グループ次元を使用", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "クリップボードにコピー", @@ -5502,9 +5691,14 @@ "apply": "適用" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "計算値はこのしきい値を下回りません", "description-step-count": "値を計算するために現在の時間範囲を分割する回数", - "interval-options": "間隔オプション" + "interval-options": "間隔オプション", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5523,11 +5717,28 @@ "last-edited": "が{{timeAgo}}に実施", "usage-count_other": "{{count}}件のダッシュボードで使用中" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "選択したフォルダに同じ名前のダッシュボードがすでに存在します。このダッシュボードの保存を続行しますか?", "title-name-already-exists": "名前はすでに存在します" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "外部サイトに移動しますか?" } @@ -5563,6 +5774,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "別の変換を追加", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "すべての変換を削除", "title-delete-all-transformations": "すべての変換を削除しますか?" }, @@ -5615,9 +5828,13 @@ "description-examples": "名前付きキャプチャグループを使用すると、表示テキストと値を分けることができます(<1>例を参照)。", "description-optional": "シリーズ名やメトリックノードセグメントの一部を抽出したい場合の任意設定です。", "label-data-source": "データソース", + "label-static-options-sort": "", "label-target-data-source": "ターゲットデータソース", + "label-use-static-options": "", + "name-regex": "", "query-options": "クエリオプション", - "selection-options": "選択オプション" + "selection-options": "選択オプション", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5630,6 +5847,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "ダッシュボードをバージョン{{version}}に復元してもよろしいですか?未保存の変更はすべて失われます。", + "confirmText-restore-version": "", "title-restore-version": "バージョンの復元" }, "save-button": { @@ -5647,7 +5865,8 @@ "label-folder": "フォルダ", "render-footer": { "title-failed-to-save-dashboard": "ダッシュボードを保存できませんでした" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5722,7 +5941,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "複数の値を同時に選択可能にします", "description-enables-option-include-variables": "すべての値を含めるオプションを有効にします", - "description-enables-users-custom-values": "ユーザーがリストにカスタム値を追加できるようにします" + "description-enables-users-custom-values": "ユーザーがリストにカスタム値を追加できるようにします", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "共有メニューを切り替え" @@ -5742,6 +5965,9 @@ "copy-to-clipboard-failed": "クリップボードへのコピーが失敗しました" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(任意)", "text-options": "テキストオプション" @@ -5765,6 +5991,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "このパネルのリンクを解除してもよろしいですか?" }, "unsaved-changes-modal": { @@ -5781,6 +6009,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "この変数はどの変数やダッシュボードからも参照されていません。", "aria-label-variable-referenced-other-variables-dashboard": "この変数は他の変数やダッシュボードから参照されています。", @@ -5790,10 +6021,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "変数エディタフォーム", "back-to-list": "一覧に戻る", + "confirmText": { + "delete-variable": "" + }, "delete": "削除", "description-optional-display-name": "表示名(任意)", "description-template-variable-characters": "テンプレート変数の名前(最大50文字)。", "general": "一般", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "説明用のテキスト", "placeholder-label-name": "ラベル名", "placeholder-variable-name": "変数名", @@ -5808,13 +6045,25 @@ "variable": "変数" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "変数を削除", "tooltip-duplicate-variable": "変数を複製", "tooltip-remove-variable": "変数を削除" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "非表示" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "{{variableId}}の使用状況の表示", "tooltip-show-usages": "使用状況を表示" @@ -5841,6 +6090,7 @@ "version-history-table": { "aria-label-toggle-selection": "バージョン{{version}}の選択を切り替え", "date": "日付", + "name-latest": "", "notes": "メモ", "restore": "復元", "updated-by": "更新者", @@ -6228,7 +6478,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "アップロード" @@ -6266,6 +6517,7 @@ }, "label-limit": "制限", "label-value": "値", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6274,9 +6526,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "最大", "label-min": "最小", - "label-value": "値" + "label-value": "値", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6848,6 +7106,8 @@ "aria-label-select-service-name-operator": "サービス名演算子を選択", "aria-label-select-span-name": "スパン名を選択", "aria-label-select-span-name-operator": "スパン名演算子を選択", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "スパンフィルター", "label-duration": "継続時間", "label-service-name": "サービス名", @@ -6918,6 +7178,8 @@ "split-widen": "ペインを広げる" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "フィードバックを送信", "label-copied": "コピーしました!", "label-export": "エクスポート", @@ -7025,6 +7287,7 @@ "when": "条件" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "操作" }, "math": { @@ -7055,6 +7318,7 @@ }, "folder-filter": { "clear-folder-button": "フォルダをクリア", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "フォルダフィルター", "select-placeholder": "フォルダでフィルタリング" }, @@ -7123,7 +7387,53 @@ "incomplete-request-error": "申し訳ありませんが、リクエストを完了できませんでした。もう一度やり直してください。", "send-custom-feedback": "送金" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "緯度", "label-longitude": "経度" @@ -7132,6 +7442,14 @@ "center": "中央:", "zoom": "ズーム:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "レイヤー" @@ -7154,6 +7472,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "ジオマップスタイルルールを追加" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "レイヤーを追加", "no-layers": "レイヤーがありませんか?" @@ -7164,16 +7490,38 @@ "label-zoom": "ズーム", "use-current-map-settings": "現在のマップ設定を使用" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "シンボル" }, "measure-overlay": { "tooltip-show-measure-tools": "測定ツールを表示" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "ベースマップレイヤーはサーバー管理者によって設定されます。" }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "配置", "label-baseline": "ベースライン", "label-color": "色", @@ -7187,7 +7535,14 @@ "label-symbol-vertical-align": "シンボルの垂直配置", "label-text-label": "テキストラベル", "label-x-offset": "Xオフセット", - "label-y-offset": "Yオフセット" + "label-y-offset": "Yオフセット", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "比較演算子", @@ -7198,6 +7553,15 @@ "placeholder-feature-property": "機能プロパティ", "placeholder-numeric-value": "数値", "placeholder-value": "値" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7447,7 +7811,8 @@ "aria-label-selected-color": "{{colorLabel}}色" }, "confirm-button": { - "cancel": "キャンセル" + "cancel": "キャンセル", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "「{{confirmPromptText}}」と入力して確認" @@ -7629,6 +7994,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "パネルを折りたたむの切り替え", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "クエリをキャンセル", "tooltip-cancel-loading": "クエリをキャンセル", "tooltip-stop-streaming": "ストリーミングを停止", @@ -7695,6 +8062,9 @@ "filter-popup-match-case": "大文字/小文字を区別", "inspect-drawer-title": "値を検査", "inspect-menu-label": "値を検査", + "nested-table": { + "no-data": "" + }, "no-values-label": "値がありません", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}({{numRows}}行)" }, @@ -7703,6 +8073,7 @@ }, "tags-input": { "add": "追加", + "placeholder-new-tag": "", "remove": "タグ、{{name}}を削除" }, "time-sync-button": { @@ -7796,6 +8167,12 @@ "footer-click-to-action": "クリックして{{actionTitle}}", "footer-click-to-navigate": "クリックして{{linkTitle}}を開く", "timestamp": "タイムスタンプ" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8172,6 +8549,10 @@ "add-library-panel-modal": { "title-create-library-panel": "ライブラリパネルを作成" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "このパネルを削除しますか?" }, @@ -8194,7 +8575,7 @@ "usage-count_other": "{{count}}件のダッシュボードで使用中" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "名前と詳細で検索" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "ライブラリパネル読み込み中..." @@ -8242,9 +8623,7 @@ "share-panel": { "config-description": "次のカスタマイズ設定を使用して、組織内でパネルを共有するためのパーソナライズされた直接リンクを作成します。", "download-image": "画像をダウンロード", - "render-image": "画像を生成する", - "render-image-error": "パネル画像のレンダリングに失敗しました", - "render-image-error-description": "画像の生成中にエラーが発生しました" + "render-image": "画像を生成する" } }, "live": { @@ -8330,6 +8709,15 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8353,7 +8741,6 @@ "label-wrap-lines": "行を折り返す" }, "log-details": { - "close": "ログの詳細を閉じる", "fields": "フィールド", "links": "リンク", "log-line": "ログ行", @@ -8379,6 +8766,42 @@ "show-more": "さらに表示する", "tooltip-error": "エラー:{{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "ログ行へのリンクをコピー", "copy-log": "ログ行をコピー", @@ -8479,6 +8902,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8505,6 +8933,7 @@ "un-themed-log-details": { "aria-label-data-links": "データリンク", "aria-label-fields": "フィールド", + "aria-label-line": "", "aria-label-log-level": "ログレベル", "aria-label-no-details": "詳細なし" }, @@ -8572,6 +9001,8 @@ "updated-on": "更新日" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "削除" }, "unthemed-dashboard-import": { @@ -8581,8 +9012,17 @@ "text": { "import-dashboard": "ダッシュボードをインポート" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "このツールを使用すると、このインストールからクラウドスタックに一部のリソースを移行できます。開始するには、このインストールのスナップショットを作成する必要があります。スナップショットの作成には通常2分未満かかります。スナップショットは、このGrafanaインストールと一緒に保存されます。", @@ -8731,6 +9171,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "ダッシュボードは、クラウドインスタンスでGrafanaによってすでにプロビジョニングおよび管理されています。今後は、プロビジョニングされたダッシュボードの使用をお勧めします。それでもダッシュボードをクラウドインスタンスにコピーする場合は、ダッシュボードJSONでダッシュボードIDを変更し、新しいスナップショットを保存して再度アップロードしてください。", "datasource-already-managed": "ダッシュボードは、クラウドインスタンスでGrafanaによってすでにプロビジョニングおよび管理されています。これが別のリソースである場合は、別のUIDを設定して再試行してください。", "datasource-invalid-url": "URLが無効なデータソースがあります。有効なURLを入力して、再試行してください。", @@ -9418,6 +9860,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "組織を選択" }, "page": { @@ -9640,6 +10083,7 @@ "permission": "このページを表示する権限がありません。", "title-access-denied": "アクセス拒否" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "ルートアプリのページコンポーネントが見つかりません" }, "browse": { @@ -9683,8 +10127,7 @@ "update-status-text": "すべてのプラグインが更新されました" }, "versions": { - "confirmation-text-1": "バージョンにダウングレードしてもよろしいですか?", - "confirmation-text-2": "通常、これを行うべきではありません", + "confirmation-text": "", "downgrade-confirm": "ダウングレード", "downgrade-title": "プラグインのバージョンをダウングレードする" } @@ -9738,6 +10181,10 @@ "empty-state": { "message": "プラグインが見つかりませんでした" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9771,7 +10218,11 @@ "updating": "更新中" }, "install-controls-button": { - "title-uninstall-modal": "{{plugin}}をアンインストール " + "title-uninstall-modal": "{{plugin}}をアンインストール ", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "このプラグインは<2>grafana.com/pluginsに公開されておらず、カタログから管理できません。", @@ -9942,6 +10393,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "この機能は現在開発中です。最高の体験と最新の改善を利用するには、Grafanaの<2>ナイトリービルドの使用をお勧めします。" @@ -10802,6 +11262,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "サービスアカウント選択ツール", "select-placeholder": "サービスアカウントを検索するには入力を開始してください" }, @@ -10847,6 +11308,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "サービスアカウントトークンを追加", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "サービスアカウントを削除", "disable-service-account": "サービスアカウントを無効化", "enable-service-account": "サービスアカウントを有効化", @@ -10873,6 +11338,7 @@ "used-by": "使用者" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "編集" }, "service-account-role-row": { @@ -10886,10 +11352,15 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "サービスアカウントを追加", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "名前でサービスアカウントを検索", "sub-title": "サービスアカウントとそのトークンを使用して、Grafana APIに対する認証を行えます。詳細については<2>ドキュメントをご覧ください", "title-delete-service-account": "サービスアカウントを削除", - "title-disable-service-account": "サービスアカウントを無効化" + "title-disable-service-account": "サービスアカウントを無効化", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "このトークンは期限切れです", @@ -10903,6 +11374,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "JSON形式でエクスポート", "share-externally-title": "外部と共有", "share-internally-title": "内部と共有", @@ -10938,6 +11410,19 @@ "view-button": "JSONを表示", "view-button-yaml": "YAMLを表示" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "ライブラリパネルを作成します。" }, @@ -10946,7 +11431,7 @@ "info-text": "以下のオプションでカスタマイズした、このダッシュボードまたはパネルへの直接リンクを作成します。", "link-url": "URLをリンク", "render-alert": "画像レンダラープラグインがインストールされていません", - "render-instructions": "パネル画像をレンダリングするには、<2>Grafana画像レンダラープラグインをインストールする必要があります。プラグインをインストールするには、Grafana管理者にお問い合わせください。", + "render-instructions": "", "rendered-image": "レンダリングされた画像への直接リンク", "save-alert": "ダッシュボードは保存されていません", "save-dashboard": "パネル画像をレンダリングするには、最初にダッシュボードを保存する必要があります。", @@ -11014,6 +11499,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "パネルプレビュー" }, @@ -11353,6 +11844,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "画像が表示できない場合やスクリーンリーダーを使用するユーザーのために表示される代替テキスト", "description-title-text": "画像にカーソルを合わせたときに表示されるテキスト", @@ -11373,6 +11865,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11423,6 +11916,7 @@ "tag-option-label": "タグオプション" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "チームの選択ツール", "select-placeholder": "チームを選択" }, @@ -11724,6 +12218,18 @@ "placeholder-fields-or-number": "フィールドまたは数値" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "エイリアス", "label-mode": "モード", "label-replace-all-fields": "すべてのフィールドを置き換え" @@ -11738,6 +12244,13 @@ "concatenate-transformer-editor": { "label-label": "ラベル", "label-name": "名前", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "フレーム" }, "config-from-query-transformer-editor": { @@ -11748,6 +12261,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "フィールドタイプ変換機能を追加", "aria-label-remove-convert-field-type-transformer": "フィールドタイプ変換機能を削除", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "ブラウザ", "utc": "UTC" @@ -11758,7 +12275,13 @@ "label-input-format": "入力形式", "label-join-with": "結合先", "label-set-timezone": "タイムゾーンを設定", + "name": { + "convert-field-type": "" + }, "placeholder-type": "タイプ", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "配列値を結合する際に明示的な区切り文字を使用", "tooltip-input-format": "Grafanaが日付文字列を正しく解析できるよう、入力フィールドの形式を指定してください。", "tooltip-specify-the-output-format": "出力形式を指定してください。", @@ -11780,6 +12303,19 @@ "desc": "", "off": "オフ" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "列挙値を追加", "generate-enum-values-from-data": "データから列挙値を生成" @@ -11790,11 +12326,22 @@ "remove-enum-row-tooltip-delete": "削除" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "区切り文字", "label-format": "形式", "label-keep-time": "時間を保持", "label-replace-all-fields": "すべてのフィールドを置き換え", "label-source": "送信元", + "name": { + "extract-fields": "" + }, "placeholder-auto": "自動", "placeholder-select-delimiter": "区切り文字を選択...", "tooltip-regexp": "例:{{regexExample}}" @@ -11803,8 +12350,22 @@ "aria-label-threshold-color": "しきい値の色" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "フィールド", - "label-lookup": "検索" + "label-lookup": "検索", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "追加設定", @@ -11813,12 +12374,26 @@ "use-as": "次として使用" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "変数から", "label-identifier": "識別子", "label-use-variable": "変数を使用", "label-variable": "変数", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "正規表現パターン" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "フィールド", "label-match": "一致", @@ -11828,14 +12403,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "条件を追加", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "条件", - "label-filter-type": "フィルタータイプ" + "label-filter-type": "フィルタータイプ", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "フィールド", "label-format": "形式", "label-substring-range": "部分文字列の範囲" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "形式", "label-set-timezone": "タイムゾーンを設定", @@ -11843,6 +12452,70 @@ "tooltip-format": "<2>Moment.js形式文字列として指定されたフィールドの出力形式。", "tooltip-timezone-manually": "日付のタイムゾーンを手動で設定" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "ブラウザ", @@ -11870,22 +12543,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "無視", "placeholder-select-stats": "統計を選択" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "グループ化されているフィールドがない場合、計算は効果がありません" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "有効にすると、入れ子になったテーブルではフィールド名がテーブルヘッダーとして表示されます", "label-show-field-names-in-nested-tables": "ネストされたテーブルにフィールド名を表示", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "グループ化されているフィールドがない場合、計算は効果がありません。" }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "セルの値:", "label-column": "列", "label-empty-value": "空の値", "label-row": "行", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "ブール値のfalse値", @@ -11904,6 +12601,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "自動", "placeholder-default": "デフォルト:30", "placeholder-none": "なし" @@ -11912,7 +12615,16 @@ "label-as-percentile": "パーセンタイルとして", "tooltip-transform-index-percentile": "行インデックスをパーセンタイルに変換します。" }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "入力にラベルが含まれていません", "no-input-or-labels-found": "入力(またはラベル)が見つかりません", @@ -11934,10 +12646,30 @@ "label-labels": "ラベル", "label-mode": "モード", "label-value-field-name": "値フィールド名", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(任意)ラベルを選択", "tooltip-replace-value-field-label": "値フィールド名をラベルに置き換える" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "値または変数" }, "long-info": { @@ -11948,7 +12680,13 @@ "string-values-separate": "文字列値はラベルではなく個別のフィールドとして表示されます" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "単一フレームに適用した場合、マージは効果がありません。" + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "単一フレームに適用した場合、マージは効果がありません。", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "すべての値は数値です", @@ -11958,10 +12696,16 @@ "time-in-ascending-order": "時間は昇順です" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "自動", "field-order-manual": "", - "first-frame-warning": "フィールドの整理は単一フレームでのみ機能します。最初に変換の結合を適用するか、入力をフィルタリングすることを検討してください。" + "first-frame-warning": "フィールドの整理は単一フレームでのみ機能します。最初に変換の結合を適用するか、入力をフィルタリングすることを検討してください。", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "選択したフィールドの名前の表示方法を設定します。フレーム名は通常、表形式データに適しています" @@ -11986,9 +12730,41 @@ "partition-values-works-single-frame": "値によるパーティション分割は、単一フレームでのみ機能します。", "tooltip-keeps-partition-fields-frames": "フレーム内のパーティションフィールドを保持します" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "形式", - "label-info": "情報" + "label-info": "情報", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "と", @@ -12000,6 +12776,9 @@ "label-operation": "操作" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "計算", "label-include-time": "時間を含める", "label-labels-to-fields": "ラベルをフィールドに", @@ -12014,12 +12793,23 @@ "series-to-rows": "系列を行に変換" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "統計を選択" }, "regex-matcher-editor": { "placeholder-value-or-variable": "値または変数", "variable-regex-documentation": "複数値変数を正規表現文字列にフォーマットします" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "3次", @@ -12041,6 +12831,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "予測するX、Yポイント数" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "一致", "label-replace": "置換", @@ -12049,11 +12847,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "フィールド", - "label-mode": "モード" + "label-mode": "モード", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "フィールド", "label-reverse": "反転", + "name": { + "sort-by": "" + }, "placeholder-select-field": "フィールドを選択" }, "supplier": { @@ -12073,6 +12897,17 @@ "transform": "変換" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "統計", "label-time-field": "時間フィールド", @@ -12086,10 +12921,34 @@ "placeholder-value": "値", "tooltip-name-for-value-fields": "値フィールドの名前" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "操作", "placeholder-field": "フィールド" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "任意のタイプの複数の値フィールド", "single-frame": "単一フレーム", @@ -12142,6 +13001,7 @@ "title": "組織" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "ユーザー選択ツール", "select-placeholder": "ユーザーを検索するには入力を開始してください" }, @@ -12227,6 +13087,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "変数を削除" }, "create-ad-hoc-variable-adapter": { @@ -12275,9 +13137,34 @@ "label-refresh": "更新" }, "query-variable-sort-select": { - "description-values-variable": "この変数の値を並べ替える方法" + "description-values-variable": "この変数の値を並べ替える方法", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "デフォルト値(ある場合)", "text-options": "テキストオプション" }, @@ -12306,6 +13193,8 @@ "description-optional-display-name": "表示名(任意)", "description-template-variable-characters": "テンプレート変数の名前(最大50文字)。", "general": "一般", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "説明用のテキスト", "placeholder-label-name": "ラベル名", "placeholder-variable-name": "変数名", @@ -12320,9 +13209,15 @@ "tooltip-duplicate-variable": "変数を複製", "tooltip-remove-variable": "変数を削除" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "すべての値を切り替え" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "使用状況を表示" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 579cbc69bb3..6780f3520b6 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "일부 기능은 안정적이며(GA 단계) 기본적으로 활성화되어 있지만, 일부 기능은 현재 예비 베타 단계에 있으며 조기 도입이 가능하도록 제공하고 있습니다.", "confirm-modal-body-2": "변경 사항을 적용하기 전에 각 기능의 변경이 어떤 영향을 미칠지 파악하는 것이 좋습니다.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "베타", "content-general-availability": "정식 제공", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "조직 삭제", + "confirmText-delete": "", "title-delete": "삭제" }, "anon-users": { "not-found": "익명의 사용자를 찾을 수 없습니다." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "모든 장치에서 강제 로그아웃" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "이 조직의 사용자를 볼 수 있는 권한이 없습니다. 이 조직을 업데이트하려면 서버 관리자에게 문의하세요.", "heading": "조직 편집", @@ -208,9 +216,11 @@ "not-editable": "이 사용자의 역할은 인증 제공자에서 동기화되었기 때문에 편집할 수 없습니다. 자세한 내용은 <1>Grafana 인증 문서를 참고하세요." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "역할" }, + "confirmText-delete": "", "delete-aria-label": "사용자 삭제: {{name}}", "title-delete": "삭제" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "이러한 시스템 설정은 grafana.ini 또는 custom.ini에 정의되어 있습니다(또는 ENV 변수에서 덮어쓰기됩니다). 현재 이를 변경하려면 Grafana를 다시 시작해야 합니다." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Enterprise 라이선스" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "변경", + "confirmText-change": "", "grafana-admin-key": "Grafana 관리자", "grafana-admin-no": "아니요", "grafana-admin-yes": "네", "title": "권한" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "사용자 삭제", "disable-button": "사용자 비활성화", "edit-button": "편집", @@ -312,6 +330,9 @@ "title-delete-user": "사용자 삭제", "title-disable-user": "사용자 비활성화" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "브라우저 및 OS", "force-logout-all-button": "모든 장치에서 강제 로그아웃", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "활성 타이밍" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "더 추가하기" }, @@ -453,6 +478,9 @@ "label-muting-grouping-and-timings-optional": "알림 비활성화, 그룹화 및 타이밍(선택 사항)", "title-muting-grouping-and-timings": "알림 비활성화, 그룹화 및 타이밍" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "링크 복사", "duplicate": "복제", @@ -476,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML 편집", @@ -542,6 +573,7 @@ "view-configuration": "구성 보기" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "내부 Grafana Alertmanager 구성은 수동으로 변경할 수 없습니다. 이 구성을 변경하려면 UI를 통해 개별 리소스를 편집하세요.", "gma-manual-configuration-is-not-supported": "수동 구성 변경은 지원되지 않음", "message": { @@ -556,11 +588,13 @@ "title-resetting-alertmanager-configuration": "Alertmanager 구성 재설정 중" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "비교", "restore": "복구", "text-latest": "최근" }, + "confirmText-yes-restore-configuration": "", "loading": "로딩 중...", "no-previous-configurations": "이전 구성 없음", "this-might-take-a-while": "다소 시간이 걸릴 수 있습니다...", @@ -840,8 +874,10 @@ }, "contact-point-header": { "aria-label-more-actions": "'{{contactPointName}}' 연락처에 대한 추가 동작", + "ariaLabel-delete": "", "button-edit": "편집", "button-view": "보기", + "export-ariaLabel-export": "", "export-label-export": "내보내기", "label-delete": "삭제", "label-manage-permissions": "권한 관리", @@ -1374,6 +1410,7 @@ "label-disable-resolved-message": "해제 메시지 비활성화" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "양의 정수여야 합니다.", "must-enter-a-group-name": "그룹 이름을 입력해야 합니다." @@ -1465,15 +1502,12 @@ "group-details": { "ds-features-error": "데이터 소스 세부 정보 로딩 중 오류 발생", "edit": "편집", - "evaluations-to-fire": "경고 발생 평가 주기", "export": "내보내기", "folder": "폴더", "group-loading-error": "그룹 로딩 중 오류 발생", "interval": "간격", "namespace": "네임스페이스", - "pending-period": "보류 기간", - "recording": "기록", - "rule-name": "규칙 이름" + "new": "" }, "group-edit": { "ds-error": "데이터 소스 세부 정보 로딩 중 오류 발생", @@ -1519,8 +1553,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "시작하기", "label-insights": "인사이트", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "경고" }, "import-to-gma": { @@ -1819,7 +1865,11 @@ "other-data-sources": "기타 데이터 소스" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "비활성화됨", @@ -2069,9 +2119,11 @@ "update-errors": { "conflict": "알림 정책 트리가 다른 사용자에 의해 업데이트되었습니다.", "error-code": "오류 메시지: '{{error}}'", - "fallback": "알림 정책을 업데이트하는 중에 문제가 발생했습니다.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "페이지를 새로고침한 후 다시 시도해 주세요.", - "title": "알림 정책 저장 중 오류 발생" + "title": "" }, "n-more-policies_other": "추가 정책 {{count}}개" }, @@ -2126,6 +2178,7 @@ "query-and-expressions-step": { "add-query": "쿼리 추가", "body-queries-expressions-configured": "경고할 쿼리 또는 표현식을 하나 이상 생성합니다", + "confirmText-deactivate": "", "expressions": "표현식", "loading-data-sources": "데이터 소스 로딩 중...", "manipulate-returned-queries-other-operations": "쿼리에서 반환된 데이터를 수학 및 기타 연산으로 조작합니다.", @@ -2175,6 +2228,9 @@ "label-export-all": "모두 내보내기" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "{{errorMessage}}(으)로 인해 쿼리 편집기를 로딩할 수 없습니다" }, @@ -2193,6 +2249,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "원본 규칙이 프로비저닝되어 UI에서 생성된 규칙에 사용할 수 없으므로 복사한 규칙에 대한 새 평가 그룹을 설정해야 합니다.", "body-not-provisioned": "새 규칙은 프로비저닝된 규칙으로 표시되지 <1>않습니다.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "프로비저닝된 경고 규칙 복사" }, "redirect-to-rule-viewer": { @@ -2389,8 +2446,6 @@ "title-inspect-alert-rule": "경고 규칙 검사" }, "rule-list": { - "cannot-find-rule-details-for": "UID {{uid}}에 대한 규칙 세부 정보를 찾을 수 없습니다", - "cannot-load-rule-details-for": "UID {{uid}}에 대한 규칙 세부 정보를 불러올 수 없습니다", "configure-datasource": "구성", "draft-new-rule": "새 규칙 초안 작성", "ds-error": { @@ -2585,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "원하는 데이터 소스를 찾을 수 없나요? 일부 데이터 소스는 경고용으로 지원되지 않습니다. 자세한 내용을 보려면 아이콘을 클릭하세요." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "새로운 Alertmanager 추가", "builtin-alertmanager": "내장 Alertmanager", @@ -2737,6 +2797,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "알림 템플릿 선택", "loading": "로딩 중...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "알림 템플릿 선택" } @@ -2763,6 +2826,8 @@ }, "templates-table": { "actions": "작업", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "정의된 템플릿이 없습니다.", "template-group": "템플릿 그룹", "title-delete-template-group": "템플릿 그룹 삭제" @@ -2815,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}} 전)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "대상 폴더가 비어 있지 않습니다. 일부 규칙이 덮어쓰이거나 제거될 수 있습니다. 정말 이 경고 규칙을 Grafana 관리 규칙으로 가져오시겠어요?", @@ -2890,6 +2960,11 @@ "title-delete-contact-point": "연락처 삭제" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "알림 정책 삭제" @@ -3046,7 +3121,8 @@ "annotation-field-mapper": { "annotation": "주석", "first-value": "첫 번째 값", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "주석 쿼리 추가", @@ -3054,6 +3130,16 @@ "info-box-content-2": "자세한 내용은 <2>주석 문서를 확인하세요.", "title": "아직 추가된 사용자 정의 주석 쿼리 없음" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}}개의 이벤트({{numFields}}개의 필드에서 발견)", "no-events-found": "이벤트를 찾을 수 없습니다", @@ -3129,6 +3215,9 @@ "define-allowed-teams-ids-label": "허용된 팀 ID 정의", "display-name-description": "로그인 페이지에 '다음으로 로그인'으로 표시됩니다. 두 개 이상의 ID 제공자 또는 SSO 프로토콜을 사용하는 경우 유용합니다.", "display-name-label": "표시 이름", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "OAuth2 ID 토큰의 속성 맵 내에서 사용자 이메일 조회에 사용할 키의 이름입니다.", "email-attribute-name-label": "이메일 속성 이름", "email-attribute-path-description": "사용자 정보에서 사용자 이메일 조회에 사용할 JMESPath 표현식입니다.", @@ -3180,7 +3269,7 @@ "team-ids-github": "팀 ID의 정수 목록입니다.", "team-ids-label": "팀 ID", "team-ids-numbers": "팀 ID는 숫자여야 합니다.", - "team-ids-other": "팀 ID의 문자열 목록입니다.", + "team-ids-other": "", "team-ids-placeholder": "팀 ID를 입력하고 엔터 키를 눌러 추가", "teams-url-description": "팀 ID에 대한 쿼리에 사용되는 URL입니다. 설정하지 않으면 기본값은 /teams입니다.", "teams-url-description-oauth": "\"{{ teamsURLLabel }}\"을(를) 구성하는 경우, \"{{ teamIDsAttributePathLabel }}\"도 구성해야 합니다.", @@ -3224,6 +3313,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "기본값으로 재설정" }, + "confirmText-reset": "", "disable": "비활성화", "disabling": "비활성화 중...", "discard": "무시", @@ -3455,11 +3545,11 @@ "button-create": "생성", "button-creating": "생성 중...", "cancel": "취소", - "error-invalid-folder-name": "유효하지 않은 폴더 이름", "error-required": "폴더 이름은 필수 입력 항목입니다", "folder-name-input-placeholder-enter-folder-name": "폴더 이름 입력", "label-folder-name": "폴더 이름", "text-pull-request-created": "이 폴더에 대한 변경 사항을 포함하는 풀 요청이 생성되었습니다.", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "풀 요청 생성됨", "title-this-repository-is-read-only": "이 리포지토리는 읽기 전용입니다." }, @@ -3877,6 +3967,7 @@ "tooltip": "닫기" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "옵션을 불러오는 중에 오류가 발생했습니다." @@ -4145,8 +4236,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "시간 범위를 지정된 수로 나누어 동적으로 간격을 계산합니다." + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4308,12 +4399,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "외부 사이트로 이동하시겠어요?" } }, "render-left-actions": { - "text-public": "공개", "tooltip-view-as-scene": "씬으로 보기" } }, @@ -4506,6 +4599,14 @@ "errors": { "failed-to-load": "대시보드 로딩에 실패했습니다." }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "대시보드 가져오기 실패" @@ -4522,6 +4623,13 @@ "editable": "편집 가능", "readonly": "읽기 전용" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4571,6 +4679,18 @@ "transform-data": "데이터 변환" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4825,6 +4945,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "정말 대시보드를 {{version}} 버전으로 복원하시겠어요? 저장하지 않은 변경 사항은 모두 손실됩니다.", + "confirmText-restore-version": "", "title-restore-version": "버전 복구" }, "row-options-button": { @@ -4875,6 +4996,9 @@ "title-not-unique": "이 제목은 고유하지 않습니다" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "다음 이름으로 저장" }, @@ -4909,6 +5033,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "선택한 폴더에 동일한 이름의 대시보드가 이미 존재합니다.<1><2>그래도 이 대시보드를 저장하시겠어요?", "body-version-mismatch": "다른 사람이 이 대시보드를 업데이트했습니다<1><2>그래도 이 대시보드를 저장하시겠어요?", + "confirmText-save-and-overwrite": "", "title-name-exists": "충돌", "title-version-mismatch": "충돌" }, @@ -5074,7 +5199,6 @@ "playlist-next": "다음 대시보드로 이동", "playlist-previous": "이전 대시보드로 이동", "playlist-stop": "플레이리스트 중지", - "public-dashboard": "공개", "refresh": "대시보드 새로 고침", "save": "대시보드 저장", "save-dashboard": { @@ -5105,7 +5229,9 @@ "label-apply-transformation-to": "변환 적용 대상" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "디버그", "title-disable-transformation": "변환 비활성화", "title-filter": "필터", @@ -5125,12 +5251,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "변환 검색", "show-images": "이미지 표시", - "title-add-another-transformation": "다른 변환 추가" + "title-add-another-transformation": "다른 변환 추가", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "다른 변환 추가" }, + "confirmText-delete-all": "", "delete-all-transformations": "모든 변환 삭제", "title-delete-all-transformations": "모든 변환을 삭제하시겠어요?", "tooltip-clear-search": "검색 초기화", @@ -5167,6 +5298,7 @@ "version-history-table": { "aria-label-toggle-selection": "{{version}} 버전 선택 토글", "date": "날짜", + "name-latest": "", "notes": "메모", "restore": "복구", "updated-by": "업데이트한 사용자", @@ -5243,7 +5375,8 @@ "description-enables-users-custom-values": "사용자가 목록에 사용자 지정 값을 추가할 수 있습니다", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "차원에 대한 다음 정보를 CSV로 제공: {{name}}, {{value}}", "label-data-source": "데이터 소스", - "label-use-static-key-dimensions": "고정 키 차원 사용" + "label-use-static-key-dimensions": "고정 키 차원 사용", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5316,6 +5449,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "공개 URL 철회" } @@ -5327,6 +5463,7 @@ }, "custom-variable-form": { "custom-options": "사용자 지정 옵션", + "name-values-separated-comma": "", "selection-options": "선택 옵션" }, "dashboard-edit-pane-renderer": { @@ -5345,6 +5482,12 @@ "label-type": "유형", "label-url": "URL", "label-with-tags": "태그 포함", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "대시보드 열기" }, "dashboard-link-list": { @@ -5357,15 +5500,9 @@ "type": "유형" }, "dashboard-preview-banner": { - "not-saved": "값이 아직 Grafana 데이터베이스에 저장되지 않았습니다.", "not-yet-saved": "값이 Grafana 데이터베이스에 저장되지 않았습니다.", - "open-pull-request-in-git-hub": "GitHub에서 풀 요청 열기", - "title-dashboard-loaded-branch-git-hub": "이 대시보드는 GitHub의 브랜치에서 로딩됩니다.", "title-dashboard-loaded-external-repository": "이 대시보드는 외부 리포지토리에서 로딩됩니다.", - "title-dashboard-loaded-request-git-hub": "이 대시보드는 GitHub의 풀 요청에서 로딩됩니다.", - "title-error-loading-dashboard": "대시보드 로딩 중 오류 발생", - "value-not-saved": "값이 아직 Grafana 데이터베이스에 저장되지 않았습니다.", - "view-pull-request-in-git-hub": "GitHub에서 풀 요청 보기" + "title-error-loading-dashboard": "대시보드 로딩 중 오류 발생" }, "dashboard-scene": { "text": { @@ -5391,6 +5528,8 @@ "data-source-options": "데이터 소스 옵션", "description-instance-name-filter": "변수 값 목록에서 어떤 데이터 소스 인스턴스를 선택할지에 대한 정규 표현식 필터입니다. 모두 비워 둡니다.", "example-instance-name-filter": "예시: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "선택 옵션" }, "default-grid-layout-manager": { @@ -5436,12 +5575,61 @@ "empty-transformations-message": { "add-transformation": "변환 추가" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "열 옵션", "row-height-options": "열 높이 옵션" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5466,7 +5654,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "차원에 대한 다음 정보를 CSV로 제공: {{name}}, {{value}}", "group-by-options": "옵션을 기준으로 그룹화", "label-data-source": "데이터 소스", - "label-use-static-group-by-dimensions": "고정 그룹 차원 사용" + "label-use-static-group-by-dimensions": "고정 그룹 차원 사용", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "클립보드로 복사", @@ -5502,9 +5691,14 @@ "apply": "적용" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "계산된 값은 이 임계값 아래로 내려가지 않습니다", "description-step-count": "값을 계산하기 위해 현재 시간 범위를 나눌 횟수", - "interval-options": "간격 옵션" + "interval-options": "간격 옵션", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5523,11 +5717,28 @@ "last-edited": " 님({{timeAgo}}에)", "usage-count_other": "{{count}}개의 대시보드에서 사용됨" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "선택한 폴더에 동일한 이름의 대시보드가 이미 존재합니다. 그래도 이 대시보드를 저장하시겠어요?", "title-name-already-exists": "이미 존재하는 이름입니다" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "외부 사이트로 이동하시겠어요?" } @@ -5563,6 +5774,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "다른 변환 추가", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "모든 변환 삭제", "title-delete-all-transformations": "모든 변환을 삭제하시겠어요?" }, @@ -5615,9 +5828,13 @@ "description-examples": "이름을 지정한 캡처 그룹을 사용하여 표시 텍스트와 값을 분리할 수 있습니다(<1>예시 참고).", "description-optional": "선택 사항, 시리즈 이름 또는 메트릭 노드 세그먼트의 일부를 추출하려는 경우.", "label-data-source": "데이터 소스", + "label-static-options-sort": "", "label-target-data-source": "대상 데이터 소스", + "label-use-static-options": "", + "name-regex": "", "query-options": "쿼리 옵션", - "selection-options": "선택 옵션" + "selection-options": "선택 옵션", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5630,6 +5847,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "정말 대시보드를 {{version}} 버전으로 복원하시겠어요? 저장하지 않은 변경 사항은 모두 손실됩니다.", + "confirmText-restore-version": "", "title-restore-version": "버전 복구" }, "save-button": { @@ -5647,7 +5865,8 @@ "label-folder": "폴더", "render-footer": { "title-failed-to-save-dashboard": "대시보드 저장 실패" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5722,7 +5941,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "여러 값을 동시에 선택할 수 있도록 합니다", "description-enables-option-include-variables": "모든 값을 포함하는 옵션 활성화", - "description-enables-users-custom-values": "사용자가 목록에 사용자 지정 값을 추가할 수 있습니다" + "description-enables-users-custom-values": "사용자가 목록에 사용자 지정 값을 추가할 수 있습니다", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "공유 메뉴 토글" @@ -5742,6 +5965,9 @@ "copy-to-clipboard-failed": "클립보드로 복사 실패" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(선택 사항)", "text-options": "텍스트 옵션" @@ -5765,6 +5991,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "정말 이 패널의 연결을 해제하시겠어요?" }, "unsaved-changes-modal": { @@ -5781,6 +6009,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "이 변수는 어떤 변수나 대시보드에서도 참조되지 않습니다.", "aria-label-variable-referenced-other-variables-dashboard": "이 변수는 다른 변수 또는 대시보드에서 참조됩니다.", @@ -5790,10 +6021,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "변수 편집기 양식", "back-to-list": "목록으로 돌아가기", + "confirmText": { + "delete-variable": "" + }, "delete": "삭제", "description-optional-display-name": "표시 이름(선택 사항)", "description-template-variable-characters": "템플릿 변수의 이름입니다. (최대 50자)", "general": "일반", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "설명 텍스트", "placeholder-label-name": "라벨 이름", "placeholder-variable-name": "변수 이름", @@ -5808,13 +6045,25 @@ "variable": "변수" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "변수 삭제", "tooltip-duplicate-variable": "변수 복제", "tooltip-remove-variable": "변수 제거" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "숨기기" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "다음 변수의 사용처 표시 중: {{variableId}}", "tooltip-show-usages": "사용처 표시" @@ -5841,6 +6090,7 @@ "version-history-table": { "aria-label-toggle-selection": "{{version}} 버전 선택 토글", "date": "날짜", + "name-latest": "", "notes": "메모", "restore": "복구", "updated-by": "업데이트한 사용자", @@ -6228,7 +6478,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "업로드" @@ -6266,6 +6517,7 @@ }, "label-limit": "제한", "label-value": "값", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6274,9 +6526,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "최대", "label-min": "최소", - "label-value": "값" + "label-value": "값", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6848,6 +7106,8 @@ "aria-label-select-service-name-operator": "서비스 이름 연산자 선택", "aria-label-select-span-name": "스팬 이름 선택", "aria-label-select-span-name-operator": "스팬 이름 연산자 선택", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "스팬 필터", "label-duration": "지속 시간", "label-service-name": "서비스 이름", @@ -6918,6 +7178,8 @@ "split-widen": "창 확대" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "피드백 제출하기", "label-copied": "복사됨!", "label-export": "내보내기", @@ -7025,6 +7287,7 @@ "when": "다음과 같은 때" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "연산" }, "math": { @@ -7055,6 +7318,7 @@ }, "folder-filter": { "clear-folder-button": "폴더 비우기", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "폴더 필터", "select-placeholder": "폴더별로 필터링" }, @@ -7123,7 +7387,53 @@ "incomplete-request-error": "죄송합니다. 요청을 완료할 수 없었습니다. 다시 시도해 주세요.", "send-custom-feedback": "전송" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "위도", "label-longitude": "경도" @@ -7132,6 +7442,14 @@ "center": "중앙:", "zoom": "확대/축소:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "레이어" @@ -7154,6 +7472,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "지오맵(GeoMap) 스타일 규칙 추가하기" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "레이어 추가", "no-layers": "레이어가 없나요?" @@ -7164,16 +7490,38 @@ "label-zoom": "확대/축소", "use-current-map-settings": "현재 지도 설정 사용" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "기호" }, "measure-overlay": { "tooltip-show-measure-tools": "측정 도구 표시" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "베이스맵 레이어는 서버 관리자가 구성합니다." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "정렬", "label-baseline": "베이스라인", "label-color": "색상", @@ -7187,7 +7535,14 @@ "label-symbol-vertical-align": "기호 세로 정렬", "label-text-label": "텍스트 레이블", "label-x-offset": "X축 상쇄", - "label-y-offset": "Y축 상쇄" + "label-y-offset": "Y축 상쇄", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "비교 연산자", @@ -7198,6 +7553,15 @@ "placeholder-feature-property": "기능 속성", "placeholder-numeric-value": "숫자 값", "placeholder-value": "값" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7447,7 +7811,8 @@ "aria-label-selected-color": "{{colorLabel}} 색상" }, "confirm-button": { - "cancel": "취소" + "cancel": "취소", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "확인하려면 '{{confirmPromptText}}'을(를) 입력하세요." @@ -7629,6 +7994,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "패널 접기 토글", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "쿼리 취소", "tooltip-cancel-loading": "쿼리 취소", "tooltip-stop-streaming": "스트리밍 중지", @@ -7695,6 +8062,9 @@ "filter-popup-match-case": "대소문자 일치", "inspect-drawer-title": "값 검사", "inspect-menu-label": "값 검사", + "nested-table": { + "no-data": "" + }, "no-values-label": "값 없음", "pagination-summary": "{{numRows}}개의 행 중에서 {{itemsRangeStart}}~{{displayedEnd}}" }, @@ -7703,6 +8073,7 @@ }, "tags-input": { "add": "추가", + "placeholder-new-tag": "", "remove": "태그 제거: {{name}}" }, "time-sync-button": { @@ -7796,6 +8167,12 @@ "footer-click-to-action": "클릭하여 {{actionTitle}} 작업 수행", "footer-click-to-navigate": "클릭하여 {{linkTitle}} 열기", "timestamp": "시간 기록" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8172,6 +8549,10 @@ "add-library-panel-modal": { "title-create-library-panel": "라이브러리 패널 생성" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "이 패널을 삭제하시겠어요?" }, @@ -8194,7 +8575,7 @@ "usage-count_other": "{{count}}개의 대시보드에서 사용됨" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "이름 또는 설명으로 검색" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "라이브러리 패널 로딩 중..." @@ -8242,9 +8623,7 @@ "share-panel": { "config-description": "다음 사용자 지정 설정을 사용하여 조직 내에서 패널을 공유할 수 있는 개인화된 직접 링크를 생성합니다.", "download-image": "이미지 다운로드", - "render-image": "이미지 생성", - "render-image-error": "패널 이미지 렌더링 실패", - "render-image-error-description": "이미지를 생성하던 중 오류 발생" + "render-image": "이미지 생성" } }, "live": { @@ -8330,6 +8709,15 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8353,7 +8741,6 @@ "label-wrap-lines": "줄 바꿈" }, "log-details": { - "close": "로그 세부 정보 닫기", "fields": "필드", "links": "링크", "log-line": "로그 라인", @@ -8379,6 +8766,42 @@ "show-more": "더 보기", "tooltip-error": "오류: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "링크를 로그 라인에 복사", "copy-log": "로그 라인 복사", @@ -8479,6 +8902,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8505,6 +8933,7 @@ "un-themed-log-details": { "aria-label-data-links": "데이터 링크", "aria-label-fields": "필드", + "aria-label-line": "", "aria-label-log-level": "로그 수준", "aria-label-no-details": "세부 정보 없음" }, @@ -8572,6 +9001,8 @@ "updated-on": "업데이트 날짜" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "삭제" }, "unthemed-dashboard-import": { @@ -8581,8 +9012,17 @@ "text": { "import-dashboard": "대시보드 가져오기" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "이 도구를 사용하면 현재 설치 인스턴스에서 클라우드 스택으로 일부 리소스를 마이그레이션할 수 있습니다. 시작하려면 이 설치 인스턴스에 대한 스냅샷을 생성해야 합니다. 스냅샷을 생성하는 데는 일반적으로 2분이 채 걸리지 않습니다. 현재 Grafana 설치 인스턴스와 함께 해당 스냅샷이 저장됩니다.", @@ -8731,6 +9171,8 @@ "resource-details": { "dismiss-button": "확인", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "대시보드가 이미 클라우드 인스턴스에서 Grafana에 의해 프로비저닝 및 관리되고 있습니다. 앞으로 프로비저닝된 대시보드를 사용하는 것이 좋습니다. 대시보드를 클라우드 인스턴스에 복사하려는 경우, 대시보드 JSON에서 대시보드 ID를 변경하고 새 스냅샷을 저장한 다음 다시 업로드하세요.", "datasource-already-managed": "데이터 소스가 이미 클라우드 인스턴스에서 Grafana에 의해 프로비저닝 및 관리되고 있습니다. 다른 리소스인 경우 다른 UID를 설정하고 다시 시도하세요.", "datasource-invalid-url": "잘못된 URL을 가진 데이터 소스가 있습니다. 유효한 URL을 입력하고 다시 시도해 주세요.", @@ -9418,6 +9860,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "조직 선택" }, "page": { @@ -9640,6 +10083,7 @@ "permission": "이 페이지를 볼 수 있는 권한이 없습니다.", "title-access-denied": "액세스 거부됨" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "루트 앱 페이지 구성 요소를 찾을 수 없습니다" }, "browse": { @@ -9683,8 +10127,7 @@ "update-status-text": "플러그인 업데이트 완료" }, "versions": { - "confirmation-text-1": "정말 버전으로 다운그레이드하시겠어요?", - "confirmation-text-2": "권장하지 않는 작업입니다.", + "confirmation-text": "", "downgrade-confirm": "다운그레이드", "downgrade-title": "플러그인 버전 다운그레이드" } @@ -9738,6 +10181,10 @@ "empty-state": { "message": "찾은 플러그인 없음" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "확인" @@ -9771,7 +10218,11 @@ "updating": "업데이트 중" }, "install-controls-button": { - "title-uninstall-modal": "{{plugin}} 제거" + "title-uninstall-modal": "{{plugin}} 제거", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "이 플러그인은 <2>grafana.com/plugins에 게시되어 있지 않으며 카탈로그를 통해 관리할 수 없습니다.", @@ -9942,6 +10393,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "이 기능은 현재 적극적으로 개발 중입니다. 최상의 경험과 최신 개선 사항 적용을 위해 Grafana의 <2>야간 빌드를 사용하는 것이 좋습니다." @@ -10802,6 +11262,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "서비스 계정 선택기", "select-placeholder": "입력하여 서비스 계정 검색" }, @@ -10847,6 +11308,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "서비스 계정 토큰 추가", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "서비스 계정 삭제", "disable-service-account": "서비스 계정 비활성화", "enable-service-account": "서비스 계정 활성화", @@ -10873,6 +11338,7 @@ "used-by": "사용자" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "편집" }, "service-account-role-row": { @@ -10886,10 +11352,15 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "서비스 계정 추가", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "이름으로 서비스 계정 검색", "sub-title": "서비스 계정과 해당 계정의 토큰을 사용하여 Grafana API에 대한 인증을 수행할 수 있습니다. 자세한 내용은 Grafana의 <2>문서", "title-delete-service-account": "서비스 계정 삭제", - "title-disable-service-account": "서비스 계정 비활성화" + "title-disable-service-account": "서비스 계정 비활성화", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "이 토큰은 만료되었습니다", @@ -10903,6 +11374,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "JSON으로 내보내기", "share-externally-title": "외부 공유", "share-internally-title": "내부 공유", @@ -10938,6 +11410,19 @@ "view-button": "JSON 보기", "view-button-yaml": "YAML 보기" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "라이브러리 패널을 생성합니다." }, @@ -10946,7 +11431,7 @@ "info-text": "아래 옵션으로 사용자 정의된 이 대시보드 또는 패널에 대한 직접 링크를 생성합니다.", "link-url": "링크 URL", "render-alert": "이미지 렌더러 플러그인 미설치", - "render-instructions": "패널 이미지를 렌더링하려면 <2>Grafana 이미지 렌더러 플러그인을 설치해야 합니다. 플러그인을 설치하려면 Grafana 관리자에게 문의하세요.", + "render-instructions": "", "rendered-image": "렌더링된 이미지에 직접 연결", "save-alert": "대시보드 저장되지 않음", "save-dashboard": "패널 이미지를 렌더링하려면 먼저 대시보드를 저장해야 합니다.", @@ -11014,6 +11499,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "패널 미리 보기" }, @@ -11353,6 +11844,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "이미지를 표시할 수 없거나 화면 리더를 이용하는 사용자를 위해 표시되는 대체 텍스트", "description-title-text": "이미지에 커서를 가져갔을 때 표시되는 텍스트", @@ -11373,6 +11865,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11423,6 +11916,7 @@ "tag-option-label": "태그 옵션" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "팀 선택기", "select-placeholder": "팀 선택" }, @@ -11724,6 +12218,18 @@ "placeholder-fields-or-number": "필드 또는 숫자" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "별칭", "label-mode": "모드", "label-replace-all-fields": "모든 필드 교체" @@ -11738,6 +12244,13 @@ "concatenate-transformer-editor": { "label-label": "라벨", "label-name": "이름", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "프레임" }, "config-from-query-transformer-editor": { @@ -11748,6 +12261,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "변환 필드 유형 변환기 추가", "aria-label-remove-convert-field-type-transformer": "변환 필드 유형 변환기 제거", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "브라우저", "utc": "UTC" @@ -11758,7 +12275,13 @@ "label-input-format": "입력 형식", "label-join-with": "조인 대상", "label-set-timezone": "시간대 설정", + "name": { + "convert-field-type": "" + }, "placeholder-type": "유형", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "배열 값을 조인할 때 명시적인 구분 기호를 사용합니다", "tooltip-input-format": "Grafana가 날짜 문자열을 올바르게 분석할 수 있도록 입력 필드의 형식을 지정하세요.", "tooltip-specify-the-output-format": "출력 형식을 지정하세요.", @@ -11780,6 +12303,19 @@ "desc": "", "off": "끄기" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "열거형 값 추가", "generate-enum-values-from-data": "데이터에서 열거형 값 생성" @@ -11790,11 +12326,22 @@ "remove-enum-row-tooltip-delete": "삭제" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "구분 기호", "label-format": "형식", "label-keep-time": "시간 유지", "label-replace-all-fields": "모든 필드 교체", "label-source": "소스", + "name": { + "extract-fields": "" + }, "placeholder-auto": "자동", "placeholder-select-delimiter": "구분 기호 선택...", "tooltip-regexp": "예: {{regexExample}}" @@ -11803,8 +12350,22 @@ "aria-label-threshold-color": "임계값 색상" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "필드", - "label-lookup": "조회" + "label-lookup": "조회", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "추가 설정", @@ -11813,12 +12374,26 @@ "use-as": "다음으로 사용" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "변수에서", "label-identifier": "식별자", "label-use-variable": "변수 사용", "label-variable": "변수", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "정규 표현식 패턴" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "필드", "label-match": "일치", @@ -11828,14 +12403,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "조건 추가", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "조건", - "label-filter-type": "필터 유형" + "label-filter-type": "필터 유형", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "필드", "label-format": "형식", "label-substring-range": "부분 문자열 범위" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "형식", "label-set-timezone": "시간대 설정", @@ -11843,6 +12452,70 @@ "tooltip-format": "<2>Moment.js 형식 문자열로 지정된 필드의 출력 형식입니다.", "tooltip-timezone-manually": "날짜의 시간대를 수동으로 설정합니다" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "브라우저", @@ -11870,22 +12543,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "무시됨", "placeholder-select-stats": "통계 선택" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "그룹화된 필드가 없는 경우 계산이 영향을 미치지 않습니다" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "활성화하면 중첩 테이블의 필드 이름이 테이블 헤더에 표시됩니다", "label-show-field-names-in-nested-tables": "중첩된 테이블에 필드 이름 표시", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "그룹화된 필드가 없는 경우 계산이 영향을 미치지 않습니다." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "셀 값", "label-column": "열", "label-empty-value": "빈 값", "label-row": "행", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "부울 거짓값", @@ -11904,6 +12601,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "자동", "placeholder-default": "기본값: 30", "placeholder-none": "없음" @@ -11912,7 +12615,16 @@ "label-as-percentile": "백분위수로", "tooltip-transform-index-percentile": "행 인덱스를 백분위수로 변환합니다." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "입력에 라벨이 포함되어 있지 않습니다", "no-input-or-labels-found": "입력(또는 라벨)을 찾을 수 없습니다", @@ -11934,10 +12646,30 @@ "label-labels": "라벨", "label-mode": "모드", "label-value-field-name": "값 필드 이름", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "라벨 선택(선택 사항)", "tooltip-replace-value-field-label": "값 필드 이름을 레이블로 교체합니다" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "값 또는 변수" }, "long-info": { @@ -11948,7 +12680,13 @@ "string-values-separate": "문자열 값은 라벨이 아닌 별도의 필드로 표시됩니다" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "단일 프레임에 병합을 적용하면 아무런 효과가 없습니다." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "단일 프레임에 병합을 적용하면 아무런 효과가 없습니다.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "모든 값은 숫자입니다", @@ -11958,10 +12696,16 @@ "time-in-ascending-order": "시간(오름차순)" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "자동", "field-order-manual": "", - "first-frame-warning": "필드 정리는 단일 프레임에서만 가능합니다. 먼저 조인 변환을 적용하거나 입력을 필터링하는 것이 좋습니다." + "first-frame-warning": "필드 정리는 단일 프레임에서만 가능합니다. 먼저 조인 변환을 적용하거나 입력을 필터링하는 것이 좋습니다.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "선택한 필드의 이름을 표시하는 방법을 설정합니다. 표 형식 데이터에는 프레임 이름을 사용하는 것이 일반적으로 더 좋습니다" @@ -11986,9 +12730,41 @@ "partition-values-works-single-frame": "값을 기준으로 분할하는 것은 단일 프레임에서만 가능합니다.", "tooltip-keeps-partition-fields-frames": "프레임의 분할 필드 유지" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "형식", - "label-info": "정보" + "label-info": "정보", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "및", @@ -12000,6 +12776,9 @@ "label-operation": "연산" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "계산", "label-include-time": "시간 포함", "label-labels-to-fields": "라벨을 필드로", @@ -12014,12 +12793,23 @@ "series-to-rows": "계열에서 행으로" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "통계 선택" }, "regex-matcher-editor": { "placeholder-value-or-variable": "값 또는 변수", "variable-regex-documentation": "다중 값 변수를 정규식 문자열로 형식 지정" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "입방", @@ -12041,6 +12831,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "예측할 X, Y 포인트 수" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "일치", "label-replace": "교체", @@ -12049,11 +12847,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "필드", - "label-mode": "모드" + "label-mode": "모드", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "필드", "label-reverse": "되돌리기", + "name": { + "sort-by": "" + }, "placeholder-select-field": "필드 선택" }, "supplier": { @@ -12073,6 +12897,17 @@ "transform": "변환" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "통계", "label-time-field": "시간 필드", @@ -12086,10 +12921,34 @@ "placeholder-value": "값", "tooltip-name-for-value-fields": "값 필드의 이름" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "연산", "placeholder-field": "필드" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "모든 유형의 여러 값 필드", "single-frame": "단일 프레임", @@ -12142,6 +13001,7 @@ "title": "조직" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "사용자 선택기", "select-placeholder": "입력하여 사용자 검색" }, @@ -12227,6 +13087,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "변수 삭제" }, "create-ad-hoc-variable-adapter": { @@ -12275,9 +13137,34 @@ "label-refresh": "새로 고침" }, "query-variable-sort-select": { - "description-values-variable": "이 변수의 값을 정렬하는 방법" + "description-values-variable": "이 변수의 값을 정렬하는 방법", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "기본값(있는 경우)", "text-options": "텍스트 옵션" }, @@ -12306,6 +13193,8 @@ "description-optional-display-name": "표시 이름(선택 사항)", "description-template-variable-characters": "템플릿 변수의 이름입니다. (최대 50자)", "general": "일반", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "설명 텍스트", "placeholder-label-name": "라벨 이름", "placeholder-variable-name": "변수 이름", @@ -12320,9 +13209,15 @@ "tooltip-duplicate-variable": "변수 복제", "tooltip-remove-variable": "변수 제거" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "모든 값 토글" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "사용처 표시" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index a29386de438..a7b651adde6 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Sommige functies zijn stabiel (GA) en standaard ingeschakeld, terwijl sommige zich momenteel in de voorlopige bètafase bevinden en beschikbaar zijn voor vroege adoptie.", "confirm-modal-body-2": "We raden aan om de implicaties van elke functiewijziging te begrijpen voordat je wijzigingen aanbrengt.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Bèta", "content-general-availability": "Algemene beschikbaarheid", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Organisatie verwijderen", + "confirmText-delete": "", "title-delete": "Verwijderen" }, "anon-users": { "not-found": "Geen anonieme gebruikers gevonden." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Uitloggen van alle apparaten forceren" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Je hebt geen toestemming om gebruikers in deze organisatie te zien. Neem contact op met je serverbeheerder om deze organisatie bij te werken.", "heading": "Organisatie bewerken", @@ -208,9 +216,11 @@ "not-editable": "De rol van deze gebruiker kan niet worden bewerkt omdat deze is gesynchroniseerd met je authenticatieprovider. Raadpleeg de <1>Grafana-authenticatiedocumenten voor meer informatie." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Rol" }, + "confirmText-delete": "", "delete-aria-label": "Gebruiker verwijderen: {{name}}", "title-delete": "Verwijderen" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Deze systeeminstellingen worden gedefinieerd in grafana.ini of custom.ini (of overschreven in ENV-variabelen). Om deze te wijzigen moet je Grafana momenteel opnieuw opstarten." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Enterprise-licentie" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Wijzigen", + "confirmText-change": "", "grafana-admin-key": "Grafana-beheerder", "grafana-admin-no": "Nee", "grafana-admin-yes": "Ja", "title": "Toestemmingen" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Gebruiker verwijderen", "disable-button": "Gebruiker uitschakelen", "edit-button": "Bewerken", @@ -312,6 +330,9 @@ "title-delete-user": "Gebruiker verwijderen", "title-disable-user": "Gebruiker uitschakelen" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Browser en besturingssysteem", "force-logout-all-button": "Uitloggen van alle apparaten forceren", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Actieve timings" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Meer toevoegen" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Dempen, groeperen en timings (optioneel)", "title-muting-grouping-and-timings": "Dempen, groeperen en timings" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Link kopiëren", "duplicate": "Dupliceren", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML bewerken", @@ -546,6 +577,7 @@ "view-configuration": "Configuratie bekijken" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "De interne Grafana Alertmanager-configuratie kan niet handmatig worden gewijzigd. Bewerk de individuele bronnen via de gebruikersinterface om deze configuratie te wijzigen.", "gma-manual-configuration-is-not-supported": "Handmatige configuratiewijzigingen worden niet ondersteund", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Configuratie van Alertmanager opnieuw instellen" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Vergelijken", "restore": "Herstellen", "text-latest": "Nieuwste" }, + "confirmText-yes-restore-configuration": "", "loading": "Bezig met laden ...", "no-previous-configurations": "Geen eerdere configuraties", "this-might-take-a-while": "Dit kan even duren...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Meer acties voor contactpunt '{{contactPointName}}'", + "ariaLabel-delete": "", "button-edit": "Bewerken", "button-view": "Weergave", + "export-ariaLabel-export": "", "export-label-export": "Exporteren", "label-delete": "Verwijderen", "label-manage-permissions": "Toestemmingen beheren", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Opgelost bericht uitschakelen" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Moet een positief geheel getal zijn.", "must-enter-a-group-name": "Een groepsnaam is vereist" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Er is een fout opgetreden bij het laden van de gegevensbrondetails", "edit": "Bewerken", - "evaluations-to-fire": "Evaluatiecycli tot activering", "export": "Exporteren", "folder": "Map", "group-loading-error": "Er is een fout opgetreden bij het laden van de groep", "interval": "Interval", "namespace": "Namespace", - "pending-period": "Wachttijd", - "recording": "Opnemen", - "rule-name": "Regelnaam" + "new": "" }, "group-edit": { "ds-error": "Er is een fout opgetreden bij het laden van de gegevensbrondetails", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Aan de slag", "label-insights": "Inzichten", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alerting" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Andere gegevensbronnen" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Uitgeschakeld", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "Een andere gebruiker heeft wijzigingen aangebracht in de structuur van het notificatiebeleid.", "error-code": "Foutmelding: '{{error}}'", - "fallback": "Er ging iets mis bij het bijwerken van je meldingsbeleid.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Vernieuw de pagina en probeer het opnieuw.", - "title": "Fout bij opslaan meldingsbeleid" + "title": "" }, "n-more-policies_one": "{{count}} aanvullend beleid", "n-more-policies_other": "{{count}} aanvullend beleid" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Query toevoegen", "body-queries-expressions-configured": "Maak ten minste één query of expressie om te worden gewaarschuwd", + "confirmText-deactivate": "", "expressions": "Expressie", "loading-data-sources": "Gegevensbronnen laden...", "manipulate-returned-queries-other-operations": "Manipuleer gegevens die worden geretourneerd uit query's met wiskundige en andere bewerkingen.", @@ -2184,6 +2237,9 @@ "label-export-all": "Alles exporteren" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Kon querybewerker niet laden vanwege: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Je moet een nieuwe evaluatiegroep instellen voor de gekopieerde regel omdat de oorspronkelijke is geprovisioneerd en niet kan worden gebruikt voor regels die in de gebruikersinterface zijn gemaakt.", "body-not-provisioned": "De nieuwe regel wordt <1>niet gemarkeerd als een ingestelde regel.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Provisioned waarschuwingsregel kopiëren" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Waarschuwingsregel inspecteren" }, "rule-list": { - "cannot-find-rule-details-for": "Kan regelgegevens niet vinden voor UID {{uid}}", - "cannot-load-rule-details-for": "Kan regelgegevens niet laden voor UID {{uid}}", "configure-datasource": "Configureren", "draft-new-rule": "Een nieuwe regel opstellen", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Kun je de gewenste gegevensbron niet vinden? Sommige gegevensbronnen worden niet ondersteund voor waarschuwingen. Klik op het pictogram voor meer informatie." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Nieuwe Alertmanager toevoegen", "builtin-alertmanager": "Ingebouwde Alertmanager", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Kies een meldingssjabloon", "loading": "Bezig met laden ...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Meldingssjabloon selecteren" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Acties", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Geen sjablonen gedefinieerd.", "template-group": "Sjabloongroep", "title-delete-template-group": "Sjabloongroep verwijderen" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} geleden)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "De doelmap is niet leeg, sommige regels kunnen worden overschreven of verwijderd. Weet je zeker dat je deze waarschuwingsregels wilt importeren naar door Grafana beheerde regels?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Contactpunt verwijderen" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Meldingsbeleid verwijderen" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Opmerking", "first-value": "Eerste waarde", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Annotatiequery toevoegen", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Bekijk de <2>Annotatiedocumentatie voor meer informatie.", "title": "Er zijn nog geen aangepaste annotatiequery's toegevoegd" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} gebeurtenissen (van {{numFields}} velden)", "no-events-found": "Geen gebeurtenissen gevonden", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Toegestane team-id's definiëren", "display-name-description": "Wordt weergegeven op de inlogpagina als 'Aanmelden met ...'. Handig als je meer dan één identiteitsprovider of SSO-protocol gebruikt.", "display-name-label": "Weergavenaam", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Naam van de sleutel die moet worden gebruikt voor het opzoeken van gebruikers-e-mails in de attribuutkaart van het OAuth2 ID-token.", "email-attribute-name-label": "Naam e-mailattribuut", "email-attribute-path-description": "JMESPath-expressie om te gebruiken voor het opzoeken van gebruikers-e-mails uit de gebruikersinformatie.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Gehele lijst van team-id's.", "team-ids-label": "Team-id's", "team-ids-numbers": "Team-id's moeten cijfers zijn.", - "team-ids-other": "Tekenreekslijst met team-id's.", + "team-ids-other": "", "team-ids-placeholder": "Voer team-id's in en druk op Enter om toe te voegen", "teams-url-description": "De URL die wordt gebruikt om naar team-id's te zoeken. Indien niet ingesteld, is de standaardwaarde /teams.", "teams-url-description-oauth": "Als je '{{ teamsURLLabel }}' configureert, moet je ook '{{ teamIDsAttributePathLabel }}' configureren.", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Terugzetten naar standaardwaarden" }, + "confirmText-reset": "", "disable": "Uitschakelen", "disabling": "Uitschakelen...", "discard": "Negeren", @@ -3473,11 +3563,11 @@ "button-create": "Aanmaken", "button-creating": "Aanmaken...", "cancel": "Annuleren", - "error-invalid-folder-name": "Ongeldige mapnaam", "error-required": "Mapnaam is vereist", "folder-name-input-placeholder-enter-folder-name": "Mapnaam invoeren", "label-folder-name": "Mapnaam", "text-pull-request-created": "Er is een pull request gemaakt met wijzigingen in deze map:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull request is aangemaakt", "title-this-repository-is-read-only": "Deze repository is alleen-lezen" }, @@ -3895,6 +3985,7 @@ "tooltip": "Sluiten" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Er is een fout opgetreden bij het laden van opties." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Berekent dynamisch interval door tijdsbereik te delen door het opgegeven aantal" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Doorgaan naar externe site?" } }, "render-left-actions": { - "text-public": "Openbaar", "tooltip-view-as-scene": "Weergeven als scène" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Kan dashboard niet laden" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Kan dashboard niet ophalen" @@ -4540,6 +4641,13 @@ "editable": "Bewerkbaar", "readonly": "Alleen-lezen" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Gegevens transformeren" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Weet je zeker dat je het dashboard naar versie {{version}} wilt herstellen? Alle niet opgeslagen wijzigingen zullen verloren gaan.", + "confirmText-restore-version": "", "title-restore-version": "Versie herstellen" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Deze titel is niet uniek" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Opslaan als" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Er bestaat al een dashboard met dezelfde naam in de geselecteerde map.<1><2>Wil je dit dashboard nog steeds opslaan?", "body-version-mismatch": "Iemand anders heeft dit dashboard bijgewerkt<1><2>Wil je dit dashboard nog steeds opslaan?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Conflict", "title-version-mismatch": "Conflict" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Naar het volgende dashboard", "playlist-previous": "Naar het vorige dashboard", "playlist-stop": "Afspeellijst stoppen", - "public-dashboard": "Openbaar", "refresh": "Dashboard vernieuwen", "save": "Dashboard opslaan", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Transformatie toepassen op" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Fouten opsporen", "title-disable-transformation": "Transformatie uitschakelen", "title-filter": "Filter", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Transformatie zoeken", "show-images": "Afbeeldingen weergeven", - "title-add-another-transformation": "Nog een transformatie toevoegen" + "title-add-another-transformation": "Nog een transformatie toevoegen", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Nog een transformatie toevoegen" }, + "confirmText-delete-all": "", "delete-all-transformations": "Alle transformaties verwijderen", "title-delete-all-transformations": "Alle transformaties verwijderen?", "tooltip-clear-search": "Zoekopdracht wissen", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Selectie van versie {{version}} in-/uitschakelen", "date": "Datum", + "name-latest": "", "notes": "Opmerkingen", "restore": "Herstellen", "updated-by": "Bijgewerkt door", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Stelt gebruikers in staat om aangepaste waarden aan de lijst toe te voegen", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Geef afmetingen op als csv: {{name}}, {{value}}", "label-data-source": "Gegevensbron", - "label-use-static-key-dimensions": "Gebruik statische sleutelafmetingen" + "label-use-static-key-dimensions": "Gebruik statische sleutelafmetingen", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Openbare URL intrekken" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Aangepaste opties", + "name-values-separated-comma": "", "selection-options": "Selectiemogelijkheden" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Type", "label-url": "URL", "label-with-tags": "Met labels", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Dashboard openen" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Type" }, "dashboard-preview-banner": { - "not-saved": "De waarde is nog niet opgeslagen in de Grafana-database", "not-yet-saved": "De waarde is niet opgeslagen in de Grafana-database", - "open-pull-request-in-git-hub": "Pull-verzoek openen in GitHub", - "title-dashboard-loaded-branch-git-hub": "Dit dashboard wordt geladen vanuit een filiaal in GitHub.", "title-dashboard-loaded-external-repository": "Dit dashboard wordt geladen vanuit een externe repository", - "title-dashboard-loaded-request-git-hub": "Dit dashboard wordt geladen vanuit een pull-verzoek in GitHub.", - "title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard", - "value-not-saved": "De waarde is nog niet opgeslagen in de Grafana-database", - "view-pull-request-in-git-hub": "Pull-verzoek bekijken in GitHub" + "title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Opties voor gegevensbronnen", "description-instance-name-filter": "Regex-filter voor welke gegevensbroninstanties uit de lijst met variabele waarden moet worden gekozen. Laat leeg voor alles.", "example-instance-name-filter": "Voorbeeld: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Selectiemogelijkheden" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Transformatie toevoegen" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Kolomopties", "row-height-options": "Opties voor rijhoogte" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Geef afmetingen op als csv: {{name}}, {{value}}", "group-by-options": "Groeperen op opties", "label-data-source": "Gegevensbron", - "label-use-static-group-by-dimensions": "Gebruik statische groepsafmetingen" + "label-use-static-group-by-dimensions": "Gebruik statische groepsafmetingen", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Kopiëren naar klembord", @@ -5521,9 +5710,14 @@ "apply": "Toepassen" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "De berekende waarde zal niet onder deze drempelwaarde komen", "description-step-count": "Hoe vaak het huidige tijdbereik moet worden verdeeld om de waarde te berekenen", - "interval-options": "Opties voor intervallen" + "interval-options": "Opties voor intervallen", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Gebruikt op {{count}} dashboards", "usage-count_other": "Gebruikt op {{count}} dashboards" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Er bestaat al een dashboard met dezelfde naam in de geselecteerde map. Wil je dit dashboard nog steeds opslaan?", "title-name-already-exists": "Naam bestaat al" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Doorgaan naar externe site?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Nog een transformatie toevoegen", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Alle transformaties verwijderen", "title-delete-all-transformations": "Alle transformaties verwijderen?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Benoemde opnamegroepen kunnen worden gebruikt om de weergavetekst en -waarde te scheiden (<1>zie voorbeelden).", "description-optional": "Optioneel, als je een deel van een serienaam of metrisch knooppuntsegment wilt extraheren.", "label-data-source": "Gegevensbron", + "label-static-options-sort": "", "label-target-data-source": "Doelgegevensbron", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opties voor query's", - "selection-options": "Selectiemogelijkheden" + "selection-options": "Selectiemogelijkheden", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Weet je zeker dat je het dashboard naar versie {{version}} wilt herstellen? Alle niet opgeslagen wijzigingen zullen verloren gaan.", + "confirmText-restore-version": "", "title-restore-version": "Versie herstellen" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Map", "render-footer": { "title-failed-to-save-dashboard": "Kan dashboard niet opslaan" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Maakt het mogelijk om meerdere waarden tegelijkertijd te selecteren", "description-enables-option-include-variables": "Schakelt een optie in om alle waarden op te nemen", - "description-enables-users-custom-values": "Stelt gebruikers in staat om aangepaste waarden aan de lijst toe te voegen" + "description-enables-users-custom-values": "Stelt gebruikers in staat om aangepaste waarden aan de lijst toe te voegen", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Menu delen in-/uitschakelen" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Kopiëren naar klembord mislukt" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(optioneel)", "text-options": "Tekstopties" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Wil je dit paneel echt ontkoppelen?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Er wordt door geen enkele variabele of dashboard naar deze variabele verwezen.", "aria-label-variable-referenced-other-variables-dashboard": "Deze variabele wordt gebruikt door andere variabelen of dashboard.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulier variabele bewerker", "back-to-list": "Terug naar lijst", + "confirmText": { + "delete-variable": "" + }, "delete": "Verwijderen", "description-optional-display-name": "Optionele weergavenaam", "description-template-variable-characters": "De naam van de sjabloonvariabele. (Max. 50 tekens)", "general": "Algemeen", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Omschrijving", "placeholder-label-name": "Labelnaam", "placeholder-variable-name": "Naam variabele", @@ -5829,13 +6066,25 @@ "variable": "Variabele" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Variabele verwijderen", "tooltip-duplicate-variable": "Variabele dupliceren", "tooltip-remove-variable": "Variabele verwijderen" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Verbergen" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Gebruik weergeven voor: {{variableId}}", "tooltip-show-usages": "Gebruik weergeven" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Selectie van versie {{version}} in-/uitschakelen", "date": "Datum", + "name-latest": "", "notes": "Opmerkingen", "restore": "Herstellen", "updated-by": "Bijgewerkt door", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Uploaden" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limiet", "label-value": "Waarde", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max.", "label-min": "Min.", - "label-value": "Waarde" + "label-value": "Waarde", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Servicenaam operator selecteren", "aria-label-select-span-name": "Selecteer een spannaam", "aria-label-select-span-name-operator": " Operator voor een spannaam selecteren", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Spanfilters", "label-duration": "Duur", "label-service-name": "Servicenaam", @@ -6939,6 +7199,8 @@ "split-widen": "Deelvenster verbreden" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Feedback geven", "label-copied": "Gekopieerd.", "label-export": "Exporteren", @@ -7046,6 +7308,7 @@ "when": "WANNEER" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Werking" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Mappen wissen", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Mapfilter", "select-placeholder": "Filteren op map" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Sorry, ik kon je verzoek niet voltooien. Probeer het opnieuw.", "send-custom-feedback": "Verzenden" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Breedtegraad", "label-longitude": "Lengtegraad" @@ -7153,6 +7463,14 @@ "center": "Midden:", "zoom": "Zoom:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Laag" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Geomap-stijlregel toevoegen" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Laag toevoegen", "no-layers": "Geen lagen?" @@ -7185,16 +7511,38 @@ "label-zoom": "Inzoomen", "use-current-map-settings": "Huidige kaartinstellingen gebruiken" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Symbool" }, "measure-overlay": { "tooltip-show-measure-tools": "Meetinstrumenten weergeven" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "De basiskaartlaag wordt geconfigureerd door de serverbeheerder." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Uitlijning", "label-baseline": "Uitgangswaarde", "label-color": "Kleur", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Symbool verticaal uitlijnen", "label-text-label": "Labeltekst", "label-x-offset": "X offset", - "label-y-offset": "Y offset" + "label-y-offset": "Y offset", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Vergelijkingsoperator", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Eigenschap uitlichten", "placeholder-numeric-value": "Numerieke waarde", "placeholder-value": "waarde" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "Kleur {{colorLabel}}" }, "confirm-button": { - "cancel": "Annuleren" + "cancel": "Annuleren", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Type '{{confirmPromptText}}' om te bevestigen" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "paneel in-/uitklappen", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Query annuleren", "tooltip-cancel-loading": "Query annuleren", "tooltip-stop-streaming": "Streamen stoppen", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Case matchen", "inspect-drawer-title": "Waarde inspecteren", "inspect-menu-label": "Waarde inspecteren", + "nested-table": { + "no-data": "" + }, "no-values-label": "Geen waarden", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} van {{numRows}} rijen" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Toevoegen", + "placeholder-new-tag": "", "remove": "Label verwijderen: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Klik om te {{actionTitle}}", "footer-click-to-navigate": "Klik om {{linkTitle}} te openen", "timestamp": "Tijdstempel" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Bibliotheekpaneel aanmaken" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Wil je dit paneel verwijderen?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Gebruikt op {{count}} dashboards" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Zoeken op naam of beschrijving" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Bibliotheekpaneel laden..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Maak een gepersonaliseerde, directe link om je dashboard binnen je organisatie te delen, met de volgende aanpassingsinstellingen:", "download-image": "Afbeelding downloaden", - "render-image": "Afbeelding genereren", - "render-image-error": "Kan paneelafbeelding niet weergeven", - "render-image-error-description": "Er is een fout opgetreden bij het genereren van de afbeelding" + "render-image": "Afbeelding genereren" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Lijnen omsluiten" }, "log-details": { - "close": "Logboekdetails sluiten", "fields": "Velden", "links": "Links", "log-line": "Logregel", @@ -8405,6 +8795,42 @@ "show-more": "meer", "tooltip-error": "Fout: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Link naar logboekregel kopiëren", "copy-log": "Logboekregel kopiëren", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Gegevenslinks", "aria-label-fields": "Velden", + "aria-label-line": "", "aria-label-log-level": "Logniveau", "aria-label-no-details": "Geen details" }, @@ -8598,6 +9030,8 @@ "updated-on": "Bijgewerkt op" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Verwijderen" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Dashboard importeren" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Deze tool kan sommige bronnen van deze installatie naar je cloudstack migreren. Om aan de slag te gaan, moet je een snapshot van deze installatie maken. Het maken van een snapshot duurt meestal minder dan twee minuten. De snapshot wordt naast deze Grafana-installatie opgeslagen.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Dashboard is al provisioned en beheerd door Grafana in de cloudinstantie. We raden je aan om voortaan het provisioned dashboard te gebruiken. Als je het dashboard nog steeds naar de cloud-instantie wilt kopiëren, wijzig dan het dashboard-ID in het dashboard-JSON, sla een nieuw snapshot op en upload opnieuw.", "datasource-already-managed": "Dashboard is al provisioned en beheerd door Grafana in de cloudinstantie. Als dit een andere bron is, stel dan een andere UID in en probeer het opnieuw.", "datasource-invalid-url": "Er is een gegevensbron met een ongeldige URL. Geef een geldige URL op en probeer het opnieuw.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": ">100 knooppunten", "aria-label-hidden-marker": "Verborgen knooppuntenmarkering: {{marker}}", - "node-count_one": "{{count}} knooppunt", + "node-count_one": "{{count}} knooppunten", "node-count_other": "{{count}} knooppunten" }, "node": { @@ -9318,9 +9763,9 @@ "aria-label-nodes-hidden-warning": "Waarschuwing verborgen knooppunten", "computing-layout": "Indeling berekenen", "no-data": "Geen gegevens", - "hidden-nodes_one": "<0> {{count}} knooppunt is verborgen om prestatieredenen.", + "hidden-nodes_one": "<0> {{count}} knooppunten zijn verborgen om prestatieredenen.", "hidden-nodes_other": "<0> {{count}} knooppunten zijn verborgen om prestatieredenen.", - "processed-nodes_one": "<0> Gelaagde indeling kan traag zijn met {{count}} knooppunt.", + "processed-nodes_one": "<0> Gelaagde indeling kan traag zijn met {{count}} knooppunten.", "processed-nodes_other": "<0> Gelaagde indeling kan traag zijn met {{count}} knooppunten." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Organisatie selecteren" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Je hebt geen toegangsrechten tot deze pagina.", "title-access-denied": "Toegang geweigerd" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Geen paginacomponent voor hoofdapp gevonden" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "plug-ins zijn bijgewerkt" }, "versions": { - "confirmation-text-1": "Weet je echt zeker dat je wilt downgraden naar versie", - "confirmation-text-2": "Normaal gesproken zou je dit niet moeten doen", + "confirmation-text": "", "downgrade-confirm": "Downgraden", "downgrade-title": "Plug-inversie downgraden" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Geen plug-ins gevonden" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Updaten" }, "install-controls-button": { - "title-uninstall-modal": "{{plugin}} verwijderen" + "title-uninstall-modal": "{{plugin}} verwijderen", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Deze plug-in is niet gepubliceerd op <2>grafana.com/plugins en kan niet via de catalogus worden beheerd.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Er wordt momenteel actief gewerkt aan de ontwikkeling van deze functie. Voor de beste ervaring en de nieuwste verbeteringen raden we je aan om de <2>nachtelijke versie van Grafana te gebruiken." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Serviceaccountkiezer", "select-placeholder": "Begin met typen om naar serviceaccounts te zoeken" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Serviceaccounttoken toevoegen", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Serviceaccount verwijderen", "disable-service-account": "Serviceaccount uitschakelen", "enable-service-account": "Serviceaccount inschakelen", @@ -10907,6 +11375,7 @@ "used-by": "Gebruikt door" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Bewerken" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Serviceaccount toevoegen", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Serviceaccount zoeken op naam", "sub-title": "Serviceaccounts en hun tokens kunnen worden gebruikt om te verifiëren tegen de Grafana API. Lees meer in onze <2>documentatie.", "title-delete-service-account": "Serviceaccount verwijderen", - "title-disable-service-account": "Serviceaccount uitschakelen" + "title-disable-service-account": "Serviceaccount uitschakelen", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Dit token is verlopen", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exporteren als JSON", "share-externally-title": "Extern delen", "share-internally-title": "Intern delen", @@ -10972,6 +11448,19 @@ "view-button": "JSON weergeven", "view-button-yaml": "YAML weergeven" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Bibliotheekpaneel aanmaken." }, @@ -10980,7 +11469,7 @@ "info-text": "Maak een directe link naar dit dashboard of paneel, aangepast met de onderstaande opties.", "link-url": "Link-URL", "render-alert": "Afbeeldingweergave-plug-in niet geïnstalleerd", - "render-instructions": "Om een paneelafbeelding weer te geven, moet je de <2>Grafana- afbeeldingsweergave-plug-in installeren. Neem contact op met je Grafana-beheerder om de plug-in te installeren.", + "render-instructions": "", "rendered-image": "Directe link naar gerenderde afbeelding", "save-alert": "Dashboard is niet opgeslagen", "save-dashboard": "Om een paneelafbeelding weer te geven, moet je eerst het dashboard opslaan.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Voorbeeld van paneel" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Te veel punten om goed te visualiseren. <1>Werk de query bij om minder punten te retourneren. <3>({{count}} punt ontvangen)", + "too-many-points_one": "Te veel punten om goed te visualiseren. <1>Werk de query bij om minder punten te retourneren. <3>({{count}} punten ontvangen)", "too-many-points_other": "Te veel punten om goed te visualiseren. <1>Werk de query bij om minder punten te retourneren. <3>({{count}} punten ontvangen)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Alternatieve tekst die wordt weergegeven als een afbeelding niet kan worden weergegeven of voor gebruikers die een schermlezer gebruiken", "description-title-text": "Tekst die wordt weergegeven wanneer de afbeelding wordt aangeraakt door een cursor", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Labeloptie" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Teamkiezer", "select-placeholder": "Selecteer een team" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Veld(en) of nummer" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Modus", "label-replace-all-fields": "Alle velden vervangen" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Label", "label-name": "Naam", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Frame" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Transformator omgezet veldtype toevoegen", "aria-label-remove-convert-field-type-transformer": "Transformator omgezet veldtype verwijderen", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Browser", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Invoerindeling", "label-join-with": "Meedoen met", "label-set-timezone": "Tijdzone instellen", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Type", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Gebruik een expliciet scheidingsteken bij het samenvoegen van reekswaarden", "tooltip-input-format": "Geef de indeling van het invoerveld op, zodat Grafana de datumreeks correct kan parseren.", "tooltip-specify-the-output-format": "Geef de uitvoerindeling op.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Uit" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Enum-waarde toevoegen", "generate-enum-values-from-data": "Enum-waarden genereren uit gegevens" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Verwijderen" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Scheidingsteken", "label-format": "Formaat", "label-keep-time": "Tijd bijhouden", "label-replace-all-fields": "Alle velden vervangen", "label-source": "Bron", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automatisch", "placeholder-select-delimiter": "Selecteer het scheidingsteken...", "tooltip-regexp": "Voorbeeld: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Drempelkleur" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Veld", - "label-lookup": "Opzoeken" + "label-lookup": "Opzoeken", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Aanvullende instellingen", @@ -11848,12 +12413,26 @@ "use-as": "Gebruiken als" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Van variabele", "label-identifier": "Identificatie", "label-use-variable": "Gebruik variabele", "label-variable": "Variabele", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Regulier expressiepatroon" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Veld", "label-match": "Match", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Voorwaarde toevoegen", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Voorwaarden", - "label-filter-type": "Type filteren" + "label-filter-type": "Type filteren", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Veld", "label-format": "Formaat", "label-substring-range": "Tekenreeksbereik" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formaat", "label-set-timezone": "Tijdzone instellen", @@ -11878,6 +12491,70 @@ "tooltip-format": "De uitvoerindeling voor het veld dat is opgegeven als een <2>Moment.js-indelingsreeks.", "tooltip-timezone-manually": "Stel de tijdzone van de datum handmatig in" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Browser", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Genegeerd", "placeholder-select-stats": "Selecteer statistieken" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Berekeningen hebben geen effect als er geen velden worden gegroepeerd" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Indien ingeschakeld, geven geneste tabellen veldnamen weer als een tabelkoptekst", "label-show-field-names-in-nested-tables": "Veldnamen weergeven in geneste tabellen", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Berekeningen hebben geen effect als er geen velden worden gegroepeerd." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Celwaarde", "label-column": "Kolom", "label-empty-value": "Lege waarde", "label-row": "Rij", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Booleaanse valse waarde", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automatisch", "placeholder-default": "Standaard: 30", "placeholder-none": "Geen" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Als percentiel", "tooltip-transform-index-percentile": "Transformeer de rij-index als een percentiel." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "De invoer bevat geen labels", "no-input-or-labels-found": "Geen invoer (of labels) gevonden", @@ -11969,10 +12685,30 @@ "label-labels": "Labels", "label-mode": "Modus", "label-value-field-name": "Naam waardeveld", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Optioneel) Label selecteren", "tooltip-replace-value-field-label": "Vervang de naam van het waardeveld door een label" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Waarde of variabele" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Tekenreekswaarden worden weergegeven als afzonderlijke velden in plaats van als labels" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Samenvoegen heeft geen effect wanneer toegepast op een enkel frame." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Samenvoegen heeft geen effect wanneer toegepast op een enkel frame.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Alle waarden zijn numeriek", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Tijd in oplopende volgorde" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automatisch", "field-order-manual": "", - "first-frame-warning": "Velden organiseren werkt alleen met een enkel frame. Overweeg om eerst een verbindingstransformatie toe te passen of de invoer te filteren." + "first-frame-warning": "Velden organiseren werkt alleen met een enkel frame. Overweeg om eerst een verbindingstransformatie toe te passen of de invoer te filteren.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Stelt in hoe de namen van de geselecteerde velden worden weergegeven. Omdat framenaam meestal beter is voor tabelgegevens" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Scheiding op waarden werkt alleen met een enkel frame.", "tooltip-keeps-partition-fields-frames": "Houdt de scheidingsvelden in de frames" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formaat", - "label-info": "Info" + "label-info": "Info", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "en", @@ -12035,6 +12815,9 @@ "label-operation": "Werking" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Berekeningen", "label-include-time": "Inclusief tijd", "label-labels-to-fields": "Labels naar velden", @@ -12049,12 +12832,23 @@ "series-to-rows": "Reeksen naar rijen" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Statistieken kiezen" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Waarde of variabele", "variable-regex-documentation": "Formatteert variabele met meerdere waarden in een regex-tekenreeks" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Kubiek", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Aantal X-,Y-punten om te voorspellen" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Match", "label-replace": "Vervangen", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Veld", - "label-mode": "Modus" + "label-mode": "Modus", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Veld", "label-reverse": "Omkeren", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Veld selecteren" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transformeren" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Tijdveld", @@ -12121,10 +12960,34 @@ "placeholder-value": "Waarde", "tooltip-name-for-value-fields": "Naam voor waardevelden" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Werking", "placeholder-field": "Veld" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Meerdere waardevelden van elk type", "single-frame": "Enkel frame", @@ -12177,6 +13040,7 @@ "title": "Organisaties" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Gebruikerskiezer", "select-placeholder": "Begin te typen om te zoeken naar gebruiker" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Variabele verwijderen" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Vernieuwen" }, "query-variable-sort-select": { - "description-values-variable": "De waarden van deze variabele sorteren" + "description-values-variable": "De waarden van deze variabele sorteren", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "standaardwaarde, indien aanwezig", "text-options": "Tekstopties" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Optionele weergavenaam", "description-template-variable-characters": "De naam van de sjabloonvariabele. (Max. 50 tekens)", "general": "Algemeen", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Omschrijving", "placeholder-label-name": "Labelnaam", "placeholder-variable-name": "Naam variabele", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Variabele dupliceren", "tooltip-remove-variable": "Variabele verwijderen" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Alle waarden in-/uitschakelen" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Gebruik weergeven" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 4b87289505b..e04c0c2989f 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Niektóre funkcje są stabilne (GA) i domyślnie włączone, podczas gdy inne są obecnie we wstępnej fazie beta i dostępne do wczesnego wdrożenia.", "confirm-modal-body-2": "Przed wprowadzeniem modyfikacji zalecamy przeanalizowanie konsekwencji każdej zmiany funkcji.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Ogólna dostępność", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Usuń organizację", + "confirmText-delete": "", "title-delete": "Usuń" }, "anon-users": { "not-found": "Nie znaleziono użytkowników anonimowych." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Wymuś wylogowanie ze wszystkich urządzeń" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Nie masz uprawnień do wyświetlania użytkowników w tej organizacji. Aby zaktualizować tę organizację, skontaktuj się z administratorem serwera.", "heading": "Edytuj organizację", @@ -208,9 +216,11 @@ "not-editable": "Nie można edytować roli tego użytkownika, ponieważ synchronizuje ją dostawca autoryzacji. Szczegółowe informacje znajdziesz w <1> dokumentach uwierzytelniających usługi Grafana." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Rola" }, + "confirmText-delete": "", "delete-aria-label": "Usuń użytkownika: {{name}}", "title-delete": "Usuń" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Te ustawienia systemowe są zdefiniowane w pliku grafana.ini lub custom.ini (lub nadpisane w zmiennych ENV). Aby je zmienić, obecnie należy ponownie uruchomić usługę Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licencja Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Zmień", + "confirmText-change": "", "grafana-admin-key": "Administrator usługi Grafana", "grafana-admin-no": "Nie", "grafana-admin-yes": "Tak", "title": "Uprawnienia" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Usuń użytkownika", "disable-button": "Wyłącz użytkownika", "edit-button": "Edytuj", @@ -312,6 +330,9 @@ "title-delete-user": "Usuń użytkownika", "title-disable-user": "Wyłącz użytkownika" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Przeglądarka i system operacyjny", "force-logout-all-button": "Wymuś wylogowanie ze wszystkich urządzeń", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Aktywne harmonogramy" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Dodaj więcej" }, @@ -465,6 +490,9 @@ "label-muting-grouping-and-timings-optional": "Wyciszanie, grupowanie i harmonogramy (opcjonalnie)", "title-muting-grouping-and-timings": "Wyciszanie, grupowanie i harmonogramy" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Kopiuj link", "duplicate": "Duplikuj", @@ -488,6 +516,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Edytuj YAML", @@ -554,6 +585,7 @@ "view-configuration": "Wyświetl konfigurację" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Nie można ręcznie zmienić wewnętrznej konfiguracji menedżera alertów Grafany. Aby zmienić tę konfigurację, edytuj poszczególne zasoby z poziomu interfejsu użytkownika.", "gma-manual-configuration-is-not-supported": "Ręczne zmiany konfiguracji nie są obsługiwane", "message": { @@ -568,11 +600,13 @@ "title-resetting-alertmanager-configuration": "Resetowanie konfiguracji menedżera alertów" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Porównaj", "restore": "Przywróć", "text-latest": "Najnowsze" }, + "confirmText-yes-restore-configuration": "", "loading": "Ładowanie…", "no-previous-configurations": "Brak wcześniejszych konfiguracji", "this-might-take-a-while": "Może to chwilę potrwać…", @@ -852,8 +886,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Więcej działań dla punktu kontaktu „{{contactPointName}}”", + "ariaLabel-delete": "", "button-edit": "Edytuj", "button-view": "Wyświetl", + "export-ariaLabel-export": "", "export-label-export": "Eksport", "label-delete": "Usuń", "label-manage-permissions": "Zarządzaj uprawnieniami", @@ -1392,6 +1428,7 @@ "label-disable-resolved-message": "Wyłącz rozwiązaną wiadomość" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Musi to być dodatnia liczba całkowita.", "must-enter-a-group-name": "Należy podać nazwę grupy" @@ -1483,15 +1520,12 @@ "group-details": { "ds-features-error": "Błąd podczas ładowania szczegółów źródła danych", "edit": "Edytuj", - "evaluations-to-fire": "Cykle oceny do uruchomienia", "export": "Eksport", "folder": "Folder", "group-loading-error": "Błąd podczas ładowania grupy", "interval": "Interwał", "namespace": "Przestrzeń nazw", - "pending-period": "Okres oczekiwania", - "recording": "Nagrywanie", - "rule-name": "Nazwa reguły" + "new": "" }, "group-edit": { "ds-error": "Błąd podczas ładowania szczegółów źródła danych", @@ -1537,8 +1571,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Rozpocznij", "label-insights": "Analizy", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alerty" }, "import-to-gma": { @@ -1840,7 +1886,11 @@ "other-data-sources": "Inne źródła danych" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Wyłączone", @@ -2093,9 +2143,11 @@ "update-errors": { "conflict": "Inny użytkownik zaktualizował drzewo zbiorów zasad dot. powiadamiania.", "error-code": "Komunikat o błędzie: „{{error}}”", - "fallback": "Błąd podczas aktualizowania Twoich zasad powiadomień.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Odśwież stronę i spróbuj ponownie.", - "title": "Błąd podczas zapisywania zasad dot. powiadomień" + "title": "" }, "n-more-policies_one": "{{count}} dodatkowy zbiór zasad", "n-more-policies_few": "{{count}} dodatkowe zbiory zasad", @@ -2153,6 +2205,7 @@ "query-and-expressions-step": { "add-query": "Dodaj zapytanie", "body-queries-expressions-configured": "Utwórz co najmniej jedno zapytanie lub wyrażenie, które będzie powodować wyświetlenie alertu", + "confirmText-deactivate": "", "expressions": "Wyrażenia", "loading-data-sources": "Wczytywanie źródeł danych…", "manipulate-returned-queries-other-operations": "Manipulowanie danymi zwracanymi z zapytań za pomocą działań matematycznych i innych operacji.", @@ -2202,6 +2255,9 @@ "label-export-all": "Eksportuj wszystkie" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Nie udało się załadować edytora zapytań. Powód: {{errorMessage}}" }, @@ -2220,6 +2276,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Konieczne będzie ustawienie nowej grupy oceny dla skopiowanej reguły, ponieważ oryginalna grupa została aprowizowana i nie można jej użyć w przypadku reguł utworzonych w interfejsie użytkownika.", "body-not-provisioned": "Nowa reguła <1>nie zostanie oznaczona jako reguła po aprowizacji.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Kopiuj regułę aprowizowanego alertu" }, "redirect-to-rule-viewer": { @@ -2419,8 +2476,6 @@ "title-inspect-alert-rule": "Zbadaj regułę alertu" }, "rule-list": { - "cannot-find-rule-details-for": "Nie można znaleźć szczegółów reguły dla identyfikatora {{uid}}", - "cannot-load-rule-details-for": "Nie można załadować szczegółów reguły dla UID {{uid}}", "configure-datasource": "Konfiguruj", "draft-new-rule": "Zaprojektuj nową regułę", "ds-error": { @@ -2624,6 +2679,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Nie możesz znaleźć odpowiedniego źródła danych? Niektóre źródła danych nie są obsługiwane w przypadku alertów. Aby dowiedzieć się więcej, kliknij ikonę." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Dodaj nowego menedżera alertów", "builtin-alertmanager": "Wbudowany menedżer alertów", @@ -2776,6 +2836,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Wybierz szablon powiadomienia", "loading": "Ładowanie…", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Wybierz szablon powiadomienia" } @@ -2802,6 +2865,8 @@ }, "templates-table": { "actions": "Działania", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Nie zdefiniowano szablonów.", "template-group": "Grupa szablonów", "title-delete-template-group": "Usuń grupę szablonów" @@ -2854,6 +2919,11 @@ "timestamp": { "time-ago": "({{time}} temu)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Folder docelowy nie jest pusty. Niektóre reguły mogą zostać zastąpione lub usunięte. Na pewno chcesz zaimportować te reguły alertów do reguł zarządzanych przez usługę Grafana?", @@ -2929,6 +2999,11 @@ "title-delete-contact-point": "Usuń punkt kontaktu" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Usuń zasady dotyczące powiadomień" @@ -3085,7 +3160,8 @@ "annotation-field-mapper": { "annotation": "Adnotacja", "first-value": "Pierwsza wartość", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Dodaj zapytanie do komentarza", @@ -3093,6 +3169,16 @@ "info-box-content-2": "Więcej informacji można znaleźć w <2>dokumentacji komentarzy.", "title": "Brak dodanych niestandardowych zapytań do komentarzy" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "Liczba zdarzeń: {{numEvents}} (z {{numFields}} pól)", "no-events-found": "Nie znaleziono wydarzeń", @@ -3168,6 +3254,9 @@ "define-allowed-teams-ids-label": "Zdefiniuj dozwolone identyfikatory zespołów", "display-name-description": "Nazwa będzie się wyświetlać na stronie logowania jako część tekstu „Zaloguj się za pomocą...”. To przydatne, jeśli korzystasz z więcej niż jednego dostawcy tożsamości lub protokołów SSO.", "display-name-label": "Nazwa wyświetlana", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nazwa klucza używanego do wyszukiwania adresu e-mail użytkownika w mapie atrybutów tokena OAuth2 ID.", "email-attribute-name-label": "Nazwa atrybutu adresu e-mail", "email-attribute-path-description": "Wyrażenie JMESPath używane do wyszukiwania adresu e-mail użytkownika na podstawie danych użytkownika.", @@ -3219,7 +3308,7 @@ "team-ids-github": "Lista identyfikatorów zespołów w postaci liczb całkowitych.", "team-ids-label": "Identyfikatory zespołów", "team-ids-numbers": "Identyfikatory zespołów muszą być liczbami.", - "team-ids-other": "Lista identyfikatorów zespołów w postaci ciągów.", + "team-ids-other": "", "team-ids-placeholder": "Wprowadź identyfikatory zespołów i naciśnij klawisz Enter, aby je dodać", "teams-url-description": "Adres URL używany do wyszukiwania identyfikatorów zespołów. Jeśli nie jest ustawiony, wartością domyślną jest /teams.", "teams-url-description-oauth": "Jeśli skonfigurujesz „{{ teamsURLLabel }}”, musisz również skonfigurować „{{ teamIDsAttributePathLabel }}”.", @@ -3263,6 +3352,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Zresetuj do wartości domyślnych" }, + "confirmText-reset": "", "disable": "Wyłącz", "disabling": "Wyłączanie…", "discard": "Odrzuć", @@ -3509,11 +3599,11 @@ "button-create": "Utwórz", "button-creating": "Tworzenie…", "cancel": "Anuluj", - "error-invalid-folder-name": "Nieprawidłowa nazwa folderu", "error-required": "Wymagana jest nazwa folderu", "folder-name-input-placeholder-enter-folder-name": "Podaj nazwę folderu", "label-folder-name": "Nazwa folderu", "text-pull-request-created": "Utworzono żądanie pull ze zmianami w tym folderze:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Utworzono żądanie pull", "title-this-repository-is-read-only": "To repozytorium jest tylko do odczytu" }, @@ -3931,6 +4021,7 @@ "tooltip": "Zamknij" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Podczas ładowania opcji wystąpił błąd." @@ -4199,8 +4290,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Dynamicznie oblicza odstęp czasu, dzieląc zakres przez określoną liczbę" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4362,12 +4453,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Przejść do witryny zewnętrznej?" } }, "render-left-actions": { - "text-public": "Publiczny", "tooltip-view-as-scene": "Wyświetl jako scenę" } }, @@ -4560,6 +4653,14 @@ "errors": { "failed-to-load": "Nie udało się załadować pulpitu" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Nie udało się pobrać pulpitu" @@ -4576,6 +4677,13 @@ "editable": "Możliwość edycji", "readonly": "Tylko do odczytu" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4625,6 +4733,18 @@ "transform-data": "Transformacja danych" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4882,6 +5002,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Czy na pewno chcesz przywrócić pulpit w wersji {{version}}? Wszystkie niezapisane zmiany zostaną utracone.", + "confirmText-restore-version": "", "title-restore-version": "Przywróć wersję" }, "row-options-button": { @@ -4932,6 +5053,9 @@ "title-not-unique": "Ten tytuł nie jest unikalny" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Zapisz jako" }, @@ -4966,6 +5090,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "W wybranym folderze istnieje już pulpit o tej samej nazwie.<1><2>Czy nadal chcesz zapisać ten pulpit?", "body-version-mismatch": "Ktoś inny zaktualizował ten pulpit<1><2>Czy nadal chcesz zapisać ten pulpit?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Konflikt", "title-version-mismatch": "Konflikt" }, @@ -5131,7 +5256,6 @@ "playlist-next": "Przejdź do następnego pulpitu", "playlist-previous": "Przejdź do poprzedniego pulpitu", "playlist-stop": "Zatrzymaj autoodtwarzanie", - "public-dashboard": "Publiczny", "refresh": "Odśwież pulpit", "save": "Zapisz pulpit", "save-dashboard": { @@ -5162,7 +5286,9 @@ "label-apply-transformation-to": "Zastosuj transformację do" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Debugowanie", "title-disable-transformation": "Wyłącz transformację", "title-filter": "Filtr", @@ -5182,12 +5308,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Szukaj transformacji", "show-images": "Pokaż obrazki", - "title-add-another-transformation": "Dodaj inną transformację" + "title-add-another-transformation": "Dodaj inną transformację", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Dodaj inną transformację" }, + "confirmText-delete-all": "", "delete-all-transformations": "Usuń wszystkie transformacje", "title-delete-all-transformations": "Usunąć wszystkie transformacje?", "tooltip-clear-search": "Wyczyść wyszukiwanie", @@ -5224,6 +5355,7 @@ "version-history-table": { "aria-label-toggle-selection": "Przełącz wybór wersji {{version}}", "date": "Data", + "name-latest": "", "notes": "Uwagi", "restore": "Przywróć", "updated-by": "Zaktualizowane przez", @@ -5300,7 +5432,8 @@ "description-enables-users-custom-values": "Umożliwia użytkownikom dodawanie niestandardowych wartości do listy", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Podaj wymiary jako wartości rozdzielone przecinkami: {{name}}, {{value}}", "label-data-source": "Źródło danych", - "label-use-static-key-dimensions": "Użyj statycznych wymiarów klucza" + "label-use-static-key-dimensions": "Użyj statycznych wymiarów klucza", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5373,6 +5506,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Cofnij upublicznienie URL" } @@ -5384,6 +5520,7 @@ }, "custom-variable-form": { "custom-options": "Opcje niestandardowe", + "name-values-separated-comma": "", "selection-options": "Opcje wyboru" }, "dashboard-edit-pane-renderer": { @@ -5402,6 +5539,12 @@ "label-type": "Typ", "label-url": "URL", "label-with-tags": "Z tagami", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Otwórz pulpit" }, "dashboard-link-list": { @@ -5414,15 +5557,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Wartość nie została jeszcze zapisana w bazie danych Grafana", "not-yet-saved": "Wartość nie została zapisana w bazie danych Grafana", - "open-pull-request-in-git-hub": "Otwórz pull request w GitHub", - "title-dashboard-loaded-branch-git-hub": "Ten pulpit nawigacyjny jest ładowany z gałęzi w GitHub.", "title-dashboard-loaded-external-repository": "Ten pulpit nawigacyjny jest ładowany z zewnętrznego repozytorium", - "title-dashboard-loaded-request-git-hub": "Ten pulpit nawigacyjny jest ładowany z pull request w GitHub.", - "title-error-loading-dashboard": "Błąd wczytywania pulpitu", - "value-not-saved": "Wartość nie została jeszcze zapisana w bazie danych Grafana", - "view-pull-request-in-git-hub": "Wyświetl pull request w GitHub" + "title-error-loading-dashboard": "Błąd wczytywania pulpitu" }, "dashboard-scene": { "text": { @@ -5448,6 +5585,8 @@ "data-source-options": "Opcje źródła danych", "description-instance-name-filter": "Filtr wyrażeń regularnych, w przypadku których instancje źródeł danych należy wybrać z listy wartości zmiennych. Pozostaw puste dla wszystkich.", "example-instance-name-filter": "Przykład: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Opcje wyboru" }, "default-grid-layout-manager": { @@ -5493,12 +5632,61 @@ "empty-transformations-message": { "add-transformation": "Dodaj transformację" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Opcje kolumn", "row-height-options": "Opcje wysokości wiersza" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5523,7 +5711,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Podaj wymiary jako wartości rozdzielone przecinkami: {{name}}, {{value}}", "group-by-options": "Grupuj według opcji", "label-data-source": "Źródło danych", - "label-use-static-group-by-dimensions": "Użyj statycznych wymiarów grupy" + "label-use-static-group-by-dimensions": "Użyj statycznych wymiarów grupy", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Kopiuj do schowka", @@ -5559,9 +5748,14 @@ "apply": "Zastosuj" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Obliczona wartość nie spadnie poniżej tego progu", "description-step-count": "Ile razy należy podzielić bieżący zakres czasu, aby obliczyć wartość", - "interval-options": "Opcje odstępu czasu" + "interval-options": "Opcje odstępu czasu", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5583,11 +5777,28 @@ "usage-count_many": "Używane na {{count}} pulpitach", "usage-count_other": "Używane na {{count}} pulpitach" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "W wybranym folderze istnieje już pulpit o tej samej nazwie. Czy nadal chcesz zapisać ten pulpit?", "title-name-already-exists": "Nazwa już istnieje" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Przejść do witryny zewnętrznej?" } @@ -5623,6 +5834,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Dodaj inną transformację", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Usuń wszystkie transformacje", "title-delete-all-transformations": "Usunąć wszystkie transformacje?" }, @@ -5675,9 +5888,13 @@ "description-examples": "Nazwane grupy rejestracji mogą być używane do oddzielania wyświetlanego tekstu i wartości (<1>patrz przykłady).", "description-optional": "Opcjonalnie pozwalają wyodrębnić część nazwy serii lub segmentu węzła metryki.", "label-data-source": "Źródło danych", + "label-static-options-sort": "", "label-target-data-source": "Docelowe źródło danych", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opcje zapytania", - "selection-options": "Opcje wyboru" + "selection-options": "Opcje wyboru", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5690,6 +5907,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Czy na pewno chcesz przywrócić pulpit w wersji {{version}}? Wszystkie niezapisane zmiany zostaną utracone.", + "confirmText-restore-version": "", "title-restore-version": "Przywróć wersję" }, "save-button": { @@ -5707,7 +5925,8 @@ "label-folder": "Folder", "render-footer": { "title-failed-to-save-dashboard": "Nie udało się zapisać pulpitu" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5785,7 +6004,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Umożliwia wybór wielu wartości jednocześnie", "description-enables-option-include-variables": "Włącza opcję uwzględniania wszystkich wartości", - "description-enables-users-custom-values": "Umożliwia użytkownikom dodawanie niestandardowych wartości do listy" + "description-enables-users-custom-values": "Umożliwia użytkownikom dodawanie niestandardowych wartości do listy", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Przełącz menu udostępniania" @@ -5805,6 +6028,9 @@ "copy-to-clipboard-failed": "Nie udało się skopiować do schowka" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opcjonalnie)", "text-options": "Opcje tekstu" @@ -5828,6 +6054,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Czy na pewno chcesz odłączyć ten panel?" }, "unsaved-changes-modal": { @@ -5844,6 +6072,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Do tej zmiennej nie odwołuje się żadna zmienna ani pulpit.", "aria-label-variable-referenced-other-variables-dashboard": "Do tej zmiennej odwołują się inne zmienne lub pulpit.", @@ -5853,10 +6084,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formularz edytora zmiennych", "back-to-list": "Powrót do listy", + "confirmText": { + "delete-variable": "" + }, "delete": "Usuń", "description-optional-display-name": "Opcjonalna nazwa wyświetlana", "description-template-variable-characters": "Nazwa zmiennej szablonu. (maks. 50 znaków)", "general": "Ogólne", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Tekst opisowy", "placeholder-label-name": "Nazwa etykiety", "placeholder-variable-name": "Nazwa zmiennej", @@ -5871,13 +6108,25 @@ "variable": "Zmienna" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Usuń zmienną", "tooltip-duplicate-variable": "Duplikuj zmienną", "tooltip-remove-variable": "Usuń zmienną" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Ukryj" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Pokazuje użycie dla: {{variableId}}", "tooltip-show-usages": "Wyświetl użycie" @@ -5904,6 +6153,7 @@ "version-history-table": { "aria-label-toggle-selection": "Przełącz wybór wersji {{version}}", "date": "Data", + "name-latest": "", "notes": "Uwagi", "restore": "Przywróć", "updated-by": "Zaktualizowane przez", @@ -6291,7 +6541,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Prześlij" @@ -6329,6 +6580,7 @@ }, "label-limit": "Limit", "label-value": "Wartość", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6337,9 +6589,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Maks.", "label-min": "Min.", - "label-value": "Wartość" + "label-value": "Wartość", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6911,6 +7169,8 @@ "aria-label-select-service-name-operator": "Wybierz operator nazwy usługi", "aria-label-select-span-name": "Wybierz nazwę zakresu", "aria-label-select-span-name-operator": "Wybierz operator nazwy zakresu", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtry zakresu", "label-duration": "Czas trwania", "label-service-name": "Nazwa usługi", @@ -6981,6 +7241,8 @@ "split-widen": "Poszerz okno" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Przekaż opinię", "label-copied": "Skopiowano", "label-export": "Eksport", @@ -7088,6 +7350,7 @@ "when": "KIEDY" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operacja" }, "math": { @@ -7118,6 +7381,7 @@ }, "folder-filter": { "clear-folder-button": "Wyczyść foldery", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtr folderów", "select-placeholder": "Filtruj wg folderu" }, @@ -7186,7 +7450,53 @@ "incomplete-request-error": "Nie udało nam się zrealizować Twojego żądania. Spróbuj ponownie.", "send-custom-feedback": "Wyślij" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Szerokość geograficzna", "label-longitude": "Długość geograficzna" @@ -7195,6 +7505,14 @@ "center": "Środek:", "zoom": "Powiększenie:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Warstwa" @@ -7217,6 +7535,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Dodaj regułę stylu geomapy" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Dodaj warstwę", "no-layers": "Brak warstw?" @@ -7227,16 +7553,38 @@ "label-zoom": "Powiększenie", "use-current-map-settings": "Użyj bieżących ustawień mapy" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Symbol" }, "measure-overlay": { "tooltip-show-measure-tools": "Pokaż narzędzia pomiarowe" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Warstwa mapy bazowej jest konfigurowana przez administratora serwera." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Wyrównanie", "label-baseline": "Linia bazowa", "label-color": "Kolor", @@ -7250,7 +7598,14 @@ "label-symbol-vertical-align": "Wyrównanie symbolu w pionie", "label-text-label": "Etykieta tekstowa", "label-x-offset": "Przesunięcie względem osi X", - "label-y-offset": "Przesunięcie względem osi Y" + "label-y-offset": "Przesunięcie względem osi Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operator porównania", @@ -7261,6 +7616,15 @@ "placeholder-feature-property": "Właściwość funkcji", "placeholder-numeric-value": "Wartość numeryczna", "placeholder-value": "wartość" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7510,7 +7874,8 @@ "aria-label-selected-color": "Kolor {{colorLabel}}" }, "confirm-button": { - "cancel": "Anuluj" + "cancel": "Anuluj", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Wpisz „{{confirmPromptText}}”, aby potwierdzić" @@ -7692,6 +8057,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "przełącznik zwinięcia panelu", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Anuluj zapytanie", "tooltip-cancel-loading": "Anuluj zapytanie", "tooltip-stop-streaming": "Zatrzymaj strumieniowanie", @@ -7758,6 +8125,9 @@ "filter-popup-match-case": "Dopasuj wielkość liter", "inspect-drawer-title": "Sprawdź wartość", "inspect-menu-label": "Sprawdź wartość", + "nested-table": { + "no-data": "" + }, "no-values-label": "Brak wartości", "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} z {{numRows}} wierszy" }, @@ -7766,6 +8136,7 @@ }, "tags-input": { "add": "Dodaj", + "placeholder-new-tag": "", "remove": "Usuń znacznik: {{name}}" }, "time-sync-button": { @@ -7859,6 +8230,12 @@ "footer-click-to-action": "Kliknij, aby podjąć działanie: {{actionTitle}}", "footer-click-to-navigate": "Kliknij, aby otworzyć {{linkTitle}}", "timestamp": "Znacznik czasu" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8241,6 +8618,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Utwórz panel biblioteki" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Czy chcesz usunąć ten panel?" }, @@ -8266,7 +8647,7 @@ "usage-count_other": "Używane na {{count}} pulpitu" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Szukaj według nazwy lub opisu" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Wczytywanie panelu biblioteki…" @@ -8320,9 +8701,7 @@ "share-panel": { "config-description": "Utwórz spersonalizowany, bezpośredni link, aby udostępnić panel w swojej organizacji z następującymi ustawieniami dostosowywania:", "download-image": "Pobierz obraz", - "render-image": "Wygeneruj obraz", - "render-image-error": "Nie udało się wyrenderować obrazu panelu", - "render-image-error-description": "Podczas generowania obrazu wystąpił błąd" + "render-image": "Wygeneruj obraz" } }, "live": { @@ -8408,6 +8787,24 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_few": "", + "indexed-label_many": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_few": "", + "parsedl-label_many": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_few": "", + "structured-metadata_many": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8431,7 +8828,6 @@ "label-wrap-lines": "Zawijaj wiersze" }, "log-details": { - "close": "Zamknij szczegóły dziennika", "fields": "Pola", "links": "Linki", "log-line": "Wiersz logu", @@ -8457,6 +8853,42 @@ "show-more": "pokaż więcej", "tooltip-error": "Błąd: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Kopiuj link do wiersza logu", "copy-log": "Kopiuj wiersz logu", @@ -8557,6 +8989,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8583,6 +9020,7 @@ "un-themed-log-details": { "aria-label-data-links": "Połączenia danych", "aria-label-fields": "Pola", + "aria-label-line": "", "aria-label-log-level": "Poziom logów", "aria-label-no-details": "Brak szczegółów" }, @@ -8650,6 +9088,8 @@ "updated-on": "Data aktualizacji" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Usuń" }, "unthemed-dashboard-import": { @@ -8659,8 +9099,17 @@ "text": { "import-dashboard": "Importuj pulpit" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "To narzędzie może migrować niektóre zasoby z tej instalacji do stosu w chmurze. Aby rozpocząć, musisz utworzyć migawkę tej instalacji. Utworzenie migawki zajmuje zwykle mniej niż dwie minuty. Migawka jest przechowywana obok instalacji Grafana.", @@ -8809,6 +9258,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Pulpit jest już skonfigurowany w usłudze Grafana i zarządzany z jej poziomu w instancji w chmurze. Zalecamy korzystanie z pulpitu skonfigurowanego. Jeśli nadal chcesz skopiować pulpit do instancji w chmurze, zmień identyfikator pulpitu w jego pliku JSON, zapisz nową migawkę i prześlij ponownie.", "datasource-already-managed": "Źródło danych jest już skonfigurowane w usłudze Grafana i zarządzane z jej poziomu w instancji w chmurze. Jeśli jest to inny zasób, ustaw inny UID i spróbuj ponownie.", "datasource-invalid-url": "Istnieje źródło danych, które ma nieprawidłowy adres URL. Podaj prawidłowy adres URL i spróbuj ponownie.", @@ -9359,7 +9810,7 @@ "marker": { "100-node-count": ">100 węzłów", "aria-label-hidden-marker": "Znacznik ukrytych węzłów: {{marker}}", - "node-count_one": "{{count}} węzeł", + "node-count_one": "{{count}} węzła", "node-count_few": "{{count}} węzły", "node-count_many": "{{count}} węzłów", "node-count_other": "{{count}} węzła" @@ -9372,13 +9823,13 @@ "aria-label-nodes-hidden-warning": "Ostrzeżenie o ukrytych węzłach", "computing-layout": "Obliczanie układu", "no-data": "Brak danych", - "hidden-nodes_one": "<0> {{count}} węzeł jest ukryty ze względu na wydajność.", + "hidden-nodes_one": "<0> {{count}} węzła jest ukryte ze względu na wydajność.", "hidden-nodes_few": "<0> {{count}} węzły są ukryte ze względu na wydajność.", "hidden-nodes_many": "<0> {{count}} węzłów jest ukrytych ze względu na wydajność.", "hidden-nodes_other": "<0> {{count}} węzła jest ukryte ze względu na wydajność.", - "processed-nodes_one": "<0> Układ warstwowy może działać powoli w przypadku {{count}} węzła.", + "processed-nodes_one": "<0> Układ warstwowy może działać powoli w przypadku {{count}} węzła.", "processed-nodes_few": "<0> Układ warstwowy może działać powoli w przypadku {{count}} węzłów.", - "processed-nodes_many": "<0> Układ warstwowy może działać powoli w przypadku {{count}} węzłów.", + "processed-nodes_many": "<0> Układ warstwowy może działać powoli w przypadku {{count}} węzła.", "processed-nodes_other": "<0> Układ warstwowy może działać powoli w przypadku {{count}} węzła." }, "node-graph-panel": { @@ -9505,6 +9956,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Wybierz organizację" }, "page": { @@ -9727,6 +10179,7 @@ "permission": "Nie masz uprawnień do wyświetlenia tej strony.", "title-access-denied": "Odmowa dostępu" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Nie znaleziono komponentu strony głównej aplikacji" }, "browse": { @@ -9770,8 +10223,7 @@ "update-status-text": "wtyczki są aktualne" }, "versions": { - "confirmation-text-1": "Czy na pewno chcesz przejść na starszą wersję", - "confirmation-text-2": "To działanie nie jest zwykle zalecane", + "confirmation-text": "", "downgrade-confirm": "Przejście na starszą wersję", "downgrade-title": "Użyj starszej wersji wtyczki" } @@ -9825,6 +10277,10 @@ "empty-state": { "message": "Nie znaleziono wtyczek" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9858,7 +10314,11 @@ "updating": "Aktualizowanie" }, "install-controls-button": { - "title-uninstall-modal": "Odinstaluj wtyczkę {{plugin}}" + "title-uninstall-modal": "Odinstaluj wtyczkę {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Ta wtyczka nie została opublikowana na stronie <2>grafana.com/plugins i nie można nią zarządzać za pośrednictwem katalogu.", @@ -10029,6 +10489,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Ta funkcja jest obecnie aktywnie rozwijana. Aby uzyskać najlepsze wrażenia i najnowsze ulepszenia, zalecamy korzystanie z <2>nocnej kompilacji aplikacji Grafana." @@ -10904,6 +11373,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Selektor konta usługi", "select-placeholder": "Zacznij pisać, aby wyszukać konta usług" }, @@ -10949,6 +11419,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Dodaj token konta usługi", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Usuń konto usługi", "disable-service-account": "Wyłącz konto usługi", "enable-service-account": "Włącz konto usługi", @@ -10975,6 +11449,7 @@ "used-by": "Używane przez" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Edytuj" }, "service-account-role-row": { @@ -10988,10 +11463,18 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Dodaj konto usługi", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Wyszukaj konto usługi według nazwy", "sub-title": "Konta usług i ich tokeny mogą być używane do uwierzytelniania w interfejsie API Grafana. Więcej informacji znajdziesz w <2>dokumentacji.", "title-delete-service-account": "Usuń konto usługi", - "title-disable-service-account": "Wyłącz konto usługi" + "title-disable-service-account": "Wyłącz konto usługi", + "body-delete_one": "", + "body-delete_few": "", + "body-delete_many": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Token stracił ważność", @@ -11005,6 +11488,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Eksportowanie jako JSON", "share-externally-title": "Udostępnij na zewnątrz", "share-internally-title": "Udostępnij wewnętrznie", @@ -11040,6 +11524,19 @@ "view-button": "Widok JSON", "view-button-yaml": "Wyświetl kod YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Utwórz panel biblioteki." }, @@ -11048,7 +11545,7 @@ "info-text": "Utwórz bezpośredni link do tego pulpitu lub panelu, dostosowany za pomocą poniższych opcji.", "link-url": "Link URL", "render-alert": "Nie zainstalowano wtyczki renderowania obrazów", - "render-instructions": "Aby wyrenderować obraz panelu, musisz zainstalować wtyczkę <2>renderowania obrazów w usłudze Grafana. Skontaktuj się z administratorem usługi Grafana, aby zainstalować wtyczkę.", + "render-instructions": "", "rendered-image": "Obraz renderowany z bezpośredniego linku", "save-alert": "Nie zapisano pulpitu", "save-dashboard": "Aby wyrenderować obraz panelu, musisz najpierw zapisać pulpit.", @@ -11116,6 +11613,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Podgląd panelu" }, @@ -11383,7 +11886,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Zbyt wiele punktów, aby można było prawidłowo je zwizualizować. <1>Zaktualizuj zapytanie, aby zwróciło mniej punktów. <3>(Otrzymano {{count}} punkt)", + "too-many-points_one": "Zbyt wiele punktów, aby można było prawidłowo je zwizualizować. <1>Zaktualizuj zapytanie, aby zwróciło mniej punktów. <3>(Otrzymano {{count}} punktu)", "too-many-points_few": "Zbyt wiele punktów, aby można było prawidłowo je zwizualizować. <1>Zaktualizuj zapytanie, aby zwróciło mniej punktów. <3>(Otrzymano {{count}} punkty)", "too-many-points_many": "Zbyt wiele punktów, aby można było prawidłowo je zwizualizować. <1>Zaktualizuj zapytanie, aby zwróciło mniej punktów. <3>(Otrzymano {{count}} punktów)", "too-many-points_other": "Zbyt wiele punktów, aby można było prawidłowo je zwizualizować. <1>Zaktualizuj zapytanie, aby zwróciło mniej punktów. <3>(Otrzymano {{count}} punktu)" @@ -11458,6 +11961,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Alternatywny tekst wyświetlany, jeśli obraz nie może być wyświetlony, lub w przypadku użytkowników korzystających z czytnika ekranu", "description-title-text": "Tekst wyświetlany po najechaniu kursorem na obraz", @@ -11478,6 +11982,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11528,6 +12033,7 @@ "tag-option-label": "Opcja tagu" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Selektor zespołu", "select-placeholder": "Wybierz zespół" }, @@ -11829,6 +12335,18 @@ "placeholder-fields-or-number": "Pola lub liczby" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Tryb", "label-replace-all-fields": "Zastąp wszystkie pola" @@ -11843,6 +12361,13 @@ "concatenate-transformer-editor": { "label-label": "Etykieta", "label-name": "Nazwa", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Ramka" }, "config-from-query-transformer-editor": { @@ -11853,6 +12378,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Dodaj transformator typu Konwersja pól", "aria-label-remove-convert-field-type-transformer": "Usuń transformator typu Konwersja pól", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Przeglądarka", "utc": "UTC" @@ -11863,7 +12392,13 @@ "label-input-format": "Format danych wejściowych", "label-join-with": "Połącz za pomocą", "label-set-timezone": "Ustaw strefę czasową", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Typ", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Podczas łączenia wartości tablic należy używać jawnego separatora", "tooltip-input-format": "Określ format pola wejściowego, aby usługa Grafana mogła poprawnie przetwarzać ciąg daty.", "tooltip-specify-the-output-format": "Określ format danych wyjściowych.", @@ -11885,6 +12420,19 @@ "desc": "", "off": "Wył." }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Dodaj wartość enum", "generate-enum-values-from-data": "Wygeneruj wartości enum z danych" @@ -11895,11 +12443,22 @@ "remove-enum-row-tooltip-delete": "Usuń" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Znak rozdzielający", "label-format": "Format", "label-keep-time": "Rejestruj czas", "label-replace-all-fields": "Zastąp wszystkie pola", "label-source": "Źródło", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automatycznie", "placeholder-select-delimiter": "Wybierz znak rozdzielający…", "tooltip-regexp": "Przykład: {{regexExample}}" @@ -11908,8 +12467,22 @@ "aria-label-threshold-color": "Kolor progu" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Pole", - "label-lookup": "Wyszukiwanie" + "label-lookup": "Wyszukiwanie", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Ustawienia dodatkowe", @@ -11918,12 +12491,26 @@ "use-as": "Użyj jako" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Ze zmiennej", "label-identifier": "Identyfikator", "label-use-variable": "Użyj zmiennej", "label-variable": "Zmienna", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Wzorzec wyrażenia regularnego" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Pole", "label-match": "Dopasowanie", @@ -11933,14 +12520,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Dodaj warunek", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Warunki", - "label-filter-type": "Typ filtra" + "label-filter-type": "Typ filtra", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Pole", "label-format": "Format", "label-substring-range": "Zakres ciągu podrzędnego" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Ustaw strefę czasową", @@ -11948,6 +12569,70 @@ "tooltip-format": "Format wyjściowy pola określonego jako <2>Ciąg formatowania Moment.js.", "tooltip-timezone-manually": "Ręcznie ustaw strefę czasową daty" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Przeglądarka", @@ -11975,22 +12660,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorowane", "placeholder-select-stats": "Wybierz statystyki" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Obliczenia nie będą miały żadnego wpływu, jeśli nie zostaną pogrupowane żadne pola" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Jeśli ta opcja jest włączona, w tabelach zagnieżdżonych nazwy pól będą wyświetlać się jako nagłówki tabeli", "label-show-field-names-in-nested-tables": "Pokaż nazwy pól w tabelach zagnieżdżonych", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Obliczenia nie będą miały żadnego wpływu, jeśli nie zostaną pogrupowane żadne pola." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Wartość komórki", "label-column": "Kolumna", "label-empty-value": "Pusta wartość", "label-row": "Wiersz", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Wartość logiczna Fałsz", @@ -12009,6 +12718,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automatycznie", "placeholder-default": "Domyślnie: 30", "placeholder-none": "Brak" @@ -12017,7 +12732,16 @@ "label-as-percentile": "Jako percentyl", "tooltip-transform-index-percentile": "Przekształć indeks wiersza jako percentyl." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Dane wejściowe nie zawierają żadnych etykiet", "no-input-or-labels-found": "Nie znaleziono danych wejściowych (lub etykiet)", @@ -12039,10 +12763,30 @@ "label-labels": "Etykiety", "label-mode": "Tryb", "label-value-field-name": "Nazwa pola wartości", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opcjonalnie) Wybierz etykietę", "tooltip-replace-value-field-label": "Zastąp nazwę pola wartości etykietą" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Wartość lub zmienna" }, "long-info": { @@ -12053,7 +12797,13 @@ "string-values-separate": "Wartości typu ciąg znaków są reprezentowane jako osobne pola, a nie jako etykiety" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Scalanie nie ma żadnych skutków po zastosowaniu do pojedynczej ramki." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Scalanie nie ma żadnych skutków po zastosowaniu do pojedynczej ramki.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Wszystkie wartości są liczbowe", @@ -12063,10 +12813,16 @@ "time-in-ascending-order": "Czas w porządku rosnącym" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automatycznie", "field-order-manual": "", - "first-frame-warning": "Organizowanie pól działa tylko w przypadku pojedynczej ramki. Rozważ najpierw zastosowanie transformacji połączenia lub odfiltrowanie danych wejściowych." + "first-frame-warning": "Organizowanie pól działa tylko w przypadku pojedynczej ramki. Rozważ najpierw zastosowanie transformacji połączenia lub odfiltrowanie danych wejściowych.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Ustawia sposób wyświetlania nazw wybranych pól. Nazwa ramki jest zwykle lepsza w przypadku danych tabelarycznych" @@ -12091,9 +12847,41 @@ "partition-values-works-single-frame": "Podział według wartości działa tylko w przypadku pojedynczej ramki.", "tooltip-keeps-partition-fields-frames": "Zachowuje pola podziału w ramkach" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Informacje" + "label-info": "Informacje", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "oraz", @@ -12105,6 +12893,9 @@ "label-operation": "Operacja" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Obliczenia", "label-include-time": "Uwzględnij czas", "label-labels-to-fields": "Etykiety do pól", @@ -12119,12 +12910,23 @@ "series-to-rows": "Szeregi do wierszy" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Wybierz statystyki" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Wartość lub zmienna", "variable-regex-documentation": "Formatuje zmienną wielowartościową do postaci ciągu wyrażenia regularnego" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Sześcienna", @@ -12146,6 +12948,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Liczba punktów X,Y do prognozowania" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Dopasowanie", "label-replace": "Zastąp", @@ -12154,11 +12964,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Pole", - "label-mode": "Tryb" + "label-mode": "Tryb", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Pole", "label-reverse": "Odwróć", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Wybierz pole" }, "supplier": { @@ -12178,6 +13014,17 @@ "transform": "Przekształć" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Statystyka", "label-time-field": "Pole czasu", @@ -12191,10 +13038,34 @@ "placeholder-value": "Wartość", "tooltip-name-for-value-fields": "Nazwa pól wartości" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Operacja", "placeholder-field": "Pole" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Wiele pól wartości dowolnego typu", "single-frame": "Pojedyncza ramka", @@ -12247,6 +13118,7 @@ "title": "Organizacje" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Selektor użytkownika", "select-placeholder": "Zacznij pisać, aby wyszukać użytkownika" }, @@ -12332,6 +13204,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Usuń zmienną" }, "create-ad-hoc-variable-adapter": { @@ -12380,9 +13254,34 @@ "label-refresh": "Odśwież" }, "query-variable-sort-select": { - "description-values-variable": "Jak sortować wartości tej zmiennej" + "description-values-variable": "Jak sortować wartości tej zmiennej", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "wartość domyślna, jeśli istnieje", "text-options": "Opcje tekstu" }, @@ -12411,6 +13310,8 @@ "description-optional-display-name": "Opcjonalna nazwa wyświetlana", "description-template-variable-characters": "Nazwa zmiennej szablonu. (maks. 50 znaków)", "general": "Ogólne", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Tekst opisowy", "placeholder-label-name": "Nazwa etykiety", "placeholder-variable-name": "Nazwa zmiennej", @@ -12425,9 +13326,15 @@ "tooltip-duplicate-variable": "Duplikuj zmienną", "tooltip-remove-variable": "Usuń zmienną" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Przełącz wszystkie wartości" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Wyświetl użycie" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 4a068661cc7..a32d1d42168 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Alguns recursos estão estáveis (GA) e habilitados por padrão, enquanto outros estão atualmente em fase Beta preliminar, disponíveis para acesso antecipado.", "confirm-modal-body-2": "Aconselhamos que você entenda o que cada mudança de recurso pode causar antes de fazer alterações.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Disponibilidade geral", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Excluir org.", + "confirmText-delete": "", "title-delete": "Excluir" }, "anon-users": { "not-found": "Nenhum usuário anônimo encontrado." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Forçar desconexão de todos os dispositivos" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Você não tem permissão para ver usuários nesta organização. Para atualizar esta organização, entre em contato com o administrador do servidor.", "heading": "Editar Organização", @@ -208,9 +216,11 @@ "not-editable": "Não é possível editar a função deste usuário porque ela está sincronizada com seu provedor de autenticação. Consulte os <1>Documentos de autenticação da Grafana para saber mais." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Função" }, + "confirmText-delete": "", "delete-aria-label": "Excluir usuário: {{name}}", "title-delete": "Excluir" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Essas configurações do sistema são definidas em grafana.ini ou custom.ini (ou substituídas em variáveis ENV). Para alterá-las, você precisa reiniciar o Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licença Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Alterar", + "confirmText-change": "", "grafana-admin-key": "Administrador da Grafana", "grafana-admin-no": "Não", "grafana-admin-yes": "Sim", "title": "Permissões" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Excluir usuário", "disable-button": "Desativar usuário", "edit-button": "Editar", @@ -312,6 +330,9 @@ "title-delete-user": "Excluir usuário", "title-disable-user": "Desativar usuário" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Navegador e sistema operacional", "force-logout-all-button": "Forçar desconexão de todos os dispositivos", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Ativar cronogramas" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Adicionar mais" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Silenciamento, agrupamento e cronogramas (opcional)", "title-muting-grouping-and-timings": "Silenciamento, agrupamento e cronogramas" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Copiar link", "duplicate": "Duplicar", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Editar YAML", @@ -546,6 +577,7 @@ "view-configuration": "Ver configuração" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "A configuração interna do Grafana Alertmanager não pode ser alterada manualmente. Para alterar essa configuração, edite os recursos individuais por meio da interface do usuário.", "gma-manual-configuration-is-not-supported": "Não se permite fazer alterações de configuração manual", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Redefinindo a configuração do Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Comparar", "restore": "Restaurar", "text-latest": "Mais recentes" }, + "confirmText-yes-restore-configuration": "", "loading": "Carregando...", "no-previous-configurations": "Sem configurações anteriores", "this-might-take-a-while": "Isso pode demorar um pouco…", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Mais ações para o ponto de contato \"{{contactPointName}}\"", + "ariaLabel-delete": "", "button-edit": "Editar", "button-view": "Visualizar", + "export-ariaLabel-export": "", "export-label-export": "Exportar", "label-delete": "Excluir", "label-manage-permissions": "Gerenciar permissões", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Desativar mensagem resolvida" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Deve ser um número inteiro positivo.", "must-enter-a-group-name": "É necessário inserir um nome de grupo" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Erro ao carregar detalhes da fonte de dados", "edit": "Editar", - "evaluations-to-fire": "Ciclos de avaliação para acionar", "export": "Exportar", "folder": "Pasta", "group-loading-error": "Erro ao carregar o grupo", "interval": "Intervalo", "namespace": "Nomenclatura", - "pending-period": "Período pendente", - "recording": "Registrando", - "rule-name": "Nome da regra" + "new": "" }, "group-edit": { "ds-error": "Erro ao carregar detalhes da fonte de dados", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Primeiros passos", "label-insights": "Insights", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alertas" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Outras fontes de dados" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Desativado", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "A árvore de políticas de notificação foi atualizada por outro usuário.", "error-code": "Mensagem de erro: \"{{error}}\"", - "fallback": "Ocorreu um erro ao atualizar suas políticas de notificação.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Atualize a página e tente novamente.", - "title": "Erro ao salvar a política de notificação" + "title": "" }, "n-more-policies_one": "{{count}} política adicional", "n-more-policies_other": "{{count}} políticas adicionais" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Adicionar consulta", "body-queries-expressions-configured": "Crie pelo menos uma consulta ou expressão para receber alertas", + "confirmText-deactivate": "", "expressions": "Expressões", "loading-data-sources": "Carregando fontes de dados…", "manipulate-returned-queries-other-operations": "Manipule os dados que retornaram das consultas usando operações matemáticas e outras operações.", @@ -2184,6 +2237,9 @@ "label-export-all": "Exportar tudo" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Não foi possível carregar o editor de consultas devido a: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Você precisará definir um novo grupo de avaliação para a regra copiada, pois o original foi provisionado e não pode ser usado para regras criadas na interface do usuário.", "body-not-provisioned": "A nova regra <1>não será marcada como uma regra provisionada.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Copiar regra de alerta provisionada" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Inspecionar regra de alerta" }, "rule-list": { - "cannot-find-rule-details-for": "Não é possível encontrar detalhes da regra para o UID {{uid}}", - "cannot-load-rule-details-for": "Não é possível carregar detalhes da regra para o UID {{uid}}", "configure-datasource": "Configuração", "draft-new-rule": "Crie o rascunho de uma nova regra", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Não está encontrando a fonte de dados que você procura? Algumas fontes de dados não são compatíveis com alertas. Clique no ícone para mais informações." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Adicionar novo Alertmanager", "builtin-alertmanager": "Alertmanager integrado", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Escolha o modelo de notificação", "loading": "Carregando...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Selecionar modelo de notificação" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Ações", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Nenhum modelo definido.", "template-group": "Grupo de modelos", "title-delete-template-group": "Excluir grupo de modelos" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "(há {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "A pasta de destino não está vazia; algumas regras podem ser substituídas ou removidas. Tem certeza de que deseja importar essas regras de alerta para as regras gerenciadas pela Grafana?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Excluir ponto de contato" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Excluir política de notificação" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Anotação", "first-value": "Primeiro valor", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Adicionar consulta de anotação", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Consulte a <2>Documentação de anotações para saber mais.", "title": "Nenhuma consulta de anotações personalizadas foi adicionada ainda" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} eventos (de {{numFields}} campos)", "no-events-found": "Nenhum evento encontrado", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Definir IDs das equipes permitidas", "display-name-description": "Será exibido na página de login como \"Entrar com...\". É uma opção conveniente caso você use mais de um provedor de identidade ou protocolos SSO.", "display-name-label": "Nome de exibição", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nome da chave a ser usada para a consulta de e-mail do usuário no mapa de atributos do token de ID OAuth2.", "email-attribute-name-label": "Nome do atributo de e-mail", "email-attribute-path-description": "Expressão JMESPath a ser usada para a consulta de e-mail do usuário a partir das informações do usuário.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Lista de números inteiros de IDs de equipe.", "team-ids-label": "IDs de equipe", "team-ids-numbers": "Os IDs de equipe devem ser números.", - "team-ids-other": "Lista de strings de IDs de equipe.", + "team-ids-other": "", "team-ids-placeholder": "Insira os IDs de equipe e pressione Enter para adicioná-los", "teams-url-description": "A URL usada para consultar IDs de equipe. Se não for definido, o valor padrão é /teams.", "teams-url-description-oauth": "Se você configurar \"{{ teamsURLLabel }}\", também será necessário configurar \"{{ teamIDsAttributePathLabel }}\".", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Redefinir para os valores padrão" }, + "confirmText-reset": "", "disable": "Desativar", "disabling": "Desabilitando...", "discard": "Descartar", @@ -3473,11 +3563,11 @@ "button-create": "Criar", "button-creating": "Criando…", "cancel": "Cancelar", - "error-invalid-folder-name": "Nome da pasta inválido", "error-required": "O nome da pasta é obrigatório", "folder-name-input-placeholder-enter-folder-name": "Digitar o nome da pasta", "label-folder-name": "Nome da pasta", "text-pull-request-created": "Uma solicitação de extração foi criada com alterações nesta pasta:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Solicitação de extração criada", "title-this-repository-is-read-only": "Este repositório é somente leitura" }, @@ -3895,6 +3985,7 @@ "tooltip": "Fechar" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Ocorreu um erro ao carregar as opções." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Calcula dinamicamente o intervalo dividindo o intervalo de tempo pela contagem especificada" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Deseja prosseguir para o site externo?" } }, "render-left-actions": { - "text-public": "Pública", "tooltip-view-as-scene": "Visualizar como cena" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Falha ao carregar o painel de controle" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Falha ao buscar o painel" @@ -4540,6 +4641,13 @@ "editable": "Editável", "readonly": "Somente leitura" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Transformar dados" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Tem certeza de que deseja restaurar o painel para a versão {{version}}? Todas as alterações não salvas serão perdidas.", + "confirmText-restore-version": "", "title-restore-version": "Restaurar versão" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Este título não é exclusivo" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Salvar como" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Já existe um painel com o mesmo nome na pasta selecionada.<1><2>Deseja salvar este painel mesmo assim?", "body-version-mismatch": "Outra pessoa atualizou este painel<1><2>Deseja salvar este painel mesmo assim?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Conflito", "title-version-mismatch": "Conflito" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Ir para o próximo painel de controle", "playlist-previous": "Ir para o painel de controle anterior", "playlist-stop": "Parar lista de reprodução", - "public-dashboard": "Público", "refresh": "Atualizar painel de controle", "save": "Salvar painel de controle", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Aplicar transformação a" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Depurar", "title-disable-transformation": "Desativar transformação", "title-filter": "Filtro", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Pesquisar por transformação", "show-images": "Exibir imagens", - "title-add-another-transformation": "Adicionar outra transformação" + "title-add-another-transformation": "Adicionar outra transformação", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Adicionar outra transformação" }, + "confirmText-delete-all": "", "delete-all-transformations": "Excluir todas as transformações", "title-delete-all-transformations": "Deseja excluir todas as transformações?", "tooltip-clear-search": "Limpar pesquisa", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alternar seleção da versão {{version}}", "date": "Data", + "name-latest": "", "notes": "Observações", "restore": "Restaurar", "updated-by": "Atualizado por", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Permite que os usuários adicionem valores personalizados à lista", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Forneça dimensões como CSV: {{name}}, {{value}}", "label-data-source": "Fonte de dados", - "label-use-static-key-dimensions": "Usar dimensões de chave estática" + "label-use-static-key-dimensions": "Usar dimensões de chave estática", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Revogar URL público" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Opções personalizadas", + "name-values-separated-comma": "", "selection-options": "Opções de seleção" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Tipo", "label-url": "URL", "label-with-tags": "Com tags", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Abrir o painel" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "O valor ainda não está salvo no banco de dados da Grafana", "not-yet-saved": "O valor não está salvo no banco de dados da Grafana", - "open-pull-request-in-git-hub": "Abrir solicitação de pull no GitHub", - "title-dashboard-loaded-branch-git-hub": "Este painel de controle é carregado a partir de um branch no GitHub.", "title-dashboard-loaded-external-repository": "Este painel de controle é carregado a partir de um repositório externo", - "title-dashboard-loaded-request-git-hub": "Este painel de controle é carregado a partir de uma solicitação de pull no GitHub.", - "title-error-loading-dashboard": "Erro ao carregar o painel de controle", - "value-not-saved": "O valor ainda não está salvo no banco de dados da Grafana", - "view-pull-request-in-git-hub": "Ver solicitação de pull no GitHub" + "title-error-loading-dashboard": "Erro ao carregar o painel de controle" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Opções de fonte de dados", "description-instance-name-filter": "Filtro Regex para as instâncias de fonte de dados a serem escolhidas na lista de valores de variáveis. Deixe em branco para todos.", "example-instance-name-filter": "Exemplo: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Opções de seleção" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Adicionar transformação" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Opções de coluna", "row-height-options": "Opções de altura da linha" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Forneça dimensões como CSV: {{name}}, {{value}}", "group-by-options": "Agrupar por opções", "label-data-source": "Fonte de dados", - "label-use-static-group-by-dimensions": "Usar dimensões de grupo estático" + "label-use-static-group-by-dimensions": "Usar dimensões de grupo estático", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Copiar para a área de transferência", @@ -5521,9 +5710,14 @@ "apply": "Aplicar" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "O valor calculado não ficará abaixo desse limite", "description-step-count": "Quantas vezes o intervalo de tempo atual deve ser dividido para calcular o valor", - "interval-options": "Opções de intervalo" + "interval-options": "Opções de intervalo", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Usado em {{count}} painéis", "usage-count_other": "Usado em {{count}} painéis" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Já existe um painel com o mesmo nome na pasta selecionada. Deseja salvar este painel mesmo assim?", "title-name-already-exists": "O nome já existe" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Deseja prosseguir para o site externo?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Adicionar outra transformação", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Excluir todas as transformações", "title-delete-all-transformations": "Deseja excluir todas as transformações?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Os grupos de captura nomeados podem ser usados para separar o texto e o valor de exibição (<1>ver exemplos).", "description-optional": "Opcional, se você quiser extrair parte de um nome de série ou segmento de node de métrica.", "label-data-source": "Fonte de dados", + "label-static-options-sort": "", "label-target-data-source": "Fonte de dados de destino", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opções de consulta", - "selection-options": "Opções de seleção" + "selection-options": "Opções de seleção", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Tem certeza de que deseja restaurar o painel para a versão {{version}}? Todas as alterações não salvas serão perdidas.", + "confirmText-restore-version": "", "title-restore-version": "Restaurar versão" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Pasta", "render-footer": { "title-failed-to-save-dashboard": "Falha ao salvar o painel" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Permite que vários valores sejam selecionados ao mesmo tempo", "description-enables-option-include-variables": "Ativa uma opção para incluir todos os valores", - "description-enables-users-custom-values": "Permite que os usuários adicionem valores personalizados à lista" + "description-enables-users-custom-values": "Permite que os usuários adicionem valores personalizados à lista", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Alternar menu de compartilhamento" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Falha ao copiar para a área de transferência" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opcional)", "text-options": "Opções de texto" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Tem certeza de que deseja desvincular este painel?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Esta variável não é referenciada por nenhuma variável ou painel.", "aria-label-variable-referenced-other-variables-dashboard": "Esta variável é referenciada por outras variáveis ou painel.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulário do editor de variáveis", "back-to-list": "Voltar para a lista", + "confirmText": { + "delete-variable": "" + }, "delete": "Excluir", "description-optional-display-name": "Nome de exibição opcional", "description-template-variable-characters": "O nome da variável do modelo. (Até 50 caracteres)", "general": "Geral", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texto descritivo", "placeholder-label-name": "Nome do rótulo", "placeholder-variable-name": "Nome da variável", @@ -5829,13 +6066,25 @@ "variable": "Variável" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Excluir variável", "tooltip-duplicate-variable": "Duplicar variável", "tooltip-remove-variable": "Remover variável" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Ocultar" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Mostrando usos para: {{variableId}}", "tooltip-show-usages": "Exibir usos" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alternar seleção da versão {{version}}", "date": "Data", + "name-latest": "", "notes": "Observações", "restore": "Restaurar", "updated-by": "Atualizado por", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Carregar" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limite", "label-value": "Valor", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Máx.", "label-min": "Mín.", - "label-value": "Valor" + "label-value": "Valor", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Selecionar operador de nome do serviço", "aria-label-select-span-name": "Selecionar nome do intervalo", "aria-label-select-span-name-operator": "Selecione o operador de nome de intervalo", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtros de intervalo", "label-duration": "Duração", "label-service-name": "Nome do serviço", @@ -6939,6 +7199,8 @@ "split-widen": "Painel amplo" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Dar feedback", "label-copied": "Copiado!", "label-export": "Exportar", @@ -7046,6 +7308,7 @@ "when": "QUANDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Operação" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Limpar pastas", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtro de pasta", "select-placeholder": "Filtrar por pasta" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Não foi possível concluir sua solicitação. Tente novamente.", "send-custom-feedback": "Enviar" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Latitude", "label-longitude": "Longitude" @@ -7153,6 +7463,14 @@ "center": "Centro:", "zoom": "Zoom:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Camada" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Adicionar regra de estilo de mapa geográfico" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Adicionar camada", "no-layers": "Não há camadas?" @@ -7185,16 +7511,38 @@ "label-zoom": "Zoom", "use-current-map-settings": "Usar as configurações atuais do mapa" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Símbolo" }, "measure-overlay": { "tooltip-show-measure-tools": "Exibir ferramentas de medição" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "A camada do mapa base é configurada pelo administrador do servidor." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Alinhar", "label-baseline": "Referência", "label-color": "Cor", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Alinhamento vertical do símbolo", "label-text-label": "Rótulo do texto", "label-x-offset": "Desvio em X", - "label-y-offset": "Desvio em Y" + "label-y-offset": "Desvio em Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operador de comparação", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Propriedade do recurso", "placeholder-numeric-value": "Valor numérico", "placeholder-value": "valor" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "cor {{colorLabel}}" }, "confirm-button": { - "cancel": "Cancelar" + "cancel": "Cancelar", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Digite \"{{confirmPromptText}}\" para confirmar" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "alternar recolher painel", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Cancelar consulta", "tooltip-cancel-loading": "Cancelar consulta", "tooltip-stop-streaming": "Parar streaming", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Diferenciar maiúsculas/minúsculas", "inspect-drawer-title": "Inspecionar valor", "inspect-menu-label": "Inspecionar valor", + "nested-table": { + "no-data": "" + }, "no-values-label": "Sem valores", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Adicionar", + "placeholder-new-tag": "", "remove": "Remover etiqueta: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Clique para {{actionTitle}}", "footer-click-to-navigate": "Clique para abrir {{linkTitle}}", "timestamp": "Registro de data/hora" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Criar painel de biblioteca" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Deseja excluir este painel?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Usado em {{count}} painéis" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Pesquisar por nome ou descrição" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Carregando painel da biblioteca…" @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Crie um link direto e personalizado para compartilhar seu painel dentro da sua organização com as seguintes configurações de personalização:", "download-image": "Baixar imagem", - "render-image": "Gerar imagem", - "render-image-error": "Falha ao renderizar a imagem do painel", - "render-image-error-description": "Ocorreu um erro ao gerar a imagem" + "render-image": "Gerar imagem" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Aplicar quebra de linha" }, "log-details": { - "close": "Fechar detalhes do log", "fields": "Campos", "links": "Links", "log-line": "Linha de log", @@ -8405,6 +8795,42 @@ "show-more": "exibir mais", "tooltip-error": "Erro: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Copiar link para a linha do log", "copy-log": "Copiar linha do log", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Links de dados", "aria-label-fields": "Campos", + "aria-label-line": "", "aria-label-log-level": "Nível de log", "aria-label-no-details": "Sem detalhes" }, @@ -8598,6 +9030,8 @@ "updated-on": "Atualizado em" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Excluir" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Importar painel de controle" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Esta ferramenta pode migrar alguns recursos desta instalação para a sua pilha da nuvem. Para começar, você precisará criar uma captura desta instalação. A criação de uma captura normalmente leva menos de dois minutos. A captura é armazenada juntamente com esta instalação da Grafana.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "O painel de controle já está provisionado e é gerenciado pela Grafana na instância na nuvem. Recomendamos que você passe a usar o painel de controle provisionado. Se você ainda quiser copiar o painel de controle para a instância na nuvem, altere o ID do painel de controle no JSON do painel de controle, salve uma nova captura e envie novamente.", "datasource-already-managed": "A fonte de dados já está provisionada e é gerenciada pela Grafana na instância na nuvem. Caso se trate de outro recurso, defina outro UID e tente novamente.", "datasource-invalid-url": "Há uma fonte de dados com uma URL inválida. Forneça uma URL válida e tente novamente.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": "> 100 nós", "aria-label-hidden-marker": "Marcador de nós ocultos: {{marker}}", - "node-count_one": "{{count}} nó", + "node-count_one": "{{count}} nós", "node-count_other": "{{count}} nós" }, "node": { @@ -9318,9 +9763,9 @@ "aria-label-nodes-hidden-warning": "Aviso de nós ocultos", "computing-layout": "Layout de computação", "no-data": "Sem dados", - "hidden-nodes_one": "<0> {{count}} nó está oculto devido ao desempenho.", + "hidden-nodes_one": "<0> {{count}} nós estão ocultos devido ao desempenho.", "hidden-nodes_other": "<0> {{count}} nós estão ocultos devido ao desempenho.", - "processed-nodes_one": "<0> O layout em camadas pode ser lento com {{count}} nó.", + "processed-nodes_one": "<0> O layout em camadas pode ser lento com {{count}} nós.", "processed-nodes_other": "<0> O layout em camadas pode ser lento com {{count}} nós." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Selecionar organização" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Você não tem permissão para visualizar esta página.", "title-access-denied": "Acesso negado" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Nenhum componente de página do aplicativo de origem foi encontrado" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "plug-ins atualizados" }, "versions": { - "confirmation-text-1": "Tem certeza de que deseja fazer o downgrade da versão", - "confirmation-text-2": "Não é esperado que você faça isso", + "confirmation-text": "", "downgrade-confirm": "Fazer downgrade", "downgrade-title": "Fazer downgrade da versão do plug-in" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Nenhum plug-in encontrado" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Atualizando" }, "install-controls-button": { - "title-uninstall-modal": "Desinstalar {{plugin}}" + "title-uninstall-modal": "Desinstalar {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Este plug-in não está publicado em <2>grafana.com/plugins e não pode ser gerenciado por meio do catálogo.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Este recurso está em desenvolvimento ativo no momento. Para ter uma melhor experiência e conferir as melhorias mais recentes, recomendamos usar a <2>compilação noturna da Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Seletor de conta de serviço", "select-placeholder": "Comece a digitar para pesquisar contas de serviço" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Adicionar token da conta de serviço", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Excluir conta de serviço", "disable-service-account": "Desativar conta de serviço", "enable-service-account": "Ativar conta de serviço", @@ -10907,6 +11375,7 @@ "used-by": "Usado por" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Editar" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Adicionar conta de serviço", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Pesquisar conta de serviço por nome", "sub-title": "As contas de serviço e seus tokens podem ser usados para autenticação na API da Grafana. Saiba mais na nossa <2>documentação.", "title-delete-service-account": "Excluir conta de serviço", - "title-disable-service-account": "Desativar conta de serviço" + "title-disable-service-account": "Desativar conta de serviço", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Este token expirou", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exportar como JSON", "share-externally-title": "Compartilhar externamente", "share-internally-title": "Compartilhar internamente", @@ -10972,6 +11448,19 @@ "view-button": "Visualizar JSON", "view-button-yaml": "Visualizar YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Criar painel de biblioteca." }, @@ -10980,7 +11469,7 @@ "info-text": "Crie um link direto para este painel de controle ou painel, personalizado com as opções abaixo.", "link-url": "URL do link", "render-alert": "Plug-in renderizador de imagem não instalado", - "render-instructions": "Para renderizar uma imagem de painel, você precisa instalar o <2>plug-in renderizador de imagens do Grafana. Entre em contato com o administrador da Grafana para instalar o plug-in.", + "render-instructions": "", "rendered-image": "Link direto de imagem renderizada", "save-alert": "O painel de controle não está salvo", "save-dashboard": "Para renderizar uma imagem do painel, você deve salvar o painel de controle primeiro.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Visualização do painel" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Há um número excessivo de pontos para que haja uma visualização adequada. <1>Atualize a consulta para retornar menos pontos. <3>({{count}} ponto recebido)", + "too-many-points_one": "Há um número excessivo de pontos para que haja uma visualização adequada. <1>Atualize a consulta para retornar menos pontos. <3>({{count}} pontos recebidos)", "too-many-points_other": "Há um número excessivo de pontos para que haja uma visualização adequada. <1>Atualize a consulta para retornar menos pontos. <3>({{count}} pontos recebidos)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Texto alternativo que será exibido se uma imagem não puder ser exibida ou para usuários que usam leitor de tela", "description-title-text": "Texto que será exibido ao passar o cursor sobre a imagem", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Opção de tag" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Seletor de equipe", "select-placeholder": "Selecione uma equipe" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Campo(s) ou número" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Apelido", "label-mode": "Modo", "label-replace-all-fields": "Substituir todos os campos" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Etiqueta", "label-name": "Nome", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Quadro" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Adicionar um transformador de tipo de campo de conversor", "aria-label-remove-convert-field-type-transformer": "Remover transformador de tipo de campo de conversor", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Navegador", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Formato de entrada", "label-join-with": "Unir com", "label-set-timezone": "Definir fuso horário", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Tipo", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Use um separador explícito ao unir valores de matriz", "tooltip-input-format": "Especifique o formato do campo de entrada para que a Grafana possa analisar a string de data corretamente.", "tooltip-specify-the-output-format": "Especifique o formato de saída.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Desativar" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Adicionar valor de enumeração", "generate-enum-values-from-data": "Gerar valores enumerados a partir de dados" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Excluir" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Delimitador", "label-format": "Formato", "label-keep-time": "Manter horário", "label-replace-all-fields": "Substituir todos os campos", "label-source": "Fonte", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automático", "placeholder-select-delimiter": "Selecione o delimitador…", "tooltip-regexp": "Exemplo: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Cor do limite" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", - "label-lookup": "Consulta" + "label-lookup": "Consulta", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Configurações adicionais", @@ -11848,12 +12413,26 @@ "use-as": "Usar como" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Da variável", "label-identifier": "Identificador", "label-use-variable": "Usar variável", "label-variable": "Variável", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Padrão de expressão regular" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Campo", "label-match": "Corresponder", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Adicionar condição", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Condições", - "label-filter-type": "Tipo de filtro" + "label-filter-type": "Tipo de filtro", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", "label-format": "Formato", "label-substring-range": "Intervalo de substring" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formato", "label-set-timezone": "Definir fuso horário", @@ -11878,6 +12491,70 @@ "tooltip-format": "O formato de saída para o campo especificado como uma <2>sequência de caracteres de formato Moment.js.", "tooltip-timezone-manually": "Defina o fuso horário da data manualmente" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Navegador", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorado", "placeholder-select-stats": "Selecionar estatísticas" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Os cálculos não terão efeito se nenhum campo estiver sendo agrupado em" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Se ativadas, as tabelas aninhadas exibirão os nomes dos campos como um cabeçalho da tabela", "label-show-field-names-in-nested-tables": "Exibir nomes de campos em tabelas aninhadas", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Os cálculos não terão efeito se nenhum campo estiver sendo agrupado." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Valor da célula", "label-column": "Coluna", "label-empty-value": "Campo vazio", "label-row": "Linha", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Valor booleano falso", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automático", "placeholder-default": "Padrão: 30", "placeholder-none": "Nenhum" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Como percentil", "tooltip-transform-index-percentile": "Transforme o índice da linha como um percentil." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Os dados de entrada não contêm nenhum rótulo", "no-input-or-labels-found": "Nenhum dado de entrada (ou rótulo) foi encontrado", @@ -11969,10 +12685,30 @@ "label-labels": "Etiquetas", "label-mode": "Modo", "label-value-field-name": "Nome do campo de valor", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opcional) Selecionar rótulo", "tooltip-replace-value-field-label": "Substitua o nome do campo de valor por um rótulo" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Valor ou variável" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Os valores de string são representados como campos separados em vez de rótulos" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "A mesclagem não tem efeito quando aplicada em um único quadro." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "A mesclagem não tem efeito quando aplicada em um único quadro.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Todos os valores são numéricos", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Tempo em ordem crescente" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automático", "field-order-manual": "", - "first-frame-warning": "A organização de campos só funciona com um quadro único. Considere aplicar uma transformação de junção ou filtrar a entrada primeiro." + "first-frame-warning": "A organização de campos só funciona com um quadro único. Considere aplicar uma transformação de junção ou filtrar a entrada primeiro.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Define como os nomes dos campos selecionados são exibidos. O nome do quadro geralmente é melhor para dados tabulares" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "A partição por valores só funciona com um quadro único.", "tooltip-keeps-partition-fields-frames": "Mantém os campos de partição nos quadros" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formato", - "label-info": "Informações" + "label-info": "Informações", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "e", @@ -12035,6 +12815,9 @@ "label-operation": "Operação" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Cálculos", "label-include-time": "Incluir horário", "label-labels-to-fields": "Rótulos para campos", @@ -12049,12 +12832,23 @@ "series-to-rows": "Série para linhas" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Escolha a estatística" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Valor ou variável", "variable-regex-documentation": "Formata a variável de múltiplos valores em uma string de expressões regulares" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Cúbico", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Número de pontos X,Y a serem previstos" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Corresponder", "label-replace": "Substituir", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Campo", - "label-mode": "Modo" + "label-mode": "Modo", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Campo", "label-reverse": "Reverso", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Selecionar campo" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transformar" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Estatística", "label-time-field": "Campo de hora", @@ -12121,10 +12960,34 @@ "placeholder-value": "Valor", "tooltip-name-for-value-fields": "Nome para os campos de valor" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Operação", "placeholder-field": "Campo" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Vários campos de valor de qualquer tipo", "single-frame": "Quadro único", @@ -12177,6 +13040,7 @@ "title": "Organizações" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Seletor de usuário", "select-placeholder": "Comece a digitar para pesquisar um usuário" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Excluir variável" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Atualizar" }, "query-variable-sort-select": { - "description-values-variable": "Como classificar os valores desta variável" + "description-values-variable": "Como classificar os valores desta variável", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "valor padrão, se houver", "text-options": "Opções de texto" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Nome de exibição opcional", "description-template-variable-characters": "O nome da variável do modelo. (Até 50 caracteres)", "general": "Geral", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texto descritivo", "placeholder-label-name": "Nome do rótulo", "placeholder-variable-name": "Nome da variável", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Duplicar variável", "tooltip-remove-variable": "Remover variável" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Alternar todos os valores" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Exibir usos" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index b6aa70bb2b0..4054b5f6fe2 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Algumas funcionalidades são estáveis (GA) e ativadas por predefinição, enquanto outras estão atualmente na sua fase Beta preliminar, disponíveis para utilizar antecipadamente.", "confirm-modal-body-2": "Aconselhamos a compreensão das implicações de cada alteração de funcionalidade antes de efetuar modificações.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Disponibilidade geral", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Eliminar organização", + "confirmText-delete": "", "title-delete": "Eliminar" }, "anon-users": { "not-found": "Nenhum utilizador anónimo encontrado." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Forçar o encerramento de sessão em todos os dispositivos" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Não tem permissão para ver utilizadores desta organização. Para atualizar esta organização, contacte o administrador do servidor.", "heading": "Editar organização", @@ -208,9 +216,11 @@ "not-editable": "A função deste utilizador não é editável porque é sincronizada a partir do seu fornecedor de autenticação. Consulte os <1>documentos de autenticação da Grafana para obter detalhes." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Função" }, + "confirmText-delete": "", "delete-aria-label": "Eliminar utilizador: {{name}}", "title-delete": "Eliminar" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Estas definições de sistema são definidas em grafana.ini ou custom.ini (ou substituídas em variáveis ENV). Para alterar estas definições, tem de reiniciar a Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Licença Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Alterar", + "confirmText-change": "", "grafana-admin-key": "Administrador da Grafana", "grafana-admin-no": "Não", "grafana-admin-yes": "Sim", "title": "Permissões" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Eliminar utilizador", "disable-button": "Inativar utilizador", "edit-button": "Editar", @@ -312,6 +330,9 @@ "title-delete-user": "Eliminar utilizador", "title-disable-user": "Inativar utilizador" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Navegador e SO", "force-logout-all-button": "Forçar o encerramento de sessão em todos os dispositivos", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Tempos ativos" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Adicionar mais" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Silêncio, agrupamento e tempos (opcional)", "title-muting-grouping-and-timings": "Silêncio, agrupamento e tempos" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Copiar link", "duplicate": "Duplicar", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Editar YAML", @@ -546,6 +577,7 @@ "view-configuration": "Ver configuração" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "A configuração interna do Grafana Alertmanager não pode ser alterada manualmente. Para alterar esta configuração, edite os recursos individuais através da interface do utilizador.", "gma-manual-configuration-is-not-supported": "Alterações da configuração manual não suportadas", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "A redefinir a configuração do Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Comparar", "restore": "Restaurar", "text-latest": "Mais recentes" }, + "confirmText-yes-restore-configuration": "", "loading": "A carregar...", "no-previous-configurations": "Sem configurações anteriores", "this-might-take-a-while": "Isto pode demorar algum tempo...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Mais ações para o ponto de contacto \"{{contactPointName}}\"", + "ariaLabel-delete": "", "button-edit": "Editar", "button-view": "Ver", + "export-ariaLabel-export": "", "export-label-export": "Exportar", "label-delete": "Eliminar", "label-manage-permissions": "Gerir permissões", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Desativar mensagem resolvida" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Deve ser um número inteiro positivo.", "must-enter-a-group-name": "Deve inserir um nome de grupo" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Erro ao carregar a detalhes da origem dos dados", "edit": "Editar", - "evaluations-to-fire": "Ciclos de avaliação a disparar", "export": "Exportar", "folder": "Pasta", "group-loading-error": "Erro ao carregar o grupo", "interval": "Intervalo", "namespace": "Espaço de nome", - "pending-period": "Período pendente", - "recording": "Gravação", - "rule-name": "Nome da regra" + "new": "" }, "group-edit": { "ds-error": "Erro ao carregar a detalhes da origem dos dados", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Começar", "label-insights": "Conhecimentos", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Alerta" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Outras origens de dados" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Desativado", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "A árvore de políticas de notificação foi atualizada por outro utilizador.", "error-code": "Mensagem de erro: \"{{error}}\"", - "fallback": "Ocorreu um erro ao atualizar as suas políticas de notificação.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Por favor, atualize a página e tente novamente.", - "title": "Erro ao guardar a política de notificação" + "title": "" }, "n-more-policies_one": "{{count}} políticas adicionais", "n-more-policies_other": "{{count}} políticas adicionais" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Adicionar consulta", "body-queries-expressions-configured": "Criar pelo menos uma consulta ou expressão para receber alertas de", + "confirmText-deactivate": "", "expressions": "Expressões", "loading-data-sources": "A carregar origens de dados...", "manipulate-returned-queries-other-operations": "Manipule os dados devolvidos das consultas com operações matemáticas e outras.", @@ -2184,6 +2237,9 @@ "label-export-all": "Exportar tudo" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Não foi possível carregar o editor de consultas devido a: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Terá de definir um novo grupo de avaliação para a regra copiada, porque o original foi aprovisionado e não pode ser utilizado para regras criadas na interface do utilizador.", "body-not-provisioned": "A nova regra <1>não será marcada como uma regra aprovisionada.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Copiar regra de alerta aprovisionada" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Inspecionar regra de alerta" }, "rule-list": { - "cannot-find-rule-details-for": "Não é possível encontrar detalhes da regra para a UID {{uid}}", - "cannot-load-rule-details-for": "Não é possível encontrar detalhes da regra para a UID {{uid}}", "configure-datasource": "Configurar", "draft-new-rule": "Elabore o rascunho de uma nova regra", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Não encontra a origem de dados que pretende? Algumas origens de dados não são suportadas para alertas. Clique no ícone para obter mais informações." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Adicionar novo Alertmanager", "builtin-alertmanager": "Alertmanager integrado", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Escolher modelo de notificação", "loading": "A carregar...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Selecionar modelo de notificação" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Ações", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Nenhum modelo definido.", "template-group": "Grupo de modelos", "title-delete-template-group": "Eliminar grupo de modelos" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "(há {{time}})" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "A pasta de destino não está vazia, algumas regras podem ser substituídas ou removidas. Tem a certeza de que pretende importar estas regras de alerta para as regras geridas pela Grafana?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Eliminar ponto de contacto" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Eliminar política de notificação" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Anotação", "first-value": "Primeiro valor", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Adicionar consulta de anotação", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Consulte a <2>Documentação de anotações para obter mais informações.", "title": "Ainda não existem consultas de anotações personalizadas adicionadas" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} eventos (de {{numFields}} campos)", "no-events-found": "Nenhum evento encontrado", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Definir as ID das equipas permitidas", "display-name-description": "Será mostrado na página de início de sessão como \"Iniciar sessão com...\". Útil se utilizar mais de um fornecedor de identidade ou protocolos SSO.", "display-name-label": "Nome de exibição", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Nome da chave a utilizar para a pesquisa de e-mail do utilizador no mapa de atributos do token de ID OAuth2.", "email-attribute-name-label": "Nome do atributo de e-mail", "email-attribute-path-description": "Expressão JMESPath a utilizar para a pesquisa de e-mail do utilizador a partir das informações do utilizador.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Lista de números inteiros de ID de equipa.", "team-ids-label": "ID de equipa", "team-ids-numbers": "As ID de equipa têm de ser números.", - "team-ids-other": "Lista de cadeias de ID de equipa.", + "team-ids-other": "", "team-ids-placeholder": "Introduza ID de equipa e prima Enter para adicionar", "teams-url-description": "O URL utilizado para consultar as ID de equipa. Se não estiver definido, o valor predefinido é de /equipas.", "teams-url-description-oauth": "Se configurar \"{{ teamsURLLabel }}\", tem também de configurar \"{{ teamIDsAttributePathLabel }}\".", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Redefinir para os valores predefinidos" }, + "confirmText-reset": "", "disable": "Desativar", "disabling": "A desativar...", "discard": "Descartar", @@ -3473,11 +3563,11 @@ "button-create": "Criar", "button-creating": "A criar...", "cancel": "Cancelar", - "error-invalid-folder-name": "Nome da pasta inválido", "error-required": "O nome da pasta é obrigatório", "folder-name-input-placeholder-enter-folder-name": "Introduza o nome da pasta", "label-folder-name": "Nome da pasta", "text-pull-request-created": "Foi criado um pedido de extração com alterações a esta pasta:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pedido de extração criado", "title-this-repository-is-read-only": "Este repositório é apenas de leitura" }, @@ -3895,6 +3985,7 @@ "tooltip": "Fechar" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Ocorreu um erro ao carregar as opções." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Calcula dinamicamente o intervalo ao dividir o intervalo de tempo pela contagem especificada" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Continuar para o site externo?" } }, "render-left-actions": { - "text-public": "Público", "tooltip-view-as-scene": "Ver como cena" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Não foi possível carregar o painel de controlo" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Falha ao obter o painel de controlo" @@ -4540,6 +4641,13 @@ "editable": "Editável", "readonly": "Apenas de leitura" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Transformar dados" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Tem a certeza de que pretende restaurar o painel de controlo para a versão {{version}}? Todas as alterações não guardadas serão perdidas.", + "confirmText-restore-version": "", "title-restore-version": "Restaurar versão" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Este título não é único" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Guardar como" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Já existe um painel de controlo com o mesmo nome na pasta selecionada.<1><2>Ainda pretende guardar este painel de controlo?", "body-version-mismatch": "Outra pessoa atualizou este painel de controlo<1><2>Ainda pretende guardar este painel de controlo?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Conflito", "title-version-mismatch": "Conflito" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Ir para o próximo painel de controlo", "playlist-previous": "Ir para o painel de controlo anterior", "playlist-stop": "Parar a lista de reprodução", - "public-dashboard": "Público", "refresh": "Atualizar o painel de controlo", "save": "Guardar o painel de controlo", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Aplicar transformação a" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Depurar", "title-disable-transformation": "Desativar transformação", "title-filter": "Filtro", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Pesquisar por transformação", "show-images": "Mostrar imagens", - "title-add-another-transformation": "Adicionar outra transformação" + "title-add-another-transformation": "Adicionar outra transformação", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Adicionar outra transformação" }, + "confirmText-delete-all": "", "delete-all-transformations": "Eliminar todas as transformações", "title-delete-all-transformations": "Eliminar todas as transformações?", "tooltip-clear-search": "Limpar a pesquisa", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alternar seleção da versão {{version}}", "date": "Data", + "name-latest": "", "notes": "Observações", "restore": "Restaurar", "updated-by": "Atualizado por", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Permite que os utilizadores adicionem valores personalizados à lista", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Forneça dimensões como CSV: {{name}}, {{value}}", "label-data-source": "Origem dos dados", - "label-use-static-key-dimensions": "Utilizar dimensões de chave estática" + "label-use-static-key-dimensions": "Utilizar dimensões de chave estática", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Revogar URL público" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Opções personalizadas", + "name-values-separated-comma": "", "selection-options": "Opções de seleção" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Tipo", "label-url": "URL", "label-with-tags": "Com etiquetas", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Abrir painel de controlo" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Tipo" }, "dashboard-preview-banner": { - "not-saved": "O valor ainda não está guardado na base de dados da Grafana", "not-yet-saved": "O valor não está guardado na base de dados da Grafana", - "open-pull-request-in-git-hub": "Abrir pedido pull no GitHub", - "title-dashboard-loaded-branch-git-hub": "Este painel de controlo é carregado a partir de um ramo no GitHub.", "title-dashboard-loaded-external-repository": "Este painel de controlo é carregado a partir de um repositório externo", - "title-dashboard-loaded-request-git-hub": "Este painel de controlo é carregado a partir de um pedido pull no GitHub.", - "title-error-loading-dashboard": "Erro ao carregar o painel de controlo", - "value-not-saved": "O valor ainda não está guardado na base de dados da Grafana", - "view-pull-request-in-git-hub": "Visualizar pedido pull no GitHub" + "title-error-loading-dashboard": "Erro ao carregar o painel de controlo" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Opções de origem de dados", "description-instance-name-filter": "Filtro regex para as instâncias de origem de dados a escolher na lista de valores de variáveis. Deixe em branco para todos.", "example-instance-name-filter": "Exemplo: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Opções de seleção" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Adicionar transformação" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Opções de coluna", "row-height-options": "Opções de altura da linha" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Forneça dimensões como CSV: {{name}}, {{value}}", "group-by-options": "Agrupar por opções", "label-data-source": "Origem dos dados", - "label-use-static-group-by-dimensions": "Utilizar dimensões de grupo estático" + "label-use-static-group-by-dimensions": "Utilizar dimensões de grupo estático", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Copiar para a área de transferência", @@ -5521,9 +5710,14 @@ "apply": "Aplicar" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "O valor calculado não irá abaixo deste limite", "description-step-count": "Quantas vezes o intervalo de tempo atual deve ser dividido para calcular o valor", - "interval-options": "Opções de intervalo" + "interval-options": "Opções de intervalo", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Utilizado em {{count}} painéis de controlo", "usage-count_other": "Utilizado em {{count}} painéis de controlo" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Já existe um painel de controlo com o mesmo nome na pasta selecionada. Ainda pretende guardar este painel de controlo?", "title-name-already-exists": "Nome já existente" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Continuar para o site externo?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Adicionar outra transformação", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Eliminar todas as transformações", "title-delete-all-transformations": "Eliminar todas as transformações?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Os grupos de captura nomeados podem ser utilizados para separar o texto e o valor de exibição (<1>ver exemplos).", "description-optional": "Opcional, se pretender extrair parte de um nome de série ou segmento de nó métrico.", "label-data-source": "Origem dos dados", + "label-static-options-sort": "", "label-target-data-source": "Origem de dados de destino", + "label-use-static-options": "", + "name-regex": "", "query-options": "Opções de consulta", - "selection-options": "Opções de seleção" + "selection-options": "Opções de seleção", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Tem a certeza de que pretende restaurar o painel de controlo para a versão {{version}}? Todas as alterações não guardadas serão perdidas.", + "confirmText-restore-version": "", "title-restore-version": "Restaurar versão" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Pasta", "render-footer": { "title-failed-to-save-dashboard": "Não foi possível guardar o painel de controlo" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Permite que vários valores sejam selecionados ao mesmo tempo", "description-enables-option-include-variables": "Ativa uma opção para incluir todos os valores", - "description-enables-users-custom-values": "Permite que os utilizadores adicionem valores personalizados à lista" + "description-enables-users-custom-values": "Permite que os utilizadores adicionem valores personalizados à lista", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Alternar menu de partilha" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "A cópia para a área de transferência falhou" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(opcional)", "text-options": "Opções de texto" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Tem a certeza de que pretende desassociar este painel?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Esta variável não é referenciada por nenhuma variável ou por nenhum painel de controlo.", "aria-label-variable-referenced-other-variables-dashboard": "Esta variável é referenciada por outras variáveis ou outros painel de controlo.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulário do editor de variáveis", "back-to-list": "Voltar à lista", + "confirmText": { + "delete-variable": "" + }, "delete": "Eliminar", "description-optional-display-name": "Nome de exibição opcional", "description-template-variable-characters": "O nome da variável do modelo. (máx. 50 caracteres)", "general": "Geral", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texto descritivo", "placeholder-label-name": "Nome da etiqueta", "placeholder-variable-name": "Nome da variável", @@ -5829,13 +6066,25 @@ "variable": "Variável" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Eliminar variável", "tooltip-duplicate-variable": "Variável duplicada", "tooltip-remove-variable": "Remover variável" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Ocultar" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "A mostrar as utilizações para: {{variableId}}", "tooltip-show-usages": "Mostrar utilizações" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Alternar seleção da versão {{version}}", "date": "Data", + "name-latest": "", "notes": "Observações", "restore": "Restaurar", "updated-by": "Atualizado por", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Carregar" @@ -6287,6 +6538,7 @@ }, "label-limit": "Limite", "label-value": "Valor", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Máx.", "label-min": "Mín.", - "label-value": "Valor" + "label-value": "Valor", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Selecionar o operador do nome do serviço", "aria-label-select-span-name": "Selecionar o nome do intervalo", "aria-label-select-span-name-operator": "Selecionar o operador do nome do intervalo", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Filtros de intervalo", "label-duration": "Duração", "label-service-name": "Nome do serviço", @@ -6939,6 +7199,8 @@ "split-widen": "Alargar painel" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Dar feedback", "label-copied": "Copiado!", "label-export": "Exportar", @@ -7046,6 +7308,7 @@ "when": "QUANDO" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Funcionamento" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Limpar pastas", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Filtro de pastas", "select-placeholder": "Filtrar por pasta" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Lamentamos, mas não foi possível concluir o seu pedido. Tente novamente.", "send-custom-feedback": "Enviar" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Latitude", "label-longitude": "Longitude" @@ -7153,6 +7463,14 @@ "center": "Centro:", "zoom": "Ampliar:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Camada" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Adicionar regra de estilo geomap" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Adicionar camada", "no-layers": "Sem camadas?" @@ -7185,16 +7511,38 @@ "label-zoom": "Zoom", "use-current-map-settings": "Utilizar as definições atuais do mapa" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Símbolo" }, "measure-overlay": { "tooltip-show-measure-tools": "Mostrar ferramentas de medição" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "A camada do mapa base é configurada pelo administrador do servidor." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Alinhar", "label-baseline": "Linha de base", "label-color": "Cor", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Alinhamento vertical do símbolo", "label-text-label": "Etiqueta de texto", "label-x-offset": "Deslocamento de X", - "label-y-offset": "Deslocamento de Y" + "label-y-offset": "Deslocamento de Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Operador de comparação", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Propriedade em destaque", "placeholder-numeric-value": "Valor numérico", "placeholder-value": "valor" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "cor {{colorLabel}} " }, "confirm-button": { - "cancel": "Cancelar" + "cancel": "Cancelar", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Digite \"{{confirmPromptText}}\" para confirmar" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "alternar painel de recolha", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Cancelar consulta", "tooltip-cancel-loading": "Cancelar consulta", "tooltip-stop-streaming": "Parar a transmissão", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Diferenciar maiúsculas de minúsculas", "inspect-drawer-title": "Inspecionar valor", "inspect-menu-label": "Inspecionar valor", + "nested-table": { + "no-data": "" + }, "no-values-label": "Nenhum valor", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Adicionar", + "placeholder-new-tag": "", "remove": "Remover etiqueta: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Clique para {{actionTitle}}", "footer-click-to-navigate": "Clique para abrir {{linkTitle}}", "timestamp": "Registo de data e hora" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Criar painel de biblioteca" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Pretende eliminar este painel?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Utilizado em {{count}} painéis de controlo" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Pesquisar por nome ou descrição" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "A carregar painel de biblioteca..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Crie um link direto e personalizado para partilhar o seu painel dentro da sua organização, com as seguintes definições de personalização:", "download-image": "Descarregar imagem", - "render-image": "Gerar imagem", - "render-image-error": "Falha ao processar a imagem do painel", - "render-image-error-description": "Ocorreu um erro ao gerar a imagem" + "render-image": "Gerar imagem" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Quebra de linhas" }, "log-details": { - "close": "Fechar detalhes do registo", "fields": "Campos", "links": "Links", "log-line": "Linha de registo", @@ -8405,6 +8795,42 @@ "show-more": "mostrar mais", "tooltip-error": "Erro: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Copiar link para a linha de registo", "copy-log": "Copiar linha de registo", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Ligações de dados", "aria-label-fields": "Campos", + "aria-label-line": "", "aria-label-log-level": "Nível de registo", "aria-label-no-details": "Sem detalhes" }, @@ -8598,6 +9030,8 @@ "updated-on": "Atualizado a" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Eliminar" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Importar painel de controlo" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Esta ferramenta pode migrar alguns recursos desta instalação para a sua pilha de nuvem. Para começar, terá de criar um instantâneo desta instalação. A criação de um instantâneo geralmente demora menos de dois minutos. O instantâneo é armazenado juntamente com esta instalação da Grafana.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "O painel de controlo já está provisionado e gerido pela Grafana na instância de nuvem. Recomendamos a utilização do painel de controlo provisionado a partir de agora. Se ainda pretender copiar o painel de controlo para a instância de nuvem, altere o ID do painel de controlo no JSON do painel de controlo, guarde um novo instantâneo e carregue novamente.", "datasource-already-managed": "A origem de dados já está provisionada e gerida pela Grafana na instância de nuvem. Se este for um recurso diferente, defina outro UID e tente novamente.", "datasource-invalid-url": "Existe uma origem de dados com um URL inválido. Forneça um URL válido e tente novamente.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": ">100 nós", "aria-label-hidden-marker": "Marcador de nós ocultos: {{marker}}", - "node-count_one": "{{count}} nó", + "node-count_one": "{{count}} nós", "node-count_other": "{{count}} nós" }, "node": { @@ -9318,9 +9763,9 @@ "aria-label-nodes-hidden-warning": "Aviso de nós ocultos", "computing-layout": "Layout de computação", "no-data": "Sem dados", - "hidden-nodes_one": "<0> {{count}} nó está oculto por razões de desempenho.", + "hidden-nodes_one": "<0> {{count}} nós estão ocultos por razões de desempenho.", "hidden-nodes_other": "<0> {{count}} nós estão ocultos por razões de desempenho.", - "processed-nodes_one": "<0> O layout em camadas pode ser lento com {{count}} nó.", + "processed-nodes_one": "<0> O layout em camadas pode ser lento com {{count}} nós.", "processed-nodes_other": "<0> O layout em camadas pode ser lento com {{count}} nós." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Selecionar organização" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Não possui permissão para ver esta página.", "title-access-denied": "Acesso negado" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Não foi encontrado nenhum componente de página de aplicação de nível superior" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "plugins atualizados" }, "versions": { - "confirmation-text-1": "Tem a certeza de que pretende fazer o downgrade para a versão", - "confirmation-text-2": "Normalmente, não deveria estar a fazer isto", + "confirmation-text": "", "downgrade-confirm": "Downgrade", "downgrade-title": "Fazer o downgrade da versão do plugin" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Nenhum plugin encontrado" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "A atualizar" }, "install-controls-button": { - "title-uninstall-modal": "Desinstalar {{plugin}}" + "title-uninstall-modal": "Desinstalar {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Este plugin não está publicado em <2>grafana.com/plugins e não pode ser gerido através do catálogo.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Esta funcionalidade está atualmente em desenvolvimento ativo. Para a melhor experiência e as melhorias mais recentes, recomendamos a utilização da <2>compilação noturna da Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Selecionador da conta de serviço", "select-placeholder": "Comece a escrever para pesquisar contas de serviço" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Adicionar token da conta de serviço", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Eliminar a conta de serviço", "disable-service-account": "Desativar a conta de serviço", "enable-service-account": "Ativar a conta de serviço", @@ -10907,6 +11375,7 @@ "used-by": "Utilizado por" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Editar" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Adicionar conta de serviço", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Pesquisar uma conta de serviço por nome", "sub-title": "As contas de serviço e os seus tokens podem ser utilizados para autenticar na API da Grafana. Saiba mais na nossa <2>documentação.", "title-delete-service-account": "Eliminar a conta de serviço", - "title-disable-service-account": "Desativar a conta de serviço" + "title-disable-service-account": "Desativar a conta de serviço", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Este token expirou", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exportar como JSON", "share-externally-title": "Partilhar externamente", "share-internally-title": "Partilhar internamente", @@ -10972,6 +11448,19 @@ "view-button": "Ver JSON", "view-button-yaml": "Ver YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Criar painel de biblioteca." }, @@ -10980,7 +11469,7 @@ "info-text": "Crie um link direto para este painel de controlo ou painel, personalizado com as opções abaixo.", "link-url": "URL do link", "render-alert": "Plugin de renderização de imagem não instalado", - "render-instructions": "Para processar uma imagem de painel, tem de instalar o <2>plug-in de processamento de imagens Grafana. Entre em contacto com o seu administrador da Grafana para instalar o plugin.", + "render-instructions": "", "rendered-image": "Link direto para imagem renderizada", "save-alert": "O painel de controlo não está guardado", "save-dashboard": "Para processar uma imagem de painel, tem de guardar o painel de controlo primeiro.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Pré-visualização do painel" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Demasiados pontos para visualizar corretamente. <1>Atualizar a consulta para devolver menos pontos. <3>({{count}} ponto recebido)", + "too-many-points_one": "Demasiados pontos para visualizar corretamente. <1>Atualizar a consulta para devolver menos pontos. <3>({{count}} pontos recebidos)", "too-many-points_other": "Demasiados pontos para visualizar corretamente. <1>Atualizar a consulta para devolver menos pontos. <3>({{count}} pontos recebidos)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Texto alternativo que será exibido se uma imagem não puder ser exibida ou para utilizadores que utilizem um leitor de ecrã", "description-title-text": "Texto que será exibido quando o cursor passar por cima da imagem", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Opção de etiqueta" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Seletor de equipa", "select-placeholder": "Selecione uma equipa" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Campo(s) ou número" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Nome alternativo", "label-mode": "Modo", "label-replace-all-fields": "Substituir todos os campos" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Etiqueta", "label-name": "Nome", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Frame" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Adicionar um transformador de tipo de campo convert", "aria-label-remove-convert-field-type-transformer": "Remover transformador de tipo de campo convert", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Navegador", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Formato da entrada", "label-join-with": "Juntar com", "label-set-timezone": "Definir o fuso horário", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Tipo", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Utilize um separador explícito ao juntar valores de matriz", "tooltip-input-format": "Especificar o formato do campo de entrada para que a Grafana possa analisar a cadeia de datas corretamente.", "tooltip-specify-the-output-format": "Especifique o formato de saída.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Desligado" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Adicionar valor de enumeração", "generate-enum-values-from-data": "Gerar valores de enumeração a partir de dados" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Eliminar" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Delimitador", "label-format": "Formato", "label-keep-time": "Registar tempo", "label-replace-all-fields": "Substituir todos os campos", "label-source": "Origem", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Automóvel", "placeholder-select-delimiter": "Selecione o delimitador...", "tooltip-regexp": "Exemplo: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Cor do limite" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", - "label-lookup": "Pesquisar" + "label-lookup": "Pesquisar", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Definições adicionais", @@ -11848,12 +12413,26 @@ "use-as": "Utilizar como " }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Da variável", "label-identifier": "Identificador", "label-use-variable": "Utilizar variável", "label-variable": "Variável", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Padrão de expressão regular" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Campo", "label-match": "Correspondência", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Adicionar condição", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Condições", - "label-filter-type": "Tipo de filtro" + "label-filter-type": "Tipo de filtro", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Campo", "label-format": "Formato", "label-substring-range": "Intervalo de subcadeia" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Formato", "label-set-timezone": "Definir o fuso horário", @@ -11878,6 +12491,70 @@ "tooltip-format": "O formato de saída para o campo especificado como uma <2>sequência de caracteres de formato Moment.js.", "tooltip-timezone-manually": "Defina o fuso horário da data manualmente" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Navegador", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorado", "placeholder-select-stats": "Selecionar estatísticas" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Os cálculos não terão efeito se nenhum campo for agrupado" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Se esta opção estiver ativada, as tabelas aninhadas mostrarão os nomes dos campos como cabeçalho da tabela", "label-show-field-names-in-nested-tables": "Mostrar nomes de campos em tabelas aninhadas", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Os cálculos não terão efeito se nenhum campo for agrupado." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Valor da célula", "label-column": "Coluna", "label-empty-value": "Valor vazio", "label-row": "Linha", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Valor booleano falso", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Automóvel", "placeholder-default": "Predefinição: 30", "placeholder-none": "Nenhum" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Como percentil", "tooltip-transform-index-percentile": "Transformar o índice de linha como um percentil." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "A entrada não contém nenhumas etiquetas", "no-input-or-labels-found": "Nenhuma entrada encontrada (ou etiquetas encontradas)", @@ -11969,10 +12685,30 @@ "label-labels": "Etiquetas", "label-mode": "Modo", "label-value-field-name": "Nome do campo de valor", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Opcional) Selecionar etiqueta", "tooltip-replace-value-field-label": "Substitua o nome do campo de valor por uma etiqueta" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Valor ou variável" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Os valores de cadeia são representados como campos separados e não como etiquetas" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "A fusão não tem efeito quando aplicada num frame único." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "A fusão não tem efeito quando aplicada num frame único.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Todos os valores são numéricos", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Tempo em ordem ascendente" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Automóvel", "field-order-manual": "", - "first-frame-warning": "Organizar campos só funciona com um único frame. Considere aplicar uma transformação de junção ou filtrar a entrada primeiro." + "first-frame-warning": "Organizar campos só funciona com um único frame. Considere aplicar uma transformação de junção ou filtrar a entrada primeiro.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Define como os nomes dos campos selecionados são exibidos. Como o nome do frame é geralmente melhor para dados tabulares" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "A partição por valores só funciona com um único frame.", "tooltip-keeps-partition-fields-frames": "Mantém os campos de partição nos frames" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Formato", - "label-info": "Informação" + "label-info": "Informação", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "e", @@ -12035,6 +12815,9 @@ "label-operation": "Funcionamento" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Cálculos", "label-include-time": "Incluir hora", "label-labels-to-fields": "Etiquetas para campos", @@ -12049,12 +12832,23 @@ "series-to-rows": "Série para linhas" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Escolher estatística" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Valor ou variável", "variable-regex-documentation": "Formata a variável de múltiplos valores numa string regex" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Cúbico", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Número de pontos X, Y para prever" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Correspondência", "label-replace": "Substituir", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Campo", - "label-mode": "Modo" + "label-mode": "Modo", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Campo", "label-reverse": "Inverter", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Selecionar campo" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Transformar" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Estatística", "label-time-field": "Campo de hora", @@ -12121,10 +12960,34 @@ "placeholder-value": "Valor", "tooltip-name-for-value-fields": "Nome para campos de valor" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Funcionamento", "placeholder-field": "Campo" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Vários campos de valor de qualquer tipo", "single-frame": "Frame único", @@ -12177,6 +13040,7 @@ "title": "Organizações" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Seletor de utilizador", "select-placeholder": "Começar a digitar para pesquisar utilizador" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Eliminar variável" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Atualizar" }, "query-variable-sort-select": { - "description-values-variable": "Como ordenar os valores desta variável" + "description-values-variable": "Como ordenar os valores desta variável", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "valor predefinido, se houver", "text-options": "Opções de texto" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Nome de exibição opcional", "description-template-variable-characters": "O nome da variável do modelo. (máx. 50 caracteres)", "general": "Geral", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Texto descritivo", "placeholder-label-name": "Nome da etiqueta", "placeholder-variable-name": "Nome da variável", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Variável duplicada", "tooltip-remove-variable": "Remover variável" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Alternar todos os valores" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Mostrar utilizações" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index a38ed0702b1..3c5c73396e5 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Одни функции работают в стабильном режиме (GA) и включены по умолчанию, а другие находятся на стадии предварительной бета-версии, доступной для раннего использования.", "confirm-modal-body-2": "Прежде чем вносить изменения, рекомендуем ознакомиться с их последствиями для каждой функции.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Бета", "content-general-availability": "Общедоступность", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Удалить организацию", + "confirmText-delete": "", "title-delete": "Удаление" }, "anon-users": { "not-found": "Анонимные пользователи не найдены." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Принудительный выход на всех устройствах" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "У вас нет разрешения на просмотр пользователей в этой организации. Чтобы обновить данные этой организации, обратитесь к администратору сервера.", "heading": "Редактирование организации", @@ -208,9 +216,11 @@ "not-editable": "Роль этого пользователя не редактируется, поскольку она синхронизируется с провайдером аутентификации. Подробнее см. в <1>документации по аутентификации Grafana." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Роль" }, + "confirmText-delete": "", "delete-aria-label": "Удалить пользователя: {{name}}", "title-delete": "Удаление" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Эти параметры настройки системы заданы в файле grafana.ini или custom.ini (или переопределены в переменных ENV). Чтобы изменить их, необходимо перезапустить Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Лицензия Enterprise" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Изменить", + "confirmText-change": "", "grafana-admin-key": "Администратор Grafana", "grafana-admin-no": "Нет", "grafana-admin-yes": "Да", "title": "Разрешения" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Удалить пользователя", "disable-button": "Отключить пользователя", "edit-button": "Редактировать", @@ -312,6 +330,9 @@ "title-delete-user": "Удаление пользователя", "title-disable-user": "Отключение пользователя" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Браузер и ОС", "force-logout-all-button": "Принудительный выход на всех устройствах", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Периоды активности" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Добавить еще" }, @@ -465,6 +490,9 @@ "label-muting-grouping-and-timings-optional": "Отключение звука, группировка и определение времени (необязательно)", "title-muting-grouping-and-timings": "Отключение звука, группировка и определение времени" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Копировать ссылку", "duplicate": "Дублировать", @@ -488,6 +516,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Редактировать YAML", @@ -554,6 +585,7 @@ "view-configuration": "Просмотр конфигурации" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Внутреннюю конфигурацию обработчика оповещений Grafana Alertmanager невозможно изменить вручную. Чтобы изменить конфигурацию, измените отдельные ресурсы через пользовательский интерфейс.", "gma-manual-configuration-is-not-supported": "Изменения конфигурации вручную не поддерживаются", "message": { @@ -568,11 +600,13 @@ "title-resetting-alertmanager-configuration": "Сброс конфигурации обработчика оповещений Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Сравнить", "restore": "Восстановить", "text-latest": "Последние" }, + "confirmText-yes-restore-configuration": "", "loading": "Загрузка…", "no-previous-configurations": "Нет предыдущих конфигураций", "this-might-take-a-while": "Это может занять некоторое время...", @@ -852,8 +886,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Другие действия для точки контакта «{{contactPointName}}»", + "ariaLabel-delete": "", "button-edit": "Редактировать", "button-view": "Просмотр", + "export-ariaLabel-export": "", "export-label-export": "Экспорт", "label-delete": "Удалить", "label-manage-permissions": "Управление разрешениями", @@ -1392,6 +1428,7 @@ "label-disable-resolved-message": "Отключить сообщение об устранении" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Должно быть положительным целым числом.", "must-enter-a-group-name": "Необходимо ввести название группы" @@ -1483,15 +1520,12 @@ "group-details": { "ds-features-error": "Ошибка при загрузке сведений об источнике данных", "edit": "Редактировать", - "evaluations-to-fire": "Циклы оценки для отправки оповещений", "export": "Экспорт", "folder": "Папка", "group-loading-error": "Ошибка при загрузке группы", "interval": "Интервал", "namespace": "Пространство имен", - "pending-period": "Период ожидания", - "recording": "Запись", - "rule-name": "Имя правила" + "new": "" }, "group-edit": { "ds-error": "Ошибка при загрузке сведений об источнике данных", @@ -1537,8 +1571,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Начало работы", "label-insights": "Аналитика", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Оповещение" }, "import-to-gma": { @@ -1840,7 +1886,11 @@ "other-data-sources": "Другие источники данных" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Отключено", @@ -2093,9 +2143,11 @@ "update-errors": { "conflict": "Другой пользователь обновил дерево политик уведомления.", "error-code": "Сообщение об ошибке: «{{error}}»", - "fallback": "Ошибка при обновлении политик уведомления.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Обновите страницу и повторите попытку.", - "title": "Ошибка при сохранении политики уведомления" + "title": "" }, "n-more-policies_one": "{{count}} дополнительная политика", "n-more-policies_few": "{{count}} дополнительные политики", @@ -2153,6 +2205,7 @@ "query-and-expressions-step": { "add-query": "Добавить запрос", "body-queries-expressions-configured": "Создайте хотя бы один запрос или выражение, на основе которых будет отправляться оповещение", + "confirmText-deactivate": "", "expressions": "Выражения", "loading-data-sources": "Загрузка источников данных...", "manipulate-returned-queries-other-operations": "Управляйте данными, возвращаемыми в результате запросов, с помощью математических и других операций.", @@ -2202,6 +2255,9 @@ "label-export-all": "Экспортировать всё" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Не удалось загрузить редактор запросов. Причина: {{errorMessage}}" }, @@ -2220,6 +2276,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Нужно будет установить новую группу оценки для скопированного правила, поскольку исходная группа была подготовлена и не может использоваться для правил, созданных в пользовательском интерфейсе.", "body-not-provisioned": "Новое правило <1>не будет помечено как подготовленное.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Копирование подготовленного правила оповещения" }, "redirect-to-rule-viewer": { @@ -2419,8 +2476,6 @@ "title-inspect-alert-rule": "Проверка правила оповещения" }, "rule-list": { - "cannot-find-rule-details-for": "Не удалось найти сведения о правиле для UID {{uid}}", - "cannot-load-rule-details-for": "Не удалось загрузить сведения о правиле для UID {{uid}}", "configure-datasource": "Настроить", "draft-new-rule": "Составить новое правило", "ds-error": { @@ -2624,6 +2679,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Не удается найти нужный источник данных? Некоторые источники данных не поддерживаются для функции оповещения. Нажмите значок для получения подробной информации." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Добавить новый обработчик оповещений Alertmanager", "builtin-alertmanager": "Встроенный обработчик оповещений Alertmanager", @@ -2776,6 +2836,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Выбрать шаблон уведомления", "loading": "Загрузка…", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Выбрать шаблон уведомления" } @@ -2802,6 +2865,8 @@ }, "templates-table": { "actions": "Действия", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Шаблоны не настроены.", "template-group": "Группа шаблонов", "title-delete-template-group": "Удаление группы шаблонов" @@ -2854,6 +2919,11 @@ "timestamp": { "time-ago": "({{time}} назад)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Целевая папка не пуста, некоторые правила могут быть перезаписаны или удалены. Действительно импортировать эти правила оповещения в правила, управляемые Grafana?", @@ -2929,6 +2999,11 @@ "title-delete-contact-point": "Удаление точки контакта" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Удаление политики уведомления" @@ -3085,7 +3160,8 @@ "annotation-field-mapper": { "annotation": "Аннотация", "first-value": "Первое значение", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Добавить запрос аннотации", @@ -3093,6 +3169,16 @@ "info-box-content-2": "Для получения дополнительной информации ознакомьтесь с <2>документацией по аннотациям.", "title": "Пользовательские запросы аннотаций еще не добавлены" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "Кол-во событий: {{numEvents}} (кол-во полей: {{numFields}})", "no-events-found": "События не найдены", @@ -3168,6 +3254,9 @@ "define-allowed-teams-ids-label": "Определить идентификаторы разрешенных команд", "display-name-description": "Будет отображаться на странице входа как «Войти с помощью...». Параметр полезен, если вы используете несколько поставщиков удостоверений или протоколов единого входа.", "display-name-label": "Отображаемое имя", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Имя ключа, используемого для поиска адреса эл. почты пользователя в карте атрибутов идентификационного токена OAuth2.", "email-attribute-name-label": "Имя атрибута эл. почты", "email-attribute-path-description": "Выражение JMESPath, используемое для поиска адреса эл. почты в информации пользователя.", @@ -3219,7 +3308,7 @@ "team-ids-github": "Целочисленный список идентификаторов команд.", "team-ids-label": "Идентификаторы команд", "team-ids-numbers": "Идентификаторы команд должны быть числами.", - "team-ids-other": "Строковый список идентификаторов команд.", + "team-ids-other": "", "team-ids-placeholder": "Введите идентификаторы команд и нажмите «Enter», чтобы их добавить", "teams-url-description": "URL-адрес, используемый для запроса идентификаторов команд. Если URL-адрес не установлен, значением по умолчанию является /teams.", "teams-url-description-oauth": "Если вы устанавливаете «{{ teamsURLLabel }}», также необходимо установить «{{ teamIDsAttributePathLabel }}».", @@ -3263,6 +3352,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Сброс до значений по умолчанию" }, + "confirmText-reset": "", "disable": "Отключить", "disabling": "Отключение...", "discard": "Отменить", @@ -3509,11 +3599,11 @@ "button-create": "Создание", "button-creating": "Создание...", "cancel": "Отмена", - "error-invalid-folder-name": "Недопустимое имя папки", "error-required": "Требуется имя папки", "folder-name-input-placeholder-enter-folder-name": "Ввести имя папки", "label-folder-name": "Имя папки", "text-pull-request-created": "Создан запрос на включение изменений в папке:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Создан запрос на включение изменений", "title-this-repository-is-read-only": "Репозиторий предназначен только для чтения" }, @@ -3931,6 +4021,7 @@ "tooltip": "Закрыть" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Ошибка при загрузке параметров." @@ -4199,8 +4290,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Динамически рассчитывает интервал путем деления временного диапазона на указанное количество." + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4362,12 +4453,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Перейти на внешний сайт?" } }, "render-left-actions": { - "text-public": "Общедоступный", "tooltip-view-as-scene": "Просмотр в виде сцены" } }, @@ -4560,6 +4653,14 @@ "errors": { "failed-to-load": "Не удалось загрузить дашборд" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Не удалось получить дашборд" @@ -4576,6 +4677,13 @@ "editable": "Редактируемый", "readonly": "Только для чтения" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4625,6 +4733,18 @@ "transform-data": "Преобразовать данные" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4882,6 +5002,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Действительно восстановить дашборд до версии {{version}}? Все несохраненные изменения будут потеряны.", + "confirmText-restore-version": "", "title-restore-version": "Восстановление версии" }, "row-options-button": { @@ -4932,6 +5053,9 @@ "title-not-unique": "Заголовок не является уникальным" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Сохранить как" }, @@ -4966,6 +5090,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Дашборд с таким именем уже существует в выбранной папке.<1><2>Все равно сохранить дашборд?", "body-version-mismatch": "Дашборд обновлен другим пользователем<1><2>Все равно сохранить дашборд?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Конфликт", "title-version-mismatch": "Конфликт" }, @@ -5131,7 +5256,6 @@ "playlist-next": "Перейти к следующему дашборду", "playlist-previous": "Перейти к предыдущему дашборду", "playlist-stop": "Остановить плейлист", - "public-dashboard": "Общедоступный", "refresh": "Обновить дашборд", "save": "Сохранить дашборд", "save-dashboard": { @@ -5162,7 +5286,9 @@ "label-apply-transformation-to": "Применить преобразование к" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Отладка", "title-disable-transformation": "Отключение преобразования", "title-filter": "Фильтр", @@ -5182,12 +5308,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Поиск преобразования", "show-images": "Показать изображения", - "title-add-another-transformation": "Добавление другого преобразования" + "title-add-another-transformation": "Добавление другого преобразования", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Добавить другое преобразование" }, + "confirmText-delete-all": "", "delete-all-transformations": "Удалить все преобразования", "title-delete-all-transformations": "Удалить все преобразования?", "tooltip-clear-search": "Очистить поиск", @@ -5224,6 +5355,7 @@ "version-history-table": { "aria-label-toggle-selection": "Переключить выбор версии {{version}}", "date": "Дата", + "name-latest": "", "notes": "Примечания", "restore": "Восстановить", "updated-by": "Кем обновлено", @@ -5300,7 +5432,8 @@ "description-enables-users-custom-values": "Позволяет пользователям добавлять пользовательские значения в список.", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Укажите измерения в формате CSV: {{name}}, {{value}}.", "label-data-source": "Источник данных", - "label-use-static-key-dimensions": "Использовать измерения статического ключа" + "label-use-static-key-dimensions": "Использовать измерения статического ключа", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5373,6 +5506,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Отозвать общедоступный URL-адрес" } @@ -5384,6 +5520,7 @@ }, "custom-variable-form": { "custom-options": "Пользовательские параметры", + "name-values-separated-comma": "", "selection-options": "Параметры выбора" }, "dashboard-edit-pane-renderer": { @@ -5402,6 +5539,12 @@ "label-type": "Тип", "label-url": "URL", "label-with-tags": "С тегами", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Открыть дашборд" }, "dashboard-link-list": { @@ -5414,15 +5557,9 @@ "type": "Тип" }, "dashboard-preview-banner": { - "not-saved": "Значение еще не сохранено в базе данных Grafana", "not-yet-saved": "Значение не сохранено в базе данных Grafana", - "open-pull-request-in-git-hub": "Открыть запрос на включение изменений в GitHub", - "title-dashboard-loaded-branch-git-hub": "Дашборд загружается из ветви в GitHub.", "title-dashboard-loaded-external-repository": "Дашборд загружается из внешнего репозитория", - "title-dashboard-loaded-request-git-hub": "Дашборд загружается из запроса на включение изменений в GitHub.", - "title-error-loading-dashboard": "Ошибка при загрузке дашборда", - "value-not-saved": "Значение еще не сохранено в базе данных Grafana", - "view-pull-request-in-git-hub": "Просмотр запроса на включение изменений в GitHub" + "title-error-loading-dashboard": "Ошибка при загрузке дашборда" }, "dashboard-scene": { "text": { @@ -5448,6 +5585,8 @@ "data-source-options": "Параметры источника данных", "description-instance-name-filter": "Фильтр регулярных выражений для выбора экземпляров источников данных в списке значений переменных. Оставьте пустым для всех.", "example-instance-name-filter": "Пример: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Параметры выбора" }, "default-grid-layout-manager": { @@ -5493,12 +5632,61 @@ "empty-transformations-message": { "add-transformation": "Добавить преобразование" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Параметры столбцов", "row-height-options": "Параметры высоты строк" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5523,7 +5711,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Укажите измерения в формате CSV: {{name}}, {{value}}.", "group-by-options": "Группировать по параметрам", "label-data-source": "Источник данных", - "label-use-static-group-by-dimensions": "Использовать измерения статической группы" + "label-use-static-group-by-dimensions": "Использовать измерения статической группы", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Копировать в буфер обмена", @@ -5559,9 +5748,14 @@ "apply": "Применить" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Расчетное значение не будет ниже этого порога.", "description-step-count": "Сколько раз нужно разделить текущий временной диапазон для расчета значения.", - "interval-options": "Параметры интервалов" + "interval-options": "Параметры интервалов", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5583,11 +5777,28 @@ "usage-count_many": "Используется на {{count}} дашбордах", "usage-count_other": "Используется на {{count}} дашбордах" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Дашборд с таким названием уже существует в выбранной папке. Все равно сохранить дашборд?", "title-name-already-exists": "Название уже существует" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Перейти на внешний сайт?" } @@ -5623,6 +5834,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Добавить другое преобразование", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Удалить все преобразования", "title-delete-all-transformations": "Удалить все преобразования?" }, @@ -5675,9 +5888,13 @@ "description-examples": "Именованные группы записи можно использовать для разделения отображаемого текста и значения (<1>см. примеры).", "description-optional": "Дополнительно, если вы хотите извлечь часть имени ряда или сегмента узла метрики.", "label-data-source": "Источник данных", + "label-static-options-sort": "", "label-target-data-source": "Целевой источник данных", + "label-use-static-options": "", + "name-regex": "", "query-options": "Параметры запросов", - "selection-options": "Параметры выбора" + "selection-options": "Параметры выбора", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5690,6 +5907,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Действительно восстановить дашборд до версии {{version}}? Все несохраненные изменения будут потеряны.", + "confirmText-restore-version": "", "title-restore-version": "Восстановление версии" }, "save-button": { @@ -5707,7 +5925,8 @@ "label-folder": "Папка", "render-footer": { "title-failed-to-save-dashboard": "Не удалось сохранить дашборд" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5785,7 +6004,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Позволяет выбирать несколько значений одновременно.", "description-enables-option-include-variables": "Включает параметр для добавления всех значений", - "description-enables-users-custom-values": "Позволяет пользователям добавлять пользовательские значения в список." + "description-enables-users-custom-values": "Позволяет пользователям добавлять пользовательские значения в список.", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Переключить меню общего доступа" @@ -5805,6 +6028,9 @@ "copy-to-clipboard-failed": "Не удалось скопировать в буфер обмена" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(необязательно)", "text-options": "Параметры текста" @@ -5828,6 +6054,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Действительно отсоединить панель?" }, "unsaved-changes-modal": { @@ -5844,6 +6072,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "На эту переменную не ссылается ни одна переменная или дашборд", "aria-label-variable-referenced-other-variables-dashboard": "На эту переменную ссылаются другие переменные или дашборд", @@ -5853,10 +6084,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Форма редактора переменных", "back-to-list": "Назад к списку", + "confirmText": { + "delete-variable": "" + }, "delete": "Удалить", "description-optional-display-name": "Отображаемое имя (необязательно)", "description-template-variable-characters": "Название переменной шаблона. (Макс. 50 символов.)", "general": "Общие сведения", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Поясняющий текст", "placeholder-label-name": "Название метки", "placeholder-variable-name": "Название переменной", @@ -5871,13 +6108,25 @@ "variable": "Переменная" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Удаление переменной", "tooltip-duplicate-variable": "Дублировать переменную", "tooltip-remove-variable": "Удалить переменную" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Скрыть" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Показаны варианты использования для: {{variableId}}", "tooltip-show-usages": "Показать варианты использования" @@ -5904,6 +6153,7 @@ "version-history-table": { "aria-label-toggle-selection": "Переключить выбор версии {{version}}", "date": "Дата", + "name-latest": "", "notes": "Примечания", "restore": "Восстановить", "updated-by": "Кем обновлено", @@ -6291,7 +6541,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Передать" @@ -6329,6 +6580,7 @@ }, "label-limit": "Предельное значение", "label-value": "Значение", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6337,9 +6589,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Макс.", "label-min": "Мин.", - "label-value": "Значение" + "label-value": "Значение", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6911,6 +7169,8 @@ "aria-label-select-service-name-operator": "Выбрать оператор названия службы", "aria-label-select-span-name": "Выбрать название диапазона", "aria-label-select-span-name-operator": "Выбрать оператора названия диапазона", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Фильтры диапазонов", "label-duration": "Длительность", "label-service-name": "Название службы", @@ -6981,6 +7241,8 @@ "split-widen": "Расширить область" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Отправить отзыв", "label-copied": "Скопировано", "label-export": "Экспорт", @@ -7088,6 +7350,7 @@ "when": "КОГДА" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Операция" }, "math": { @@ -7118,6 +7381,7 @@ }, "folder-filter": { "clear-folder-button": "Очистить папки", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Фильтр папок", "select-placeholder": "Фильтровать по папкам" }, @@ -7186,7 +7450,53 @@ "incomplete-request-error": "К сожалению, не удалось выполнить запрос. Повторите попытку.", "send-custom-feedback": "Отправить" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Широта", "label-longitude": "Долгота" @@ -7195,6 +7505,14 @@ "center": "Центр:", "zoom": "Масштаб:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Слой" @@ -7217,6 +7535,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Добавить правило стиля геокарты" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Добавить слой", "no-layers": "Без слоев?" @@ -7227,16 +7553,38 @@ "label-zoom": "Масштаб", "use-current-map-settings": "Использовать текущие настройки карты" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Символ" }, "measure-overlay": { "tooltip-show-measure-tools": "Показать инструменты измерения" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Слой базовой карты конфигурируется администратором сервера." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Выравнивание", "label-baseline": "Базовая линия", "label-color": "Цвет", @@ -7250,7 +7598,14 @@ "label-symbol-vertical-align": "Вертикальное выравнивание символов", "label-text-label": "Текстовая метка", "label-x-offset": "Смещение по оси X", - "label-y-offset": "Смещение по оси Y" + "label-y-offset": "Смещение по оси Y", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Оператор сравнения", @@ -7261,6 +7616,15 @@ "placeholder-feature-property": "Свойство функции", "placeholder-numeric-value": "Числовое значение", "placeholder-value": "значение" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7510,7 +7874,8 @@ "aria-label-selected-color": "{{colorLabel}} цвет" }, "confirm-button": { - "cancel": "Отмена" + "cancel": "Отмена", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Введите «{{confirmPromptText}}» для подтверждения" @@ -7692,6 +8057,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "включить сворачивание панели", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Отмена запроса", "tooltip-cancel-loading": "Отмена запроса", "tooltip-stop-streaming": "Остановка потоковой передачи", @@ -7758,6 +8125,9 @@ "filter-popup-match-case": "С учетом регистра", "inspect-drawer-title": "Проверить значение", "inspect-menu-label": "Проверить значение", + "nested-table": { + "no-data": "" + }, "no-values-label": "Нет значений", "pagination-summary": "Строки: {{itemsRangeStart}} - {{displayedEnd}} из {{numRows}}" }, @@ -7766,6 +8136,7 @@ }, "tags-input": { "add": "Добавить", + "placeholder-new-tag": "", "remove": "Удалить тег: {{name}} " }, "time-sync-button": { @@ -7859,6 +8230,12 @@ "footer-click-to-action": "Нажмите, чтобы {{actionTitle}}", "footer-click-to-navigate": "Нажмите, чтобы открыть {{linkTitle}}", "timestamp": "Метка времени" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8241,6 +8618,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Создание панели библиотеки" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Удалить панель?" }, @@ -8266,7 +8647,7 @@ "usage-count_other": "Используется на {{count}} дашборда" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Поиск по названию или описанию" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Загрузка панели библиотеки..." @@ -8320,9 +8701,7 @@ "share-panel": { "config-description": "Создайте индивидуальную прямую ссылку, чтобы делиться своей панелью внутри организации, с помощью следующих параметров настройки:", "download-image": "Загрузить изображение", - "render-image": "Создать изображение", - "render-image-error": "Не удалось визуализировать изображение панели", - "render-image-error-description": "Ошибка при создании изображения" + "render-image": "Создать изображение" } }, "live": { @@ -8408,6 +8787,24 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_few": "", + "indexed-label_many": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_few": "", + "parsedl-label_many": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_few": "", + "structured-metadata_many": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8431,7 +8828,6 @@ "label-wrap-lines": "Переносить строки" }, "log-details": { - "close": "Закрыть сведения о журнале", "fields": "Поля", "links": "Ссылки", "log-line": "Строка журнала", @@ -8457,6 +8853,42 @@ "show-more": "больше", "tooltip-error": "Ошибка: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Копировать ссылку в строку журнала", "copy-log": "Копировать строку журнала", @@ -8557,6 +8989,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8583,6 +9020,7 @@ "un-themed-log-details": { "aria-label-data-links": "Ссылки на данные", "aria-label-fields": "Поля", + "aria-label-line": "", "aria-label-log-level": "Уровень журнала", "aria-label-no-details": "Нет сведений" }, @@ -8650,6 +9088,8 @@ "updated-on": "Дата обновления" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Удалить" }, "unthemed-dashboard-import": { @@ -8659,8 +9099,17 @@ "text": { "import-dashboard": "Импорт дашборда" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Инструмент может перенести некоторые ресурсы из этого экземпляра в облачный стек. Чтобы начать, вам нужно создать снимок этого экземпляра. Создание снимка обычно занимает менее двух минут. Снимок хранится вместе с этим экземпляром Grafana.", @@ -8809,6 +9258,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Дашборд уже подготовлен в облачном экземпляре и управляется Grafana. Рекомендуем в будущем использовать подготовленный дашборд. Если вы всё же хотите скопировать дашборд в облачный экземпляр, измените его идентификатор в JSON-файле, сохраните новый снимок и загрузите его снова.", "datasource-already-managed": "Источник данных уже подготовлен в облачном экземпляре и управляется Grafana. Если это другой ресурс, установите другой UID и повторите попытку.", "datasource-invalid-url": "Существует источник данных с недопустимым URL-адресом. Укажите действительный URL-адрес и повторите попытку.", @@ -9359,10 +9810,10 @@ "marker": { "100-node-count": ">100 узлов", "aria-label-hidden-marker": "Маркер скрытых узлов: {{marker}}", - "node-count_one": "{{count}} узел", + "node-count_one": "{{count}} узлов", "node-count_few": "{{count}} узла", "node-count_many": "{{count}} узлов", - "node-count_other": "{{count}} узла" + "node-count_other": "{{count}} узлов" }, "node": { "aria-label-node-title": "Узел: {{nodeName}}" @@ -9372,13 +9823,13 @@ "aria-label-nodes-hidden-warning": "Предупреждение о скрытых узлах", "computing-layout": "Макет вычислений", "no-data": "Нет данных", - "hidden-nodes_one": "<0> {{count}} узел скрыт из соображений производительности.", + "hidden-nodes_one": "<0> {{count}} узлов скрыты из соображений производительности.", "hidden-nodes_few": "<0> {{count}} узла скрыты из соображений производительности.", "hidden-nodes_many": "<0> {{count}} узлов скрыты из соображений производительности.", - "hidden-nodes_other": "<0> {{count}} узла скрыты из соображений производительности.", - "processed-nodes_one": "<0> Многоуровневый макет может работать медленно с {{count}} узлом.", + "hidden-nodes_other": "<0> {{count}} узлов скрыты из соображений производительности.", + "processed-nodes_one": "<0> Многоуровневый макет может работать медленно с {{count}} узла.", "processed-nodes_few": "<0> Многоуровневый макет может работать медленно с {{count}} узлами.", - "processed-nodes_many": "<0> Многоуровневый макет может работать медленно с {{count}} узлами.", + "processed-nodes_many": "<0> Многоуровневый макет может работать медленно с {{count}} узла.", "processed-nodes_other": "<0> Многоуровневый макет может работать медленно с {{count}} узла." }, "node-graph-panel": { @@ -9505,6 +9956,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Выбрать организацию" }, "page": { @@ -9727,6 +10179,7 @@ "permission": "Отсутствует разрешение на просмотр этой страницы.", "title-access-denied": "Доступ запрещен" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Компонент корневой страницы приложения не найден" }, "browse": { @@ -9770,8 +10223,7 @@ "update-status-text": "плагины обновлены" }, "versions": { - "confirmation-text-1": "Действительно вернуться к версии", - "confirmation-text-2": "Обычно такой шаг не рекомендуется", + "confirmation-text": "", "downgrade-confirm": "Понизить версию", "downgrade-title": "Понижение версии плагина" } @@ -9825,6 +10277,10 @@ "empty-state": { "message": "Плагины не найдены" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9858,7 +10314,11 @@ "updating": "Обновление" }, "install-controls-button": { - "title-uninstall-modal": "Удаление {{plugin}}" + "title-uninstall-modal": "Удаление {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Этот плагин не опубликован на странице <2>grafana.com/plugins и им невозможно управлять через каталог.", @@ -10029,6 +10489,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "В настоящее время эта функция находится в стадии активной разработки. Чтобы обеспечить максимальное удобство пользования и получить доступ к последним улучшениям, рекомендуем использовать <2>ночную сборку Grafana." @@ -10904,6 +11373,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Указатель служебной учетной записи", "select-placeholder": "Начните вводить текст для поиска служебных учетных записей" }, @@ -10949,6 +11419,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Добавить токен служебной учетной записи", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Удалить служебную учетную запись", "disable-service-account": "Отключить служебную учетную запись", "enable-service-account": "Включить служебную учетную запись", @@ -10975,6 +11449,7 @@ "used-by": "Кем используется" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Редактировать" }, "service-account-role-row": { @@ -10988,10 +11463,18 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Добавить служебную учетную запись", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Поиск служебной учетной записи по имени", "sub-title": "Служебные учетные записи и их токены можно использовать для аутентификации в API Grafana Подробнее см. в нашей <2>документации", "title-delete-service-account": "Удалить служебную учетную запись", - "title-disable-service-account": "Отключение служебной учетной записи" + "title-disable-service-account": "Отключение служебной учетной записи", + "body-delete_one": "", + "body-delete_few": "", + "body-delete_many": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Срок действия токена истек", @@ -11005,6 +11488,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Экспорт в формате JSON", "share-externally-title": "Общий доступ с внешними пользователями", "share-internally-title": "Общий доступ с внутренними пользователями", @@ -11040,6 +11524,19 @@ "view-button": "Просмотр JSON-файла", "view-button-yaml": "Просмотр YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Создайте панель библиотеки." }, @@ -11048,7 +11545,7 @@ "info-text": "Создайте прямую ссылку на дашборд или панель, настроенные с помощью приведенных ниже параметров.", "link-url": "Привязать URL-адрес", "render-alert": "Плагин визуализации изображений не установлен", - "render-instructions": "Чтобы визуализировать изображение панели, необходимо установить <2>плагин визуализации изображений Grafana. Чтобы установить плагин, обратитесь к администратору Grafana.", + "render-instructions": "", "rendered-image": "Прямая ссылка на визуализированное изображение", "save-alert": "Дашборд не сохранен", "save-dashboard": "Чтобы визуализировать изображение панели, сначала необходимо сохранить дашборд.", @@ -11116,6 +11613,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Предварительный просмотр панели" }, @@ -11383,7 +11886,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "Слишком много точек для правильной визуализации. <1>Обновите запрос, чтобы получить меньше точек. <3>(получена {{count}} точка)", + "too-many-points_one": "Слишком много точек для правильной визуализации. <1>Обновите запрос, чтобы получить меньше точек. <3>(получено {{count}} точки)", "too-many-points_few": "Слишком много точек для правильной визуализации. <1>Обновите запрос, чтобы получить меньше точек. <3>(получены {{count}} точки)", "too-many-points_many": "Слишком много точек для правильной визуализации. <1>Обновите запрос, чтобы получить меньше точек. <3>(получены {{count}} точек)", "too-many-points_other": "Слишком много точек для правильной визуализации. <1>Обновите запрос, чтобы получить меньше точек. <3>(получено {{count}} точки)" @@ -11458,6 +11961,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Альтернативный текст, который будет отображаться, если невозможно показать изображение, или для пользователей, которые используют программу чтения с экрана", "description-title-text": "Текст, который будет отображаться при наведении курсора на изображение", @@ -11478,6 +11982,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11528,6 +12033,7 @@ "tag-option-label": "Параметр тегов" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Указатель команды", "select-placeholder": "Выбрать команду" }, @@ -11829,6 +12335,18 @@ "placeholder-fields-or-number": "Поля или число" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Псевдоним", "label-mode": "Режим", "label-replace-all-fields": "Заменить все поля" @@ -11843,6 +12361,13 @@ "concatenate-transformer-editor": { "label-label": "Метка", "label-name": "Имя", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Фрейм" }, "config-from-query-transformer-editor": { @@ -11853,6 +12378,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Добавить преобразователь типа поля", "aria-label-remove-convert-field-type-transformer": "Удалить преобразователь типа поля", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Браузер", "utc": "UTC" @@ -11863,7 +12392,13 @@ "label-input-format": "Формат ввода", "label-join-with": "Объединить с", "label-set-timezone": "Установить часовой пояс", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Тип", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Используйте явный разделитель при объединении значений массива", "tooltip-input-format": "Укажите формат поля ввода, чтобы дать Grafana возможность правильно проанализировать строку даты.", "tooltip-specify-the-output-format": "Укажите формат вывода.", @@ -11885,6 +12420,19 @@ "desc": "", "off": "Выкл." }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Добавить перечислимое значение", "generate-enum-values-from-data": "Создать перечислимые значения из данных" @@ -11895,11 +12443,22 @@ "remove-enum-row-tooltip-delete": "Удалить" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Разделитель", "label-format": "Формат", "label-keep-time": "Сохранить время", "label-replace-all-fields": "Заменить все поля", "label-source": "Источник", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Авто", "placeholder-select-delimiter": "Выбор разделителя...", "tooltip-regexp": "Пример: {{regexExample}}" @@ -11908,8 +12467,22 @@ "aria-label-threshold-color": "Пороговый цвет" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Поле", - "label-lookup": "Просмотр" + "label-lookup": "Просмотр", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Дополнительные параметры", @@ -11918,12 +12491,26 @@ "use-as": "Использовать как" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Из переменной", "label-identifier": "Идентификатор", "label-use-variable": "Использовать переменную", "label-variable": "Переменная", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Шаблон регулярного выражения" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Поле", "label-match": "Сопоставить", @@ -11933,14 +12520,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Добавить условие", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Условия", - "label-filter-type": "Тип фильтра" + "label-filter-type": "Тип фильтра", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Поле", "label-format": "Формат", "label-substring-range": "Диапазон подстрок" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Формат", "label-set-timezone": "Установить часовой пояс", @@ -11948,6 +12569,70 @@ "tooltip-format": "Формат вывода для поля, указанного в виде <2>строки в формате Moment.js.", "tooltip-timezone-manually": "Установите часовой пояс вручную" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Браузер", @@ -11975,22 +12660,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Пропущено", "placeholder-select-stats": "Выбрать статистику" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Расчеты не дадут результата, если нет сгруппированных полей" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Если параметр включен, вложенные таблицы будут отображать имена полей в качестве заголовка таблицы.", "label-show-field-names-in-nested-tables": "Показывать названия полей во вложенных таблицах", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Расчеты не дадут результата, если нет сгруппированных полей" }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Значение ячейки", "label-column": "Столбец", "label-empty-value": "Пустое значение", "label-row": "Строка", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Ложное логическое значение", @@ -12009,6 +12718,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Авто", "placeholder-default": "По умолчанию: 30", "placeholder-none": "Нет" @@ -12017,7 +12732,16 @@ "label-as-percentile": "В виде процентиля", "tooltip-transform-index-percentile": "Преобразовать индекс строки в процентиль." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Ввод не содержит меток", "no-input-or-labels-found": "Не найден ввод (или метки)", @@ -12039,10 +12763,30 @@ "label-labels": "Метки", "label-mode": "Режим", "label-value-field-name": "Название поля значений", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "Выбрать метку (необязательно)", "tooltip-replace-value-field-label": "Замените имя поля значений меткой" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Значение или переменная" }, "long-info": { @@ -12053,7 +12797,13 @@ "string-values-separate": "Строковые значения представлены в виде отдельных полей, а не меток" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Слияние не дает результата, если применяется к одиночному фрейму." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Слияние не дает результата, если применяется к одиночному фрейму.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Все значения являются числовыми", @@ -12063,10 +12813,16 @@ "time-in-ascending-order": "Время в порядке возрастания" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Авто", "field-order-manual": "", - "first-frame-warning": "Функция упорядочивания полей работает только с одиночным фреймом. Попробуйте сначала применить преобразование соединения или отфильтровать ввод." + "first-frame-warning": "Функция упорядочивания полей работает только с одиночным фреймом. Попробуйте сначала применить преобразование соединения или отфильтровать ввод.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Устанавливает способ отображения названий выбранных полей. Поскольку название фрейма обычно лучше подходит для табличных данных" @@ -12091,9 +12847,41 @@ "partition-values-works-single-frame": "Разделение по значениям работает только с одиночным фреймом.", "tooltip-keeps-partition-fields-frames": "Сохраняет поля разделов в фреймах" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Формат", - "label-info": "Информация" + "label-info": "Информация", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "и", @@ -12105,6 +12893,9 @@ "label-operation": "Операция" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Расчеты", "label-include-time": "Включить время", "label-labels-to-fields": "Метки к полям", @@ -12119,12 +12910,23 @@ "series-to-rows": "Ряды в строки" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Выбрать статистику" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Значение или переменная", "variable-regex-documentation": "Форматирует многозначную переменную в строку регулярного выражения" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Кубическая", @@ -12146,6 +12948,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Количество точек X, Y для прогнозирования" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Сопоставить", "label-replace": "Заменить", @@ -12154,11 +12964,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Поле", - "label-mode": "Режим" + "label-mode": "Режим", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Поле", "label-reverse": "Изменить на обратное", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Выбрать поле" }, "supplier": { @@ -12178,6 +13014,17 @@ "transform": "Преобразовать" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Статистика", "label-time-field": "Поле времени", @@ -12191,10 +13038,34 @@ "placeholder-value": "Значение", "tooltip-name-for-value-fields": "Имя для полей значений" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Операция", "placeholder-field": "Поле" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Несколько полей значений любого типа", "single-frame": "Одиночный фрейм", @@ -12247,6 +13118,7 @@ "title": "Организации" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Указатель пользователей", "select-placeholder": "Начните вводить текст для поиска пользователя" }, @@ -12332,6 +13204,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Удаление переменной" }, "create-ad-hoc-variable-adapter": { @@ -12380,9 +13254,34 @@ "label-refresh": "Обновить" }, "query-variable-sort-select": { - "description-values-variable": "Как сортировать значения этой переменной." + "description-values-variable": "Как сортировать значения этой переменной.", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "значение по умолчанию, если имеется", "text-options": "Параметры текста" }, @@ -12411,6 +13310,8 @@ "description-optional-display-name": "Отображаемое имя (необязательно)", "description-template-variable-characters": "Название переменной шаблона. (Макс. 50 символов.)", "general": "Общие сведения", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Поясняющий текст", "placeholder-label-name": "Название метки", "placeholder-variable-name": "Название переменной", @@ -12425,9 +13326,15 @@ "tooltip-duplicate-variable": "Дублировать переменную", "tooltip-remove-variable": "Удалить переменную" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Переключить все значения" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Показать варианты использования" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 55114795598..baba4d47c57 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Vissa funktioner är stabila (GA) och aktiverade som standard, medan vissa för närvarande är i sin preliminära betafas och finns tillgängliga för tidig tillämpning.", "confirm-modal-body-2": "Vi rekommenderar att du ser till att du förstår konsekvenserna av varje funktionsändring innan du gör några ändringar.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Allmän tillgänglighet", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Radera org", + "confirmText-delete": "", "title-delete": "Radera" }, "anon-users": { "not-found": "Inga anonyma användare hittades." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Tvinga utloggning från alla enheter" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Du har inte behörighet att se användare i denna organisation. Kontakta serveradministratören om du behöver uppdatera den här organisationen.", "heading": "Redigera organisation", @@ -208,9 +216,11 @@ "not-editable": "Denna användares roll kan inte redigeras eftersom den synkroniseras från din autentiseringsleverantör. Se <1>Grafanas autentiseringsdokumentation för mer information." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Roll" }, + "confirmText-delete": "", "delete-aria-label": "Radera användare: {{name}}", "title-delete": "Radera" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Dessa systeminställningar definieras i grafana.ini eller custom.ini (eller åsidosätts genom ENV-variabler). För att ändra dessa måste du för närvarande starta om Grafana." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Företagslicens" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Ändra", + "confirmText-change": "", "grafana-admin-key": "Grafana Admin", "grafana-admin-no": "Nej", "grafana-admin-yes": "Ja", "title": "Behörigheter" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Radera användare", "disable-button": "Inaktivera användare", "edit-button": "Redigera", @@ -312,6 +330,9 @@ "title-delete-user": "Radera användare", "title-disable-user": "Inaktivera användare" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Webbläsare och operativsystem", "force-logout-all-button": "Tvinga utloggning från alla enheter", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Aktiva tidsinställningar" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Lägg till fler" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Tystnad, gruppering och tidsinställningar (valfritt)", "title-muting-grouping-and-timings": "Tystnad, gruppering och tidsinställningar" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Kopiera länk", "duplicate": "Dubblett", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "Redigera YAML", @@ -546,6 +577,7 @@ "view-configuration": "Visa konfiguration" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Den interna konfigurationen för Grafana Alertmanager kan inte ändras manuellt. Ändra den här konfigurationen genom att redigera de enskilda resurserna via användargränssnittet.", "gma-manual-configuration-is-not-supported": "Manuella konfigurationsändringar stöds inte", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Återställer konfigurationen för Alertmanager" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Jämför", "restore": "Återställ", "text-latest": "Senaste" }, + "confirmText-yes-restore-configuration": "", "loading": "Laddar …", "no-previous-configurations": "Inga tidigare konfigurationer", "this-might-take-a-while": "Det kan ta en stund …", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "Fler åtgärder för kontaktpunkten ”{{contactPointName}}”", + "ariaLabel-delete": "", "button-edit": "Redigera", "button-view": "Visa", + "export-ariaLabel-export": "", "export-label-export": "Exportera", "label-delete": "Ta bort", "label-manage-permissions": "Hantera behörigheter", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Inaktivera löst meddelande" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Måste vara ett positivt heltal.", "must-enter-a-group-name": "Måste ange ett gruppnamn" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Fel vid laddning av information för datakälla", "edit": "Redigera", - "evaluations-to-fire": "Utvärderingscykler att utlösa", "export": "Exportera", "folder": "Mapp", "group-loading-error": "Fel vid laddning av gruppen", "interval": "Intervall", "namespace": "Namnområde", - "pending-period": "Väntande period", - "recording": "Registrering", - "rule-name": "Regelns namn" + "new": "" }, "group-edit": { "ds-error": "Fel vid laddning av information för datakälla", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Kom igång nu", "label-insights": "Insikter", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Varnar" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Andra datakällor" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Inaktiverad", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "Aviseringspolicyträdet har uppdaterats av en annan användare.", "error-code": "Felmeddelande: ”{{error}}”", - "fallback": "Något gick fel när du uppdaterade dina aviseringspolicyer.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Uppdatera sidan och försök igen.", - "title": "Fel när aviseringspolicy skulle sparas" + "title": "" }, "n-more-policies_one": "{{count}} ytterligare policyer", "n-more-policies_other": "{{count}} ytterligare policyer" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Lägg till fråga", "body-queries-expressions-configured": "Skapa minst en fråga eller ett uttryck att bli larmad för", + "confirmText-deactivate": "", "expressions": "Uttryck", "loading-data-sources": "Laddar datakällor …", "manipulate-returned-queries-other-operations": "Manipulera data som returneras från frågor med matematikfunktioner och andra operationer.", @@ -2184,6 +2237,9 @@ "label-export-all": "Exportera alla" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Kunde inte ladda frågeredigerare på grund av: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Du måste ställa in en ny utvärderingsgrupp för den kopierade regeln eftersom den ursprungliga har provisionerats och inte kan användas för regler som skapats i användargränssnittet.", "body-not-provisioned": "Den nya regeln kommer <1>inte att markeras som en provisionerad regel.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Kopiera provisionerad larmregel" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Inspektera larmregel" }, "rule-list": { - "cannot-find-rule-details-for": "Kan inte hitta regelinformation för UID {{uid}}", - "cannot-load-rule-details-for": "Kunde inte ladda regelinformation för UID {{uid}}", "configure-datasource": "Konfigurera", "draft-new-rule": "Skapa utkast till en ny regel", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "Hittar du inte den datakälla du söker? Vissa datakällor stöds inte för larm. Klicka på ikonen för mer information." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Lägg till ny Alertmanager", "builtin-alertmanager": "Inbyggd Alertmanager", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Välj aviseringsmall", "loading": "Laddar …", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Välj aviseringsmall" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "Åtgärder", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Inga mallar definierade.", "template-group": "Mallgrupp", "title-delete-template-group": "Radera mallgrupp" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} sedan)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Målmappen är inte tom, vissa regler kan skrivas över eller tas bort. Är du säker på att du vill importera dessa larmregler till Grafana-hanterade regler?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "Radera kontaktpunkt" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Radera aviseringspolicy" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Kommentar", "first-value": "Första värde", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Lägg till kommenteringsfråga", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Läs <2>Kommentarsdokumentationen för mer information.", "title": "Inga anpassade kommentarsfrågor har lagts till ännu" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} händelser (från {{numFields}} fält)", "no-events-found": "Inga händelser hittades", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "Definiera tillåtna team-ID", "display-name-description": "Kommer att visas på inloggningssidan som ”Logga in med …” Användbart om du använder fler än en identitetsleverantör eller SSO-protokoll.", "display-name-label": "Visningsnamn", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "Namn på nyckeln som ska användas för användarens e-postsökning inom attributkartan för OAuth2 ID-token.", "email-attribute-name-label": "E-postattributnamn", "email-attribute-path-description": "JMESPath-uttryck som ska användas för användarens e-postsökning från användarinformationen.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Heltalslista över team-ID:n.", "team-ids-label": "Team-ID:n", "team-ids-numbers": "Team-ID:n måste vara siffror.", - "team-ids-other": "Stränglista över team-ID:n.", + "team-ids-other": "", "team-ids-placeholder": "Ange team-ID och tryck på returtangenten för att lägga till", "teams-url-description": "Webbadressen som används för att fråga efter team-ID:n. Om det inte anges är standardvärdet /teams.", "teams-url-description-oauth": "Om du konfigurerar ”{{ teamsURLLabel }}” måste du även konfigurera ”{{ teamIDsAttributePathLabel }}”.", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Återställ till standardvärden" }, + "confirmText-reset": "", "disable": "Inaktivera", "disabling": "Inaktiverar …", "discard": "Kassera", @@ -3473,11 +3563,11 @@ "button-create": "Skapa", "button-creating": "Skapar …", "cancel": "Avbryt", - "error-invalid-folder-name": "Ogiltigt mappnamn", "error-required": "Mappnamn krävs", "folder-name-input-placeholder-enter-folder-name": "Ange mappnamn", "label-folder-name": "Mappnamn", "text-pull-request-created": "En pull-begäran har skapats med ändringar i den här mappen:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull-begäran skapad", "title-this-repository-is-read-only": "Den här databasen är skrivskyddad" }, @@ -3895,6 +3985,7 @@ "tooltip": "Stäng" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Ett fel uppstod vid inläsning av alternativ." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Beräknar intervallet dynamiskt genom att dividera tidsintervallet med det angivna antalet" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Fortsätt till extern webbplats?" } }, "render-left-actions": { - "text-public": "Offentlig", "tooltip-view-as-scene": "Visa som scen" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Det gick inte att ladda instrumentpanelen" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Det gick inte att hämta instrumentpanel" @@ -4540,6 +4641,13 @@ "editable": "Redigerbar", "readonly": "Skrivskyddad" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Transformera data" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Är du säker på att du vill återställa instrumentpanelen till version {{version}}? Alla ändringar som inte sparats kommer att gå förlorade.", + "confirmText-restore-version": "", "title-restore-version": "Återställ versionen" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Den här titeln är inte unik" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Spara som" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "En instrumentpanel med samma namn finns redan i den valda mappen. <1><2>Vill du fortfarande spara denna instrumentpanel?", "body-version-mismatch": "Någon annan har uppdaterat denna instrumentpanel<1><2>Vill du fortfarande spara denna instrumentpanel?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Konflikt", "title-version-mismatch": "Konflikt" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Gå till nästa instrumentpanel", "playlist-previous": "Gå till föregående instrumentpanel", "playlist-stop": "Stoppa spellista", - "public-dashboard": "Offentlig", "refresh": "Uppdatera instrumentpanel", "save": "Spara instrumentpanel", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Tillämpa transformering på" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Felsök", "title-disable-transformation": "Inaktivera transformering", "title-filter": "Filtrera", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Sök efter transformering", "show-images": "Visa bilder", - "title-add-another-transformation": "Lägg till ytterligare en transformering" + "title-add-another-transformation": "Lägg till ytterligare en transformering", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Lägg till ytterligare en transformering" }, + "confirmText-delete-all": "", "delete-all-transformations": "Radera alla transformeringar", "title-delete-all-transformations": "Radera alla transformeringar?", "tooltip-clear-search": "Rensa sökningen", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "Växla val av version {{version}}", "date": "Datum", + "name-latest": "", "notes": "Anteckningar", "restore": "Återställ", "updated-by": "Uppdaterad av", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Gör det möjligt för användare att lägga till anpassade värden i listan", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Tillhandahåll dimensioner som CSV: {{name}}, {{value}}", "label-data-source": "Datakälla", - "label-use-static-key-dimensions": "Använd dimensioner för statiska nycklar" + "label-use-static-key-dimensions": "Använd dimensioner för statiska nycklar", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Återkalla offentlig webbadress" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Anpassade alternativ", + "name-values-separated-comma": "", "selection-options": "Urvalsalternativ" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Typ", "label-url": "URL", "label-with-tags": "Med taggar", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Öppna instrumentpanel" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Typ" }, "dashboard-preview-banner": { - "not-saved": "Värdet har ännu inte sparats i Grafana-databasen", "not-yet-saved": "Värdet sparades inte i Grafana-databasen", - "open-pull-request-in-git-hub": "Öppna pull-begäran i GitHub", - "title-dashboard-loaded-branch-git-hub": "Den här instrumentpanelen laddas från en gren i GitHub.", "title-dashboard-loaded-external-repository": "Den här instrumentpanelen laddas från ett externt arkiv", - "title-dashboard-loaded-request-git-hub": "Den här instrumentpanelen laddas från en pull-begäran i GitHub.", - "title-error-loading-dashboard": "Fel vid laddning av instrumentpanel", - "value-not-saved": "Värdet har ännu inte sparats i Grafana-databasen", - "view-pull-request-in-git-hub": "Visa pull-begäran i GitHub" + "title-error-loading-dashboard": "Fel vid laddning av instrumentpanel" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Alternativ för datakälla", "description-instance-name-filter": "Regex-filter för vilka datakällsinstanser att välja mellan i variabelvärdelistan. Lämna tomt för alla.", "example-instance-name-filter": "Exempel: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Urvalsalternativ" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Lägg till omvandling" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Kolumnalternativ", "row-height-options": "Alternativ för radhöjd" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Tillhandahåll dimensioner som CSV: {{name}}, {{value}}", "group-by-options": "Gruppera efter alternativ", "label-data-source": "Datakälla", - "label-use-static-group-by-dimensions": "Använd statiska gruppdimensioner" + "label-use-static-group-by-dimensions": "Använd statiska gruppdimensioner", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Kopiera till urklippet", @@ -5521,9 +5710,14 @@ "apply": "Tillämpa" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Det beräknade värdet kommer inte att vara under denna tröskel", "description-step-count": "Hur många gånger det aktuella tidsintervallet ska delas för att beräkna värdet", - "interval-options": "Intervallalternativ" + "interval-options": "Intervallalternativ", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "Används på {{count}} instrumentpaneler", "usage-count_other": "Används på {{count}} instrumentpaneler" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "En instrumentpanel med samma namn finns redan i den valda mappen. Vill du fortfarande spara denna instrumentpanel?", "title-name-already-exists": "Namnet finns redan" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Fortsätt till extern webbplats?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Lägg till ytterligare en transformering", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Radera alla transformeringar", "title-delete-all-transformations": "Radera alla transformeringar?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Namngivna fångstgrupper kan användas för att separera visningstexten och värdet (<1>se exempel).", "description-optional": "Valfritt, om du vill extrahera en del av ett serienamn eller metriskt nodsegment.", "label-data-source": "Datakälla", + "label-static-options-sort": "", "label-target-data-source": "Måldatakälla", + "label-use-static-options": "", + "name-regex": "", "query-options": "Frågealternativ", - "selection-options": "Urvalsalternativ" + "selection-options": "Urvalsalternativ", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Är du säker på att du vill återställa instrumentpanelen till version {{version}}? Alla ändringar som inte sparats kommer att gå förlorade.", + "confirmText-restore-version": "", "title-restore-version": "Återställ versionen" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Mapp", "render-footer": { "title-failed-to-save-dashboard": "Det gick inte att spara instrumentpanel" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Gör det möjligt att välja flera värden samtidigt", "description-enables-option-include-variables": "Aktiverar ett alternativ för att inkludera alla värden", - "description-enables-users-custom-values": "Gör det möjligt för användare att lägga till anpassade värden i listan" + "description-enables-users-custom-values": "Gör det möjligt för användare att lägga till anpassade värden i listan", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Växla delningsmenyn" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Kopiering till urklipp misslyckades" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(valfritt)", "text-options": "Textalternativ" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Vill du verkligen avlänka den här panelen?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Den här variabeln refereras inte av någon variabel eller instrumentpanel.", "aria-label-variable-referenced-other-variables-dashboard": "Den här variabeln hänvisas till av andra variabler eller instrumentpanelen.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Formulär för variabelsredigering", "back-to-list": "Tillbaka till listan", + "confirmText": { + "delete-variable": "" + }, "delete": "Ta bort", "description-optional-display-name": "Valfritt visningsnamn", "description-template-variable-characters": "Namnet på mallvariabeln. (Högst 50 tecken)", "general": "Allmänt", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Beskrivande text", "placeholder-label-name": "Etikettnamn", "placeholder-variable-name": "Variabelnamn", @@ -5829,13 +6066,25 @@ "variable": "Variabel" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Radera variabel", "tooltip-duplicate-variable": "Dubblettvariabel", "tooltip-remove-variable": "Ta bort variabel" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Dölj" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "Visar användningar för: {{variableId}}", "tooltip-show-usages": "Visa användningar" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "Växla val av version {{version}}", "date": "Datum", + "name-latest": "", "notes": "Anteckningar", "restore": "Återställ", "updated-by": "Uppdaterad av", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Ladda upp" @@ -6287,6 +6538,7 @@ }, "label-limit": "Gräns", "label-value": "Värde", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Max", "label-min": "Min", - "label-value": "Värde" + "label-value": "Värde", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Välj operator för tjänstnamn", "aria-label-select-span-name": "Välj namn på intervall", "aria-label-select-span-name-operator": "Välj operator för intervallnamn", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Intervallfilter", "label-duration": "Varaktighet", "label-service-name": "Namn på tjänst", @@ -6939,6 +7199,8 @@ "split-widen": "Bredda rutan" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Ge feedback", "label-copied": "Kopierad! ", "label-export": "Exportera", @@ -7046,6 +7308,7 @@ "when": "NÄR" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Drift" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Rensa mappar", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Mappfilter", "select-placeholder": "Filtrera efter mapp" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Tyvärr kunde jag inte slutföra din begäran. Försök igen.", "send-custom-feedback": "Skicka" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Latitud", "label-longitude": "Longitud" @@ -7153,6 +7463,14 @@ "center": "Centrera:", "zoom": "Zooma:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Lager" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Lägg till geomap-stilregel" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Lägg till lager", "no-layers": "Inga lager?" @@ -7185,16 +7511,38 @@ "label-zoom": "Zooma", "use-current-map-settings": "Använd aktuella kartinställningar" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Symbol" }, "measure-overlay": { "tooltip-show-measure-tools": "Visa mätverktyg" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Grundkartlagret konfigureras av serveradministratören." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Justera", "label-baseline": "Baslinje", "label-color": "Färg", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Vertikal justering av symbol", "label-text-label": "Textetikett", "label-x-offset": "X-förskjutning", - "label-y-offset": "Y-förskjutning" + "label-y-offset": "Y-förskjutning", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Jämförelseoperator", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Funktionsegenskap", "placeholder-numeric-value": "Numeriskt värde", "placeholder-value": "värde" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "{{colorLabel}} färg" }, "confirm-button": { - "cancel": "Avbryt" + "cancel": "Avbryt", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Skriv ”{{confirmPromptText}}” för att bekräfta" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "växla dölj panel", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Avbryt fråga", "tooltip-cancel-loading": "Avbryt fråga", "tooltip-stop-streaming": "Avsluta streaming", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Matcha gemener/versaler", "inspect-drawer-title": "Inspektera värde", "inspect-menu-label": "Inspektera värde", + "nested-table": { + "no-data": "" + }, "no-values-label": "Inga värden", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} av {{numRows}} rader" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Lägg till", + "placeholder-new-tag": "", "remove": "Ta bort tagg: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "Klicka för att {{actionTitle}}", "footer-click-to-navigate": "Klicka för att öppna {{linkTitle}}", "timestamp": "Tidsstämpel" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Skapa bibliotekspanel" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Vill du radera denna panel?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "Används på {{count}} instrumentpaneler" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Sök efter namn eller beskrivning" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Laddar bibliotekspanel …" @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Skapa en personlig direktlänk för att dela din panel inom din organisation, med följande anpassningsinställningar:", "download-image": "Ladda ner bild", - "render-image": "Generera bild", - "render-image-error": "Det gick inte att rendera panelbild", - "render-image-error-description": "Ett fel uppstod vid generering av bilden" + "render-image": "Generera bild" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Radbryt linjer" }, "log-details": { - "close": "Stäng loggdetaljer", "fields": "Fält", "links": "Länkar", "log-line": "Loggrad", @@ -8405,6 +8795,42 @@ "show-more": "visa mer", "tooltip-error": "Fel: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Kopiera länk till loggrad", "copy-log": "Kopiera loggrad", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Datalänkar", "aria-label-fields": "Fält", + "aria-label-line": "", "aria-label-log-level": "Loggnivå", "aria-label-no-details": "Inga uppgiftger" }, @@ -8598,6 +9030,8 @@ "updated-on": "Uppdaterat den" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Ta bort" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Importera instrumentpanel" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Det här verktyget kan migrera vissa resurser från den här installationen till din molnstack. För att komma igång måste du skapa en ögonblicksbild av den här installationen. Att skapa en ögonblicksbild tar vanligtvis mindre än två minuter. Ögonblicksbilden lagras tillsammans med den här Grafana-installationen.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "OK", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Instrumentpanelen tillhandahålls och hanteras redan av Grafana i molninstansen. Vi rekommenderar att du använder den tillhandahållna instrumentpanelen framöver. Om du fortfarande vill kopiera instrumentpanelens molninstans ändrar du instrumentpanelens ID i instrumentpanelens JSON, sparar en ny ögonblicksbild och laddar sedan upp på nytt.", "datasource-already-managed": "Datakällan tillhandahålls och hanteras redan av Grafana i molninstansen. Om detta är en annan resurs, ange ett annat UID och försök igen.", "datasource-invalid-url": "Det finns en datakälla som har en ogiltig webbadress. Ange en giltig webbadress och försök igen.", @@ -9307,7 +9752,7 @@ "marker": { "100-node-count": "> 100 noder", "aria-label-hidden-marker": "Dold nodmarkör: {{marker}}", - "node-count_one": "{{count}} nod", + "node-count_one": "{{count}} noder", "node-count_other": "{{count}} noder" }, "node": { @@ -9318,9 +9763,9 @@ "aria-label-nodes-hidden-warning": "Varning för dolda noder", "computing-layout": "Beräknar layout", "no-data": "Inga data", - "hidden-nodes_one": "<0> {{count}} nod är dold av prestandaskäl.", + "hidden-nodes_one": "<0> {{count}} noder är dolda av prestandaskäl.", "hidden-nodes_other": "<0> {{count}} noder är dolda av prestandaskäl.", - "processed-nodes_one": "<0> Skiktad layout kan vara långsam med {{count}} nod.", + "processed-nodes_one": "<0> Skiktad layout kan vara långsam med {{count}} noder.", "processed-nodes_other": "<0> Skiktad layout kan vara långsam med {{count}} noder." }, "node-graph-panel": { @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Välj organisation" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Du saknar behörighet att visa den här sidan.", "title-access-denied": "Åtkomst nekad" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Ingen komponent för rotapplikationssidan hittades" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "plugins uppdaterade" }, "versions": { - "confirmation-text-1": "Är du verkligen säker på att du vill nedgradera till version", - "confirmation-text-2": "Du borde normalt inte göra detta", + "confirmation-text": "", "downgrade-confirm": "Nedgradera", "downgrade-title": "Nedgradera tilläggsversion" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Inga tilläggsprogram hittades" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "OK" @@ -9800,7 +10250,11 @@ "updating": "Uppdatering" }, "install-controls-button": { - "title-uninstall-modal": "Avinstallera {{plugin}}" + "title-uninstall-modal": "Avinstallera {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Detta tillägg har inte publicerat på <2>grafana.com/plugins och kan inte hanteras via katalogen.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Den här funktionen är för närvarande under aktiv utveckling. För den bästa upplevelsen och de senaste förbättringarna rekommenderar vi att du använder <2> nightly-versionen av Grafana." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Servicekontoväljare", "select-placeholder": "Börja skriva för att söka efter servicekonton" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Lägg till servicekontotoken", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Radera servicekonto", "disable-service-account": "Inaktivera servicekonto", "enable-service-account": "Aktivera servicekonto", @@ -10907,6 +11375,7 @@ "used-by": "Använd av" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Redigera" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Lägg till servicekonto", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Sök servicekonto efter namn", "sub-title": "Servicekonton och deras token kan användas för att autentisera mot Grafana API. Läs mer i vår <2>dokumentation.", "title-delete-service-account": "Radera servicekonto", - "title-disable-service-account": "Inaktivera servicekonto" + "title-disable-service-account": "Inaktivera servicekonto", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Denna token har löpt ut", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "Exportera som JSON", "share-externally-title": "Dela externt", "share-internally-title": "Dela internt", @@ -10972,6 +11448,19 @@ "view-button": "Visa JSON", "view-button-yaml": "Visa YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Skapa bibliotekspanel." }, @@ -10980,7 +11469,7 @@ "info-text": "Skapa en direktlänk till denna instrumentpanel eller panel, anpassad med alternativen nedan.", "link-url": "Länkens webbadress", "render-alert": "Tilläggsprogram för bildrendering inte installerat", - "render-instructions": "För att rendera en panelbild måste du installera <2>Grafanas bildrenderingstillägg. Kontakta din Grafana-administratör för installation av tillägget.", + "render-instructions": "", "rendered-image": "Direktlänk till renderad bild", "save-alert": "Instrumentpanelen sparades inte", "save-dashboard": "För att rendera en panelbild måste du spara instrumentpanelen först.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Panelförhandsgranskning" }, @@ -11315,7 +11810,7 @@ "label-never": "" }, "status-history-panel": { - "too-many-points_one": "För många punkter för att visualisera ordentligt. <1>Uppdatera frågan för att returnera färre punkter. <3>({{count}} punkt mottagen)", + "too-many-points_one": "För många punkter för att visualisera ordentligt. <1>Uppdatera frågan för att returnera färre punkter. <3>({{count}} punkter mottagna)", "too-many-points_other": "För många punkter för att visualisera ordentligt. <1>Uppdatera frågan för att returnera färre punkter. <3>({{count}} punkter mottagna)" } }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Alternativ text som kommer att visas om en bild inte kan visas eller för användare som använder en skärmläsare", "description-title-text": "Text som visas när muspekaren hålls över bilden", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Etikettalternativ" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Teamväljare", "select-placeholder": "Välj ett team" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Fält eller nummer" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Alias", "label-mode": "Läge", "label-replace-all-fields": "Ersätt alla fält" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Etikett", "label-name": "Namn", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Ram" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Lägg till en transformering för att konvertera fälttyp", "aria-label-remove-convert-field-type-transformer": "Ta bort transformering för att konvertera fälttyp", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Webbläsare", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Indataformat", "label-join-with": "Sammanfoga med", "label-set-timezone": "Ange tidszon", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Typ", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Använd en tydlig avgränsare vid sammanfogning av matrisvärden", "tooltip-input-format": "Ange formatet för inmatningsfältet så att Grafana kan tolka datumsträngen korrekt.", "tooltip-specify-the-output-format": "Ange utdataformat.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Av" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Lägg till uppräkningsvärde", "generate-enum-values-from-data": "Generera uppräkningsvärde från data" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Radera" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Avgränsare", "label-format": "Format", "label-keep-time": "Registrera tid", "label-replace-all-fields": "Ersätt alla fält", "label-source": "Källa", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Välj avgränsare …", "tooltip-regexp": "Exempel: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Tröskelfärg" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Fält", - "label-lookup": "Slå upp" + "label-lookup": "Slå upp", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Ytterligare inställningar", @@ -11848,12 +12413,26 @@ "use-as": "Använd som" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Från variabel", "label-identifier": "Identifierare", "label-use-variable": "Använd variabel", "label-variable": "Variabel", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Vanligt uttrycksmönster" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Fält", "label-match": "Match", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Lägg till villkor", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Villkor", - "label-filter-type": "Filtertyp" + "label-filter-type": "Filtertyp", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Fält", "label-format": "Format", "label-substring-range": "Delsträngsintervall" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Ange tidszon", @@ -11878,6 +12491,70 @@ "tooltip-format": "Utdataformatet för fältet anges som en <2>Moment.js-formatsträng.", "tooltip-timezone-manually": "Ställ in tidszonen för datumet manuellt" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Webbläsare", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Ignorerad", "placeholder-select-stats": "Välj statistik" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Beräkningar kommer inte att ha någon effekt om inga fält grupperas efter" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Om det är aktiverat kommer kapslade tabeller att visa fältnamn som en tabellrubrik", "label-show-field-names-in-nested-tables": "Visa fältnamn i kapslade tabeller", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Beräkningar kommer inte att ha någon effekt om inga fält grupperas efter." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Cellvärde", "label-column": "Kolumn", "label-empty-value": "Tomt värde", "label-row": "Rad", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Booleskt falskt värde", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Auto", "placeholder-default": "Standard: 30", "placeholder-none": "Inget" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Som percentil", "tooltip-transform-index-percentile": "Omvandla radindexet som en percentil." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Inmatningen innehåller inte några etiketter", "no-input-or-labels-found": "Inga inmatningar (eller etiketter) hittades", @@ -11969,10 +12685,30 @@ "label-labels": "Etiketter", "label-mode": "Läge", "label-value-field-name": "Namn på värdefält", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(Valfritt) Välj etikett", "tooltip-replace-value-field-label": "Ersätt värdefältets namn med en etikett" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Värde eller variabel" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Strängvärden representeras som separata fält snarare än som etiketter" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Sammanfogning har ingen effekt när den tillämpas på en enda ram." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Sammanfogning har ingen effekt när den tillämpas på en enda ram.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Alla värden är numeriska", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Tid i stigande ordning" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Auto", "field-order-manual": "", - "first-frame-warning": "Organisera fält fungerar endast med en enda ram. Överväg att tillämpa en kopplingstransformation eller filtrera indata först." + "first-frame-warning": "Organisera fält fungerar endast med en enda ram. Överväg att tillämpa en kopplingstransformation eller filtrera indata först.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Anger hur namnen på de valda fälten ska visas. Eftersom ramnamn vanligtvis är bättre för tabelldata" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Partitionering med värden fungerar endast med en enda ram.", "tooltip-keeps-partition-fields-frames": "Håller partitionsfälten inom ramarna" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Information" + "label-info": "Information", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "och", @@ -12035,6 +12815,9 @@ "label-operation": "Drift" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Beräkningar", "label-include-time": "Inkludera tid", "label-labels-to-fields": "Etiketter till fält", @@ -12049,12 +12832,23 @@ "series-to-rows": "Serie till rader" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "Välj statistik" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Värde eller variabel", "variable-regex-documentation": "Formatera flervärdesvariabel till en regex-sträng" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Kubik", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Antal X,Y-poäng att förutsäga" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Match", "label-replace": "Byt ut", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Fält", - "label-mode": "Läge" + "label-mode": "Läge", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Fält", "label-reverse": "Omvänt", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Välj fält" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Omvandla" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Tidsfält", @@ -12121,10 +12960,34 @@ "placeholder-value": "Värde", "tooltip-name-for-value-fields": "Namn för värdefält" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Drift", "placeholder-field": "Fält" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Flera värdefält av valfri typ", "single-frame": "Enskild ram", @@ -12177,6 +13040,7 @@ "title": "Organisationer" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Användarväljare", "select-placeholder": "Börja skriva för att söka efter användare" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Radera variabel" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Uppdatera" }, "query-variable-sort-select": { - "description-values-variable": "Hur värdena för denna variabel ska sorteras" + "description-values-variable": "Hur värdena för denna variabel ska sorteras", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "standardvärde, om något", "text-options": "Textalternativ" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "Valfritt visningsnamn", "description-template-variable-characters": "Namnet på mallvariabeln. (Högst 50 tecken)", "general": "Allmänt", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Beskrivande text", "placeholder-label-name": "Etikettnamn", "placeholder-variable-name": "Variabelnamn", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Dubblettvariabel", "tooltip-remove-variable": "Ta bort variabel" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Växla alla värden" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Visa användningar" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index af48be053be..a09aae9370a 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "Bazı özellikler kararlı sürümdedir (Genel Kullanım/GA) ve varsayılan olarak etkindir, bazıları ise henüz ön aşamada olan Beta aşamasındadır ve erken benimseme için kullanılabilir durumdadır.", "confirm-modal-body-2": "Özelliklerde değişiklik yapmadan önce etkilerini gözden geçirmenizi öneririz.", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "Genel kullanılabilirlik", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "Kuruluşu sil", + "confirmText-delete": "", "title-delete": "Sil" }, "anon-users": { "not-found": "Anonim kullanıcı bulunamadı." }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "Tüm cihazlardan çıkış yapmaya zorla" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "Bu kuruluştaki kullanıcıları görme izniniz yok. Bu kuruluşu güncellemek için sunucu yöneticinize başvurun.", "heading": "Kuruluşu Düzenle", @@ -208,9 +216,11 @@ "not-editable": "Bu kullanıcının rolü, kimlik doğrulama sağlayıcınızdan eşitlendiği için düzenlenemez. Ayrıntılar için <1>Grafana kimlik doğrulama belgelerine bakın." }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "Rol" }, + "confirmText-delete": "", "delete-aria-label": "Kullanıcıyı sil: {{name}}", "title-delete": "Sil" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "Bu sistem ayarları, grafana.ini veya custom.ini dosyalarında tanımlanmıştır (veya ENV değişkenlerinde geçersiz kılınabilir). Bu ayarları değiştirmek için şu anda Grafana'yı yeniden başlatmanız gerekmektedir." }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "Enterprise lisansı" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "Değiştir", + "confirmText-change": "", "grafana-admin-key": "Grafana Yöneticisi", "grafana-admin-no": "Hayır", "grafana-admin-yes": "Evet", "title": "İzinler" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "Kullanıcıyı sil", "disable-button": "Kullanıcıyı devre dışı bırak", "edit-button": "Düzenle", @@ -312,6 +330,9 @@ "title-delete-user": "Kullanıcıyı sil", "title-disable-user": "Kullanıcıyı devre dışı bırak" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "Tarayıcı ve işletim sistemi", "force-logout-all-button": "Tüm cihazlardan çıkış yapmaya zorla", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "Aktif zamanlamalar" }, + "ad": { + "close": "", + "learn-more": "" + }, "add-button": { "add-more": "Daha fazlasını ekle" }, @@ -457,6 +482,9 @@ "label-muting-grouping-and-timings-optional": "Sessize alma, gruplandırma ve zamanlamalar (isteğe bağlı)", "title-muting-grouping-and-timings": "Sessize alma, gruplandırma ve zamanlamalar" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "Bağlantıyı kopyala", "duplicate": "Çoğalt", @@ -480,6 +508,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "YAML düzenle", @@ -546,6 +577,7 @@ "view-configuration": "Yapılandırmayı görüntüle" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "Grafana'nın dahili Alertmanager yapılandırması manuel olarak değiştirilemez. Bu yapılandırmayı değiştirmek için bireysel kaynakları kullanıcı arayüzü üzerinden düzenleyin.", "gma-manual-configuration-is-not-supported": "Manuel yapılandırma değişiklikleri desteklenmiyor", "message": { @@ -560,11 +592,13 @@ "title-resetting-alertmanager-configuration": "Alertmanager yapılandırması sıfırlanıyor" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "Karşılaştır", "restore": "Geri yükle", "text-latest": "En son" }, + "confirmText-yes-restore-configuration": "", "loading": "Yükleniyor...", "no-previous-configurations": "Önceki yapılandırmalar bulunamadı", "this-might-take-a-while": "Bu işlem biraz zaman alabilir...", @@ -844,8 +878,10 @@ }, "contact-point-header": { "aria-label-more-actions": "\"{{contactPointName}}\" iletişim noktası için daha fazla işlem", + "ariaLabel-delete": "", "button-edit": "Düzenle", "button-view": "Görüntüle", + "export-ariaLabel-export": "", "export-label-export": "Dışa aktar", "label-delete": "Sil", "label-manage-permissions": "İzinleri yönet", @@ -1380,6 +1416,7 @@ "label-disable-resolved-message": "Çözümlenmiş mesajı devre dışı bırak" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "Pozitif bir tam sayı olmalıdır.", "must-enter-a-group-name": "Bir grup adı girilmelidir" @@ -1471,15 +1508,12 @@ "group-details": { "ds-features-error": "Veri kaynağı ayrıntıları yüklenirken hata oluştu", "edit": "Düzenle", - "evaluations-to-fire": "Tetiklenecek değerlendirme döngüleri", "export": "Dışa aktar", "folder": "Klasör", "group-loading-error": "Grup yüklenirken hata oluştu", "interval": "Aralık", "namespace": "Ad alanı", - "pending-period": "Bekleme süresi", - "recording": "Kayıt", - "rule-name": "Kural adı" + "new": "" }, "group-edit": { "ds-error": "Veri kaynağı ayrıntıları yüklenirken hata oluştu", @@ -1525,8 +1559,20 @@ } }, "home": { + "irm-card-description": "", + "irm-card-item-1": "", + "irm-card-item-2": "", + "irm-card-item-3": "", + "irm-card-item-4": "", + "irm-card-title": "", "label-get-started": "Başlayalım", "label-insights": "İçgörüler", + "synthetic-monitoring-card-description": "", + "synthetic-monitoring-card-item-1": "", + "synthetic-monitoring-card-item-2": "", + "synthetic-monitoring-card-item-3": "", + "synthetic-monitoring-card-item-4": "", + "synthetic-monitoring-card-title": "", "title-alerting": "Uyarı sistemi" }, "import-to-gma": { @@ -1826,7 +1872,11 @@ "other-data-sources": "Diğer veri kaynakları" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "Devre dışı", @@ -2077,9 +2127,11 @@ "update-errors": { "conflict": "Bildirim politikası ağacı başka bir kullanıcı tarafından güncellendi.", "error-code": "Hata mesajı: \"{{error}}\"", - "fallback": "Bildirim politikalarınız güncellenirken bir hata oluştu.", + "routes": { + "conflictingMatchers": "" + }, "suffix": "Lütfen sayfayı yenileyin ve tekrar deneyin.", - "title": "Bildirim politikası kaydedilirken hata oluştu" + "title": "" }, "n-more-policies_one": "{{count}} ek politika", "n-more-policies_other": "{{count}} ek politika" @@ -2135,6 +2187,7 @@ "query-and-expressions-step": { "add-query": "Sorgu ekle", "body-queries-expressions-configured": "Uyarı oluşturabilmek için en az bir sorgu veya ifade oluşturun", + "confirmText-deactivate": "", "expressions": "İfadeler", "loading-data-sources": "Veri kaynakları yükleniyor...", "manipulate-returned-queries-other-operations": "Sorgulardan dönen verileri matematik ve diğer işlemlerle düzenleyin.", @@ -2184,6 +2237,9 @@ "label-export-all": "Tümünü dışa aktar" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "Sorgu düzenleyici şu nedenle yüklenemedi: {{errorMessage}}" }, @@ -2202,6 +2258,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "Kopyalanan kural için yeni bir değerlendirme grubu ayarlamanız gerekir çünkü orijinal kural sağlanmış ve kullanıcı arayüzünde oluşturulan kurallar için kullanılamaz.", "body-not-provisioned": "Yeni kural, sağlanmış bir kural olarak <1>işaretlenmeyecek.", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "Sağlanan uyarı kuralını kopyala" }, "redirect-to-rule-viewer": { @@ -2399,8 +2456,6 @@ "title-inspect-alert-rule": "Uyarı kuralını incele" }, "rule-list": { - "cannot-find-rule-details-for": "{{uid}} UID'si için kural bilgileri bulunamadı", - "cannot-load-rule-details-for": "{{uid}} UID'si için kural bilgileri yüklenemedi", "configure-datasource": "Yapılandır", "draft-new-rule": "Yeni bir kural taslağı oluştur", "ds-error": { @@ -2598,6 +2653,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "İstediğiniz veri kaynağını bulamıyor musunuz? Bazı veri kaynakları uyarılar için desteklenmiyor. Daha fazla bilgi için simgeye tıklayın." }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "Yeni Alertmanager ekle", "builtin-alertmanager": "Yerleşik Alertmanager", @@ -2750,6 +2810,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "Bildirim şablonu seçin", "loading": "Yükleniyor...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "Bildirim şablonu seçin" } @@ -2776,6 +2839,8 @@ }, "templates-table": { "actions": "İşlemler", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "Tanımlanmış şablon yok.", "template-group": "Şablon grubu", "title-delete-template-group": "Şablon grubunu sil" @@ -2828,6 +2893,11 @@ "timestamp": { "time-ago": "({{time}} önce)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "Hedef klasör boş değil, bazı kuralların üzerine yazılabilir veya silinebilir. Bu uyarı kurallarını Grafana tarafından yönetilen kurallara içe aktarmak istediğinize emin misiniz?", @@ -2903,6 +2973,11 @@ "title-delete-contact-point": "İletişim noktasını sil" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "Bildirim politikasını sil" @@ -3059,7 +3134,8 @@ "annotation-field-mapper": { "annotation": "Ek açıklama", "first-value": "İlk değer", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "Ek açıklama sorgusu ekle", @@ -3067,6 +3143,16 @@ "info-box-content-2": "Daha fazla bilgi için <2>Ek açıklamalar belgelerine göz atın.", "title": "Henüz eklenmiş özel ek açıklama sorgusu yok" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numFields}} olay ({{numEvents}} alandan)", "no-events-found": "Etkinlik bulunamadı", @@ -3142,6 +3228,9 @@ "define-allowed-teams-ids-label": "İzin verilen ekip kimliklerini tanımlayın", "display-name-description": "Giriş sayfasında \"Şununla giriş yap:\" olarak gösterilecektir. Birden fazla kimlik sağlayıcı veya SSO protokolü kullanıyorsanız faydalıdır.", "display-name-label": "Görünen ad", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "OAuth2 kimlik belirteci öznitelik eşlemesinde kullanıcı e-postasını aramak için kullanılacak anahtarın adı.", "email-attribute-name-label": "E-posta özniteliği adı", "email-attribute-path-description": "Kullanıcı bilgilerinden kullanıcı e-postasını aramak için kullanılacak JMESPath ifadesi.", @@ -3193,7 +3282,7 @@ "team-ids-github": "Ekip kimliklerinin tam sayı listesi.", "team-ids-label": "Ekip kimlikleri", "team-ids-numbers": "Ekip kimlikleri sayı olmalıdır.", - "team-ids-other": "Ekip Kimliklerinin dize listesi.", + "team-ids-other": "", "team-ids-placeholder": "Ekip kimliklerini girin ve eklemek için Enter tuşuna basın", "teams-url-description": "Ekip kimliklerini sorgulamak için kullanılan URL. Ayarlanmazsa varsayılan değer /teams olur.", "teams-url-description-oauth": "\"{{ teamsURLLabel }}\" alanı yapılandırıldıysa \"{{ teamIDsAttributePathLabel }}\" alanı da yapılandırılmalıdır.", @@ -3237,6 +3326,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "Varsayılan değerlere sıfırla" }, + "confirmText-reset": "", "disable": "Devre dışı bırak", "disabling": "Devre dışı bırakılıyor...", "discard": "Vazgeç", @@ -3473,11 +3563,11 @@ "button-create": "Oluştur", "button-creating": "Oluşturuluyor...", "cancel": "İptal", - "error-invalid-folder-name": "Geçersiz klasör adı", "error-required": "Klasör adı gereklidir", "folder-name-input-placeholder-enter-folder-name": "Klasör adını girin", "label-folder-name": "Klasör adı", "text-pull-request-created": "Bu klasörde yapılan değişikliklerle bir çekme isteği oluşturuldu:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Çekme isteği oluşturuldu", "title-this-repository-is-read-only": "Bu depo salt okunur" }, @@ -3895,6 +3985,7 @@ "tooltip": "Kapat" }, "cloud-enterprise-feature-badge": "", + "cloud-feature-badge": "", "combobox": { "async": { "error": "Seçenekler yüklenirken bir hata oluştu." @@ -4163,8 +4254,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "Zaman aralığını belirtilen sayıya bölerek aralığı dinamik olarak hesaplar" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4326,12 +4417,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Haricî siteye geçilsin mi?" } }, "render-left-actions": { - "text-public": "Herkese açık", "tooltip-view-as-scene": "Sahne olarak görüntüle" } }, @@ -4524,6 +4617,14 @@ "errors": { "failed-to-load": "Pano yüklenemedi" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "Pano alınamadı" @@ -4540,6 +4641,13 @@ "editable": "Düzenlenebilir", "readonly": "Salt okunur" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4589,6 +4697,18 @@ "transform-data": "Verileri dönüştür" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4844,6 +4964,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Panoyu {{version}} sürümüne geri yüklemek istediğinize emin misiniz? Kaydedilmemiş tüm değişiklikler kaybolacaktır.", + "confirmText-restore-version": "", "title-restore-version": "Sürümü geri yükle" }, "row-options-button": { @@ -4894,6 +5015,9 @@ "title-not-unique": "Bu başlık benzersiz değil" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "Farklı kaydet" }, @@ -4928,6 +5052,7 @@ "save-dashboard-error-proxy": { "body-name-exists": "Seçilen klasörde aynı isimde bir pano zaten mevcut. <1><2>Yine de bu panoyu kaydetmek istiyor musunuz?", "body-version-mismatch": "Başka bir kullanıcı bu panoyu güncelledi<1><2>Yine de bu panoyu kaydetmek ister misiniz?", + "confirmText-save-and-overwrite": "", "title-name-exists": "Çakışma", "title-version-mismatch": "Çakışma" }, @@ -5093,7 +5218,6 @@ "playlist-next": "Sonraki panoya git", "playlist-previous": "Önceki panoya git", "playlist-stop": "Oynatma listesini durdur", - "public-dashboard": "Herkese açık", "refresh": "Panoyu yenile", "save": "Panoyu kaydet", "save-dashboard": { @@ -5124,7 +5248,9 @@ "label-apply-transformation-to": "Dönüşümü şuraya uygula" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "Hata ayıklama", "title-disable-transformation": "Dönüşümü devre dışı bırakma", "title-filter": "Filtreleyin", @@ -5144,12 +5270,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Dönüşüm ara", "show-images": "Resimleri göster", - "title-add-another-transformation": "Başka bir dönüşüm ekle" + "title-add-another-transformation": "Başka bir dönüşüm ekle", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "Başka bir dönüşüm ekle" }, + "confirmText-delete-all": "", "delete-all-transformations": "Tüm dönüşümleri sil", "title-delete-all-transformations": "Tüm dönüşümler silinsin mi?", "tooltip-clear-search": "Aramayı temizle", @@ -5186,6 +5317,7 @@ "version-history-table": { "aria-label-toggle-selection": "{{version}} sürümünün seçimini değiştir", "date": "Tarih", + "name-latest": "", "notes": "Notlar", "restore": "Geri yükle", "updated-by": "Güncelleyen", @@ -5262,7 +5394,8 @@ "description-enables-users-custom-values": "Kullanıcıların listeye özel değerler eklemesine olanak tanır", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Boyutları CSV biçiminde girin: {{name}}, {{value}}", "label-data-source": "Veri kaynağı", - "label-use-static-key-dimensions": "Statik anahtar boyutları kullan" + "label-use-static-key-dimensions": "Statik anahtar boyutları kullan", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5335,6 +5468,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "Herkese açık URL'yi iptal et" } @@ -5346,6 +5482,7 @@ }, "custom-variable-form": { "custom-options": "Özel seçenekler", + "name-values-separated-comma": "", "selection-options": "Seçim ayarları" }, "dashboard-edit-pane-renderer": { @@ -5364,6 +5501,12 @@ "label-type": "Tür", "label-url": "URL", "label-with-tags": "Etiketlerle birlikte", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "Panoyu aç" }, "dashboard-link-list": { @@ -5376,15 +5519,9 @@ "type": "Tür" }, "dashboard-preview-banner": { - "not-saved": "Değer henüz Grafana veri tabanına kaydedilmedi", "not-yet-saved": "Değer Grafana veri tabanına kaydedilmedi", - "open-pull-request-in-git-hub": "GitHub'da çekme isteği aç", - "title-dashboard-loaded-branch-git-hub": "Bu pano, GitHub'daki bir daldan yüklendi.", "title-dashboard-loaded-external-repository": "Bu pano, haricî bir depodan yüklendi.", - "title-dashboard-loaded-request-git-hub": "Bu pano, GitHub'daki bir çekme isteğinden yüklendi.", - "title-error-loading-dashboard": "Pano yüklenirken hata oluştu", - "value-not-saved": "Değer henüz Grafana veri tabanına kaydedilmedi", - "view-pull-request-in-git-hub": "GitHub'da çekme isteğini görüntüle" + "title-error-loading-dashboard": "Pano yüklenirken hata oluştu" }, "dashboard-scene": { "text": { @@ -5410,6 +5547,8 @@ "data-source-options": "Veri kaynağı seçenekleri", "description-instance-name-filter": "Değişken değer listesinde hangi veri kaynağı örneklerinin seçileceğini belirlemek için regex (düzenli ifade) filtresi. Tümü için boş bırakın.", "example-instance-name-filter": "Örnek: ", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "Seçim ayarları" }, "default-grid-layout-manager": { @@ -5455,12 +5594,61 @@ "empty-transformations-message": { "add-transformation": "Dönüşüm ekle" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "Sütun seçenekleri", "row-height-options": "Satır yüksekliği seçenekleri" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5485,7 +5673,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "Boyutları CSV biçiminde girin: {{name}}, {{value}}", "group-by-options": "Seçeneklere göre grupla", "label-data-source": "Veri kaynağı", - "label-use-static-group-by-dimensions": "Statik grup boyutlarını kullan" + "label-use-static-group-by-dimensions": "Statik grup boyutlarını kullan", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "Panoya kopyala", @@ -5521,9 +5710,14 @@ "apply": "Uygula" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "Hesaplanan değer bu eşiğin altına düşmeyecektir", "description-step-count": "Değeri hesaplamak için mevcut zaman aralığının kaç kez bölüneceği", - "interval-options": "Aralık seçenekleri" + "interval-options": "Aralık seçenekleri", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5543,11 +5737,28 @@ "usage-count_one": "{{count}} panoda kullanılıyor", "usage-count_other": "{{count}} panoda kullanılıyor" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "Seçilen klasörde aynı isimde bir pano zaten mevcut. Yine de bu panoyu kaydetmek istiyor musunuz?", "title-name-already-exists": "Bu ad zaten mevcut" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "Haricî siteye geçilsin mi?" } @@ -5583,6 +5794,8 @@ }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "Başka bir dönüşüm ekle", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "Tüm dönüşümleri sil", "title-delete-all-transformations": "Tüm dönüşümler silinsin mi?" }, @@ -5635,9 +5848,13 @@ "description-examples": "Görüntü metnini ve değeri ayırmak için adlandırılmış yakalama grupları kullanılabilir (<1>örnekleri görün).", "description-optional": "İsteğe bağlıdır, bir seri adının veya metrik düğüm parçasının bir kısmını çıkarmak istiyorsanız kullanılır.", "label-data-source": "Veri kaynağı", + "label-static-options-sort": "", "label-target-data-source": "Hedef veri kaynağı", + "label-use-static-options": "", + "name-regex": "", "query-options": "Sorgu seçenekleri", - "selection-options": "Seçim ayarları" + "selection-options": "Seçim ayarları", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5650,6 +5867,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "Panoyu {{version}} sürümüne geri yüklemek istediğinize emin misiniz? Kaydedilmemiş tüm değişiklikler kaybolacaktır.", + "confirmText-restore-version": "", "title-restore-version": "Sürümü geri yükle" }, "save-button": { @@ -5667,7 +5885,8 @@ "label-folder": "Klasör", "render-footer": { "title-failed-to-save-dashboard": "Pano kaydedilemedi" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { @@ -5743,7 +5962,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "Aynı anda birden fazla değerin seçilmesini sağlar", "description-enables-option-include-variables": "Tüm değerleri dahil etme seçeneğini etkinleştirir", - "description-enables-users-custom-values": "Kullanıcıların listeye özel değerler eklemesine olanak tanır" + "description-enables-users-custom-values": "Kullanıcıların listeye özel değerler eklemesine olanak tanır", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "Paylaşım menüsünü aç/kapat" @@ -5763,6 +5986,9 @@ "copy-to-clipboard-failed": "Panoya kopyalanamadı" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(isteğe bağlı)", "text-options": "Metin seçenekleri" @@ -5786,6 +6012,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "Bu panelin bağlantısını gerçekten kaldırmak istiyor musunuz?" }, "unsaved-changes-modal": { @@ -5802,6 +6030,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "Bu değişken başka bir değişken veya pano tarafından referans alınmıyor.", "aria-label-variable-referenced-other-variables-dashboard": "Bu değişkene diğer değişkenler veya panolar tarafından başvuruluyor.", @@ -5811,10 +6042,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "Değişken düzenleyici formu", "back-to-list": "Listeye geri dön", + "confirmText": { + "delete-variable": "" + }, "delete": "Sil", "description-optional-display-name": "İsteğe bağlı görünen ad", "description-template-variable-characters": "Şablon değişkeninin adı. (Maks. 50 karakter)", "general": "Genel", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Açıklayıcı metin", "placeholder-label-name": "Etiket adı", "placeholder-variable-name": "Değişken adı", @@ -5829,13 +6066,25 @@ "variable": "Değişken" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "Değişkeni sil", "tooltip-duplicate-variable": "Değişkeni çoğalt", "tooltip-remove-variable": "Değişkeni kaldır" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "Gizle" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "{{variableId}} için kullanım yerleri gösteriliyor", "tooltip-show-usages": "Kullanımları göster" @@ -5862,6 +6111,7 @@ "version-history-table": { "aria-label-toggle-selection": "{{version}} sürümünün seçimini değiştir", "date": "Tarih", + "name-latest": "", "notes": "Notlar", "restore": "Geri yükle", "updated-by": "Güncelleyen", @@ -6249,7 +6499,8 @@ } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "Yükle" @@ -6287,6 +6538,7 @@ }, "label-limit": "Sınırlama", "label-value": "Değer", + "noOptionsMessage-no-fields-found": "", "scalar-options": { "description-clamped": "", "description-mod": "", @@ -6295,9 +6547,15 @@ } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "Maks.", "label-min": "Min.", - "label-value": "Değer" + "label-value": "Değer", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { "description-field": "", @@ -6869,6 +7127,8 @@ "aria-label-select-service-name-operator": "Hizmet adını işlecini seç", "aria-label-select-span-name": "Zaman aralığı adı seç", "aria-label-select-span-name-operator": "Zaman aralığı adı işleci seçin", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "Zaman aralığı filtreleri", "label-duration": "Süre", "label-service-name": "Hizmet adı", @@ -6939,6 +7199,8 @@ "split-widen": "Bölmeyi genişlet" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "Geri bildirim gönder", "label-copied": "Kopyalandı!", "label-export": "Dışa aktar", @@ -7046,6 +7308,7 @@ "when": "NE ZAMAN" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "Çalıştırma" }, "math": { @@ -7076,6 +7339,7 @@ }, "folder-filter": { "clear-folder-button": "Klasörleri temizle", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "Klasör filtresi", "select-placeholder": "Klasöre göre filtrele" }, @@ -7144,7 +7408,53 @@ "incomplete-request-error": "Ne yazık ki isteğiniz gerçekleştirilemedi. Lütfen tekrar deneyin.", "send-custom-feedback": "Gönder" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "Enlem", "label-longitude": "Boylam" @@ -7153,6 +7463,14 @@ "center": "Merkez:", "zoom": "Yakınlaştırma:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "Katman" @@ -7175,6 +7493,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "Coğrafi harita stili kuralı ekle" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "Katman ekle", "no-layers": "Katman yok mu?" @@ -7185,16 +7511,38 @@ "label-zoom": "Yakınlaştır", "use-current-map-settings": "Şu anki ayarları kullan" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "Sembol" }, "measure-overlay": { "tooltip-show-measure-tools": "Ölçüm araçlarını göster" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "Temel harita katmanı, sunucu yöneticisi tarafından yapılandırılır." }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "Hizalama", "label-baseline": "Başlangıç değeri", "label-color": "Renk", @@ -7208,7 +7556,14 @@ "label-symbol-vertical-align": "Sembol dikey hizalama", "label-text-label": "Metin etiketi", "label-x-offset": "X ofseti", - "label-y-offset": "Y ofseti" + "label-y-offset": "Y ofseti", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "Karşılaştırma işleci", @@ -7219,6 +7574,15 @@ "placeholder-feature-property": "Özellik niteliği", "placeholder-numeric-value": "Sayısal değer", "placeholder-value": "değer" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7468,7 +7832,8 @@ "aria-label-selected-color": "{{colorLabel}} rengi" }, "confirm-button": { - "cancel": "İptal" + "cancel": "İptal", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "Onaylamak için \"{{confirmPromptText}}\" yazın" @@ -7650,6 +8015,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "paneli daralt/genişlet", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "Sorguyu iptal et", "tooltip-cancel-loading": "Sorguyu iptal et", "tooltip-stop-streaming": "Akışı durdur", @@ -7716,6 +8083,9 @@ "filter-popup-match-case": "Büyük/küçük harf duyarlılığı", "inspect-drawer-title": "Değeri incele", "inspect-menu-label": "Değeri incele", + "nested-table": { + "no-data": "" + }, "no-values-label": "Değer yok", "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}/{{numRows}} satır" }, @@ -7724,6 +8094,7 @@ }, "tags-input": { "add": "Ekle", + "placeholder-new-tag": "", "remove": "Etiketi kaldır: {{name}}" }, "time-sync-button": { @@ -7817,6 +8188,12 @@ "footer-click-to-action": "{{actionTitle}} için tıklayın", "footer-click-to-navigate": "{{linkTitle}} ögesini açmak için tıklayın", "timestamp": "Zaman damgası" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -8195,6 +8572,10 @@ "add-library-panel-modal": { "title-create-library-panel": "Kütüphane paneli oluştur" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "Bu paneli kaldırmak istiyor musunuz?" }, @@ -8218,7 +8599,7 @@ "usage-count_other": "{{count}} panoda kullanılıyor" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "Ada veya açıklamaya göre ara" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "Kütüphane paneli yükleniyor..." @@ -8268,9 +8649,7 @@ "share-panel": { "config-description": "Panelinizi kuruluşunuz içinde paylaşmak için kişiselleştirilmiş, doğrudan bir bağlantı oluşturun. ", "download-image": "Görseli indir", - "render-image": "Görsel oluştur", - "render-image-error": "Panel görseli oluşturulamadı", - "render-image-error-description": "Görsel oluşturulurken bir hata oluştu" + "render-image": "Görsel oluştur" } }, "live": { @@ -8356,6 +8735,18 @@ "description-enable-infinite-scrolling": "", "description-enable-syntax-highlighting": "", "description-show-controls": "", + "fields": { + "type": { + "loki": { + "indexed-label_one": "", + "indexed-label_other": "", + "parsedl-label_one": "", + "parsedl-label_other": "", + "structured-metadata_one": "", + "structured-metadata_other": "" + } + } + }, "font-size-options": { "label-default": "", "label-small": "" @@ -8379,7 +8770,6 @@ "label-wrap-lines": "Satırları kaydır" }, "log-details": { - "close": "Günlük ayrıntılarını kapat", "fields": "Alanlar", "links": "Bağlantılar", "log-line": "Günlük satırı", @@ -8405,6 +8795,42 @@ "show-more": "daha fazla göster", "tooltip-error": "Hata: {{errorMessage}}" }, + "log-line-details": { + "clear-search": "", + "close": "", + "copy-shortlink": "", + "copy-to-clipboard": "", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "", + "copy-value-to-clipboard": "", + "filter-out": "", + "filter-out-query": "", + "toggle-field-button": { + "field-instead-message": "", + "hide-this-field": "" + } + }, + "fields-section": "", + "hide-log-line": "", + "inline-mode": "", + "links-section": "", + "log-line-field": "", + "log-line-section": "", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "", + "pin-line": "", + "remove-displayed-field": "", + "search": { + "no-results": "" + }, + "search-placeholder": "", + "show-context": "", + "show-log-line": "", + "sidebar-mode": "", + "unpin-line": "" + }, "log-line-menu": { "copy-link": "Bağlantıyı günlük satırına kopyala", "copy-log": "Günlük satırını kopyala", @@ -8505,6 +8931,11 @@ }, "name-common-labels": "", "name-deduplication": "", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, "name-enable-infinite-scrolling": "", "name-enable-log-details": "", "name-enable-syntax-highlighting": "", @@ -8531,6 +8962,7 @@ "un-themed-log-details": { "aria-label-data-links": "Veri bağlantıları", "aria-label-fields": "Alanlar", + "aria-label-line": "", "aria-label-log-level": "Günlük kaydı seviyesi", "aria-label-no-details": "Ayrıntı yok" }, @@ -8598,6 +9030,8 @@ "updated-on": "Güncelleme tarihi" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "Sil" }, "unthemed-dashboard-import": { @@ -8607,8 +9041,17 @@ "text": { "import-dashboard": "Panoyu içe aktarın" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "Bu araç, bazı kaynakları bu kurulumdan bulut altyapınıza taşımaya yardımcı olabilir. Başlamak için bu kurulumun bir anlık görüntüsünü oluşturmanız gerekir. Anlık görüntü oluşturma işlemi genellikle iki dakikadan kısa sürer. Anlık görüntü, bu Grafana kurulumu ile birlikte saklanır.", @@ -8757,6 +9200,8 @@ "resource-details": { "dismiss-button": "Tamam", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "Pano zaten bulut örneğinde Grafana tarafından sağlanmış ve yönetilmektedir. Bundan sonra sağlanan panoyu kullanmanızı öneririz. Yine de panoyu bulut örneğine kopyalamak istiyorsanız pano JSON'unda pano kimliğini değiştirin, yeni bir anlık görüntü kaydedin ve tekrar yükleyin.", "datasource-already-managed": "Veri kaynağı zaten bulut örneğinde Grafana tarafından sağlanmış ve yönetilmektedir. Bu farklı bir kaynaksa başka bir UID belirleyin ve tekrar deneyin.", "datasource-invalid-url": "Geçersiz bir URL içeren bir veri kaynağı var. Geçerli bir URL girin ve tekrar deneyin.", @@ -9447,6 +9892,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "Kuruluş Seçin" }, "page": { @@ -9669,6 +10115,7 @@ "permission": "Bu sayfayı görüntüleme izniniz yok.", "title-access-denied": "Erişim reddedildi" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "Kök uygulama sayfası bileşeni bulunamadı" }, "browse": { @@ -9712,8 +10159,7 @@ "update-status-text": "Eklentiler güncellendi" }, "versions": { - "confirmation-text-1": "Şu eski sürüme düşürmek istediğinizden emin misiniz:", - "confirmation-text-2": "Bunu normalde yapmamalısınız", + "confirmation-text": "", "downgrade-confirm": "Eski sürüme düşür", "downgrade-title": "Eklenti sürümünü eski sürüme düşür" } @@ -9767,6 +10213,10 @@ "empty-state": { "message": "Eklenti bulunamadı" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "Tamam" @@ -9800,7 +10250,11 @@ "updating": "Güncelleniyor" }, "install-controls-button": { - "title-uninstall-modal": "{{plugin}} eklentisini kaldır" + "title-uninstall-modal": "{{plugin}} eklentisini kaldır", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "Bu eklenti <2>grafana.com/plugins adresinde yayımlanmadı ve katalog üzerinden yönetilemez.", @@ -9971,6 +10425,15 @@ "label-workflow": "" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "Bu özellik şu anda aktif geliştirme aşamasındadır. En iyi deneyim ve en yeni geliştirmeler için Grafana'nın <2>gecelik derleme sürümünü kullanmanızı öneririz." @@ -10836,6 +11299,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "Hizmet hesabı seçici", "select-placeholder": "Hizmet hesaplarını aramak için yazmaya başlayın" }, @@ -10881,6 +11345,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "Hizmet hesabı belirteci ekle", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "Hizmet hesabını sil", "disable-service-account": "Hizmet hesabını devre dışı bırak", "enable-service-account": "Hizmet hesabını etkinleştir", @@ -10907,6 +11375,7 @@ "used-by": "Kullanan:" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "Düzenle" }, "service-account-role-row": { @@ -10920,10 +11389,16 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "Hizmet hesabı ekle", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "Hizmet hesabını ada göre ara", "sub-title": "Hizmet hesapları ve belirteçleri, Grafana API'ye karşı kimlik doğrulamak için kullanılabilir. Daha fazla bilgi için <2>belgelerimize", "title-delete-service-account": "Hizmet hesabını sil", - "title-disable-service-account": "Hizmet hesabını devre dışı bırak" + "title-disable-service-account": "Hizmet hesabını devre dışı bırak", + "body-delete_one": "", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "Bu belirtecin süresi doldu", @@ -10937,6 +11412,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "JSON olarak dışa aktar", "share-externally-title": "Haricî olarak paylaş", "share-internally-title": "Dâhilî olarak paylaş", @@ -10972,6 +11448,19 @@ "view-button": "JSON'u görüntüle", "view-button-yaml": "YAML'yi görüntüle" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "Kütüphane paneli oluşturun." }, @@ -10980,7 +11469,7 @@ "info-text": "Aşağıdaki seçeneklerle özelleştirilen bu panoya veya panele doğrudan bir bağlantı oluşturun.", "link-url": "Bağlantı URL'si", "render-alert": "Görüntü işleme eklentisi yüklü değil", - "render-instructions": "Bir panel görseli oluşturmak için <2>Grafana görüntü işleme eklentisini yüklemeniz gerekir. Lütfen eklentiyi yüklemek için Grafana yöneticinizle iletişime geçin.", + "render-instructions": "", "rendered-image": "Doğrudan bağlantılı oluşturulmuş görüntü", "save-alert": "Pano kaydedilmedi", "save-dashboard": "Panel görüntüsü oluşturmak için önce panoyu kaydetmelisiniz.", @@ -11048,6 +11537,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "Panel ön izlemesi" }, @@ -11388,6 +11883,7 @@ "description-count-rows": "", "description-fields": "", "description-min-column-width": "", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "Görsel görüntülenemediğinde veya ekran okuyucu kullanan kullanıcılar için gösterilecek alternatif metin", "description-title-text": "İmleç görselin üzerine getirildiğinde görüntülenecek metin", @@ -11408,6 +11904,7 @@ "name-min-column-width": "", "name-show-table-footer": "", "name-show-table-header": "", + "name-wrap-header-text": "", "placeholder-column-width": "", "placeholder-fields": "" }, @@ -11458,6 +11955,7 @@ "tag-option-label": "Etiket seçeneği" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "Ekip seçici", "select-placeholder": "Bir ekip seçin" }, @@ -11759,6 +12257,18 @@ "placeholder-fields-or-number": "Alanlar veya numaralar" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "Takma isim", "label-mode": "Mod", "label-replace-all-fields": "Tüm alanları değiştir" @@ -11773,6 +12283,13 @@ "concatenate-transformer-editor": { "label-label": "Etiket", "label-name": "Ad", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "Çerçeve" }, "config-from-query-transformer-editor": { @@ -11783,6 +12300,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "Alan türü dönüştürücü ekle", "aria-label-remove-convert-field-type-transformer": "Alan türü dönüştürücüyü kaldır", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "Tarayıcı", "utc": "UTC" @@ -11793,7 +12314,13 @@ "label-input-format": "Girdi biçimi", "label-join-with": "Birleştir", "label-set-timezone": "Zaman dilimini ayarla", + "name": { + "convert-field-type": "" + }, "placeholder-type": "Tür", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "Dizi değerlerini birleştirirken açık bir ayırıcı kullanın", "tooltip-input-format": "Grafana'nın tarih dizesini doğru şekilde ayrıştırabilmesi için girdi alanı biçimini belirtin.", "tooltip-specify-the-output-format": "Çıktı biçimini belirtin.", @@ -11815,6 +12342,19 @@ "desc": "", "off": "Kapalı" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Sıralı dizi değeri ekle", "generate-enum-values-from-data": "Veriden sıralı dizi değerleri oluştur" @@ -11825,11 +12365,22 @@ "remove-enum-row-tooltip-delete": "Sil" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "Sınırlayıcı", "label-format": "Biçim", "label-keep-time": "Zaman bilgisini koru", "label-replace-all-fields": "Tüm alanları değiştir", "label-source": "Kaynak", + "name": { + "extract-fields": "" + }, "placeholder-auto": "Otomatik", "placeholder-select-delimiter": "Sınırlayıcı seçin...", "tooltip-regexp": "Örnek: {{regexExample}}" @@ -11838,8 +12389,22 @@ "aria-label-threshold-color": "Eşik rengi" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Alan", - "label-lookup": "Arama" + "label-lookup": "Arama", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "Ek ayarlar", @@ -11848,12 +12413,26 @@ "use-as": "Farklı kullan" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "Değişken üzerinden", "label-identifier": "Tanımlayıcı", "label-use-variable": "Değişken kullan", "label-variable": "Değişken", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "Düzenli ifade deseni" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "Alan", "label-match": "Eşleşme", @@ -11863,14 +12442,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Koşul ekle", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "Koşullar", - "label-filter-type": "Filtre türü" + "label-filter-type": "Filtre türü", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "Alan", "label-format": "Biçim", "label-substring-range": "Alt dize aralığı" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "Biçim", "label-set-timezone": "Zaman dilimini ayarla", @@ -11878,6 +12491,70 @@ "tooltip-format": "Alan için çıktı biçimi, <2>Moment.js biçimlendirme dizesi olarak belirtilir.", "tooltip-timezone-manually": "Tarih için saat dilimini manuel olarak ayarlayın" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "Tarayıcı", @@ -11905,22 +12582,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "Yok sayıldı", "placeholder-select-stats": "İstatistikleri seç" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "Hiçbir alan gruplanmıyorsa hesaplamaların etkisi olmaz" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "Etkinleştirilirse iç içe tablolar alan adlarını tablo başlığı olarak gösterir", "label-show-field-names-in-nested-tables": "Alan adlarını iç içe tablolarda göster", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "Hiçbir alan gruplanmıyorsa hesaplamalar etkili olmayacaktır." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "Hücre değeri", "label-column": "Sütun", "label-empty-value": "Boş değer", "label-row": "Satır", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "Boole yanlış değeri", @@ -11939,6 +12640,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "Otomatik", "placeholder-default": "Varsayılan: 30", "placeholder-none": "Yok" @@ -11947,7 +12654,16 @@ "label-as-percentile": "Yüzdelik olarak", "tooltip-transform-index-percentile": "Satır dizinini yüzdelik değere dönüştürün." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "Girdi herhangi bir etiket içermiyor", "no-input-or-labels-found": "Girdi (veya etiket) bulunamadı", @@ -11969,10 +12685,30 @@ "label-labels": "Etiketler", "label-mode": "Mod", "label-value-field-name": "Değer alanı adı", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(İsteğe bağlı) Etiket seçin", "tooltip-replace-value-field-label": "Değer alanı adını bir etiketle değiştir" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "Değer veya değişken" }, "long-info": { @@ -11983,7 +12719,13 @@ "string-values-separate": "Dize değerler etiket yerine ayrı alanlar olarak temsil edilir" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Tek bir çerçeveye uygulandığında birleştirmenin etkisi olmaz." + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "Tek bir çerçeveye uygulandığında birleştirmenin etkisi olmaz.", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "Tüm değerler sayısaldır", @@ -11993,10 +12735,16 @@ "time-in-ascending-order": "Zamana göre artan sıralama" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "" + }, "field-order": "", "field-order-auto": "Otomatik", "field-order-manual": "", - "first-frame-warning": "Alanları düzenleme yalnızca tek bir çerçeveyle çalışır. Önce birleştirme dönüşümü uygulamayı veya girdiyi filtrelemeyi düşünün." + "first-frame-warning": "Alanları düzenleme yalnızca tek bir çerçeveyle çalışır. Önce birleştirme dönüşümü uygulamayı veya girdiyi filtrelemeyi düşünün.", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "Seçilen alan adlarının nasıl gösterileceğini ayarlar. Tablo verileri için çerçeve adı genellikle daha iyidir" @@ -12021,9 +12769,41 @@ "partition-values-works-single-frame": "Değerlere göre bölme yalnızca tek bir çerçevede çalışır.", "tooltip-keeps-partition-fields-frames": "Bölme alanlarını çerçevede tutar" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "Biçim", - "label-info": "Bilgi" + "label-info": "Bilgi", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "ve", @@ -12035,6 +12815,9 @@ "label-operation": "Çalıştırma" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "Hesaplamalar", "label-include-time": "Zamanı dahil et", "label-labels-to-fields": "Alan etiketleri", @@ -12049,12 +12832,23 @@ "series-to-rows": "Seriden satırlara" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "İstatistik seç" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Değer veya değişken", "variable-regex-documentation": "Çok değerli değişkeni bir regex dizesine dönüştürür" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "Kübik", @@ -12076,6 +12870,14 @@ "regression": "", "tooltip-number-of-xy-points-to-predict": "Tahmin edilecek X,Y noktalarının sayısı" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Eşleşme", "label-replace": "Değiştir", @@ -12084,11 +12886,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Alan", - "label-mode": "Mod" + "label-mode": "Mod", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "Alan", "label-reverse": "Geri döndür", + "name": { + "sort-by": "" + }, "placeholder-select-field": "Gönderi seçim alanı" }, "supplier": { @@ -12108,6 +12936,17 @@ "transform": "Dönüştür" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "İstatistik", "label-time-field": "Zaman alanı", @@ -12121,10 +12960,34 @@ "placeholder-value": "Değer", "tooltip-name-for-value-fields": "Değer alanlarının adı" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "Çalıştırma", "placeholder-field": "Alan" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Herhangi bir türde birden fazla değer alanı", "single-frame": "Tek çerçeve", @@ -12177,6 +13040,7 @@ "title": "Kuruluşlar" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "Kullanıcı seçici", "select-placeholder": "Kullanıcı aramak için yazmaya başlayın" }, @@ -12262,6 +13126,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "Değişkeni sil" }, "create-ad-hoc-variable-adapter": { @@ -12310,9 +13176,34 @@ "label-refresh": "Yenile" }, "query-variable-sort-select": { - "description-values-variable": "Bu değişkenin değerlerini nasıl sıralayacağınız" + "description-values-variable": "Bu değişkenin değerlerini nasıl sıralayacağınız", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "Varsayılan değer (varsa)", "text-options": "Metin seçenekleri" }, @@ -12341,6 +13232,8 @@ "description-optional-display-name": "İsteğe bağlı görünen ad", "description-template-variable-characters": "Şablon değişkeninin adı. (Maks. 50 karakter)", "general": "Genel", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "Açıklayıcı metin", "placeholder-label-name": "Etiket adı", "placeholder-variable-name": "Değişken adı", @@ -12355,9 +13248,15 @@ "tooltip-duplicate-variable": "Değişkeni çoğalt", "tooltip-remove-variable": "Değişkeni kaldır" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "Tüm değerleri aç/kapat" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "Kullanımları göster" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 1e1b0aa3b0e..6ad9dd39120 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "有些功能可以稳定运行(GA 正式版)并默认启用,而有些功能目前处于初步的 Beta 测试阶段,可供用户提前试用。", "confirm-modal-body-2": "我们建议在修改之前了解每个功能更改所带来的影响。", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta 测试版", "content-general-availability": "正式发布", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "删除组织", + "confirmText-delete": "", "title-delete": "删除" }, "anon-users": { "not-found": "未找到匿名用户。" }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "强制从所有设备退出登录" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "您没有权限查看此组织中的用户。要更新此组织,请与服务器管理员联系。", "heading": "编辑组织", @@ -111,10 +119,10 @@ "users-heading": "组织用户" }, "enterprise-auth-features-card": { - "dismiss": "", - "heading": "", - "learn-more-link": "", - "text": "" + "dismiss": "忽略", + "heading": "企业验证", + "learn-more-link": "了解更多", + "text": "使用 <1>SAML、<3>SCIM、<6>LDAP 和 <8>RBAC 自动管理用户、团队和权限,这些功能在 Grafana Cloud 和 Enterprise 中均可用。" }, "feature-listing": { "title-auditing": "审计", @@ -208,9 +216,11 @@ "not-editable": "此用户的角色不可编辑,因为它是从您的身份验证提供商同步而来的。有关详细信息,请参阅 <1>Grafana 身份验证文档。" }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "角色" }, + "confirmText-delete": "", "delete-aria-label": "删除用户:{{name}}", "title-delete": "删除" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "这些系统设置在 grafana.ini 或 custom.ini 中定义(或在 ENV 变量中覆盖)。要更改这些设置,您当前需要重新启动 Grafana。" }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "企业许可" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "更改", + "confirmText-change": "", "grafana-admin-key": "Grafana 管理员", "grafana-admin-no": "否", "grafana-admin-yes": "是", "title": "权限" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "删除用户", "disable-button": "禁用用户", "edit-button": "编辑", @@ -312,6 +330,9 @@ "title-delete-user": "删除用户", "title-disable-user": "禁用用户" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "浏览器和操作系统", "force-logout-all-button": "强制从所有设备退出登录", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "活跃时间设定" }, + "ad": { + "close": "关闭", + "learn-more": "了解更多" + }, "add-button": { "add-more": "添加更多" }, @@ -370,12 +395,12 @@ "missing-series-resolve": "缺少要解除的序列评估", "no-annotations": "无注释", "notification-configuration": { - "active-timings": "", + "active-timings": "活跃时间间隔", "contact-point": "联络点", "group-by": "分组方式", - "group-title": "", + "group-title": "通知配置", "mute-timings": "静音时间设定", - "timing-options": "" + "timing-options": "计时" }, "pending-period": "待处理时段", "rule": "规则", @@ -453,6 +478,9 @@ "label-muting-grouping-and-timings-optional": "静音、分组和时间设定(可选)", "title-muting-grouping-and-timings": "静音、分组和时间设定" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "复制链接", "duplicate": "复制", @@ -476,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "编辑 YAML", @@ -542,6 +573,7 @@ "view-configuration": "视图配置" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "无法手动更改内部 Grafana Alertmanager 配置。要更改此配置,请通过用户界面编辑单个资源。", "gma-manual-configuration-is-not-supported": "不支持手动进行配置更改", "message": { @@ -556,11 +588,13 @@ "title-resetting-alertmanager-configuration": "重置 Alertmanager 配置" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "比较", "restore": "还原", "text-latest": "最近" }, + "confirmText-yes-restore-configuration": "", "loading": "加载中...", "no-previous-configurations": "没有以前的配置", "this-might-take-a-while": "这可能需要一点时间...", @@ -840,8 +874,10 @@ }, "contact-point-header": { "aria-label-more-actions": "联络点“{{contactPointName}}“的更多操作", + "ariaLabel-delete": "", "button-edit": "编辑", "button-view": "查看", + "export-ariaLabel-export": "", "export-label-export": "导出", "label-delete": "删除", "label-manage-permissions": "管理权限", @@ -912,7 +948,7 @@ }, "contactPoints": { "validation": { - "notFound": "" + "notFound": "找不到联络点“{{contactPoint}}”" } }, "continue-matching-indicator": { @@ -921,7 +957,7 @@ "copy-to-clipboard": "将“{{label}}”复制到剪贴板", "create-metadata": { "label": { - "contact-point": "", + "contact-point": "通知已发送到", "dashboard": "仪表板", "dashboard-and-panel": "数据面板和面板", "evaluation-interval": "评估间隔", @@ -1069,7 +1105,7 @@ "content-show-all-errors": "显示所有错误" }, "errors": { - "failedWith": "" + "failedWith": "{{-config}} 失败,状态为{{status}}:{{-message}}" }, "evaluate-every-validation-options": { "message": { @@ -1374,6 +1410,7 @@ "label-disable-resolved-message": "禁用已解除消息" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "必须是正整数。", "must-enter-a-group-name": "输入一个组名称" @@ -1465,15 +1502,12 @@ "group-details": { "ds-features-error": "加载数据源详情时出错", "edit": "编辑", - "evaluations-to-fire": "待触发的评估周期", "export": "导出", "folder": "文件夹", "group-loading-error": "加载组时出错", "interval": "间隔", "namespace": "命名空间", - "pending-period": "待处理时段", - "recording": "录制", - "rule-name": "规则名称" + "new": "" }, "group-edit": { "ds-error": "加载数据源详情时出错", @@ -1519,8 +1553,20 @@ } }, "home": { + "irm-card-description": "使用 Grafana Cloud IRM 统一处理值班、警报和事件响应。", + "irm-card-item-1": "使用您的日历或 Terraform 管理值班时间表。", + "irm-card-item-2": "通过网络、应用、Slack 或其他渠道响应事件。", + "irm-card-item-3": "使用 AI 驱动的 Grafana SIFT 查明根本原因。", + "irm-card-item-4": "分析过去的事故,以提高响应能力和复原力。", + "irm-card-title": "事故响应和管理", "label-get-started": "开始操作", "label-insights": "洞察", + "synthetic-monitoring-card-description": "从全球位置在外部监测关键用户流、网站和 API。", + "synthetic-monitoring-card-item-1": "通过浏览器检查模拟端到端用户旅程。", + "synthetic-monitoring-card-item-2": "在每个网络层运行 ping、DNS、HTTP/S 和 TCP 检查。", + "synthetic-monitoring-card-item-3": "使用 20 多个全球探测点,或部署在防火墙后的私有探测点。", + "synthetic-monitoring-card-item-4": "使用内置的 Prometheus 风格的警报功能,直接从用户界面跟踪 SLO。", + "synthetic-monitoring-card-title": "综合监测", "title-alerting": "警报" }, "import-to-gma": { @@ -1535,7 +1581,7 @@ "no-rules-body-yaml": "没有要导入的规则。请选择不同的 yaml 文件。", "no-rules-title": "没有要导入的规则", "not-using-rules-managed-by-integrations-or-plugins": { - "text": "", + "text": "不应将由集成或插件管理的规则导入到 Grafana 管理的规则。", "title": "信息" }, "plugin-rules-warning": { @@ -1819,7 +1865,11 @@ "other-data-sources": "其他数据源" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "已禁用", @@ -1910,7 +1960,7 @@ }, "notification-policies-filter": { "label-search-by-contact-point": "按联络点搜索", - "placeholder-search-by-contact-point": "", + "placeholder-search-by-contact-point": "选择一个联络点", "search-query-input-placeholder-search": "搜索" }, "notification-policies-list": { @@ -2069,9 +2119,11 @@ "update-errors": { "conflict": "通知策略树已由其他用户更新。", "error-code": "错误消息:“{{error}}”", - "fallback": "更新通知策略时出错。", + "routes": { + "conflictingMatchers": "" + }, "suffix": "请刷新页面并重试。", - "title": "保存通知策略时出错" + "title": "" }, "n-more-policies_other": "其他 {{count}} 个策略" }, @@ -2126,6 +2178,7 @@ "query-and-expressions-step": { "add-query": "添加查询", "body-queries-expressions-configured": "至少创建一个查询或表达式以发出警报", + "confirmText-deactivate": "", "expressions": "表达式", "loading-data-sources": "正在加载数据源...", "manipulate-returned-queries-other-operations": "使用数学和其他操作处理查询返回的数据。", @@ -2175,6 +2228,9 @@ "label-export-all": "导出全部" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "由于以下原因,无法加载查询编辑器:{{errorMessage}}" }, @@ -2193,6 +2249,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "您需要为复制的规则设置一个新的评估组,因为原始规则已预配,无法用于在用户界面中创建的规则。", "body-not-provisioned": "新规则将<1>不会被标记为预配规则。", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "复制已预置的警报规则" }, "redirect-to-rule-viewer": { @@ -2389,8 +2446,6 @@ "title-inspect-alert-rule": "检查警报规则" }, "rule-list": { - "cannot-find-rule-details-for": "找不到 UID {{uid}} 的规则详情", - "cannot-load-rule-details-for": "无法加载 UID {{uid}} 的规则详情", "configure-datasource": "配置", "draft-new-rule": "起草新规则", "ds-error": { @@ -2413,7 +2468,7 @@ "new-badge": "全新!", "text": "导入到 Grafana 管理的规则" }, - "loading-more-groups": "", + "loading-more-groups": "正在加载更多组...", "more": "更多", "new-alert-rule": "新建警报规则", "new-datasource-recording-rule": "新的数据源录制规则", @@ -2585,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "找不到您想要的数据源?某些数据源不支持警报。点击图标以获取更多信息。" }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "添加新的 Alertmanager", "builtin-alertmanager": "内置 Alertmanager", @@ -2737,6 +2797,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "选择通知模板", "loading": "加载中...", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "选择通知模板" } @@ -2763,6 +2826,8 @@ }, "templates-table": { "actions": "操作", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "未定义模板。", "template-group": "模板组", "title-delete-template-group": "删除模板组" @@ -2815,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}}前)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "目标文件夹不为空,某些规则可能会被覆盖或移除。您确定要将这些警报规则导入到 Grafana 管理的规则吗?", @@ -2890,6 +2960,11 @@ "title-delete-contact-point": "删除联络点" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "删除通知策略" @@ -2941,51 +3016,51 @@ } }, "alertlist": { - "category-alert-state-filter": "", - "category-filter": "", - "category-options": "", - "descriptino-alerts-linked-to-dashboard": "", - "description-alert-instance-label": "", - "description-alert-name": "", - "description-datasource": "", - "description-folder": "", - "description-group-by": "", - "description-group-mode": "", - "description-max-items": "", - "description-show-inactive-alerts": "", - "description-sort-order": "", - "description-view-mode": "", + "category-alert-state-filter": "警报状态筛选条件", + "category-filter": "筛选条件", + "category-options": "选项", + "descriptino-alerts-linked-to-dashboard": "仅显示与此数据面板关联的警报", + "description-alert-instance-label": "使用标签查询筛选警报实例,例如:Monitoring{severity=\"critical\", instance=~\"cluster-us-.+\"}", + "description-alert-name": "筛选包含此文本的警报", + "description-datasource": "从警报源筛选", + "description-folder": "筛选所选文件夹中的警报(仅适用于 Grafana 管理的警报规则)", + "description-group-by": "使用标签查询筛选警报", + "description-group-mode": "如何对警报实例进行分组", + "description-max-items": "可显示的最大警报数", + "description-show-inactive-alerts": "包括具有 0(零)个实例的警报规则。由于这些规则没有实例,如果配置了警报实例标签筛选条件,它们将保持隐藏状态。", + "description-sort-order": "警报和警报实例的排序顺序", + "description-view-mode": "在列表视图和统计视图之间切换", "group-by": { "aria-label-group-by-label-keys": "按标签键分组", "placeholder-group-by": "分组依据" }, "group-mode-options": { - "label-custom-grouping": "", - "label-default-grouping": "" + "label-custom-grouping": "自定义分组", + "label-default-grouping": "默认分组" }, - "name-alert-instance-label": "", - "name-alert-name": "", - "name-alerts-linked-to-dashboard": "", - "name-datasource": "", - "name-error": "", - "name-firing": "", - "name-folder": "", - "name-group-by": "", - "name-group-mode": "", - "name-max-items": "", - "name-no-data": "", - "name-normal": "", - "name-pending": "", - "name-recovering": "", - "name-show-inactive-alerts": "", - "name-sort-order": "", - "name-view-mode": "", + "name-alert-instance-label": "警报实例标签", + "name-alert-name": "警报名称", + "name-alerts-linked-to-dashboard": "与此数据面板关联的警报", + "name-datasource": "数据源", + "name-error": "错误", + "name-firing": "警报/触发", + "name-folder": "文件夹", + "name-group-by": "分组方式", + "name-group-mode": "分组模式", + "name-max-items": "最大项目数", + "name-no-data": "无数据", + "name-normal": "正常", + "name-pending": "待处理", + "name-recovering": "正在恢复", + "name-show-inactive-alerts": "显示带有 0 个实例的警报", + "name-sort-order": "排序顺序", + "name-view-mode": "查看模式", "sort-order-options": { - "label-alphabetical-asc": "", - "label-alphabetical-desc": "", - "label-importance": "", - "label-time-asc": "", - "label-time-desc": "" + "label-alphabetical-asc": "按字母顺序(升序)", + "label-alphabetical-desc": "按字母顺序(降序)", + "label-importance": "重要性", + "label-time-asc": "时间(升序)", + "label-time-desc": "时间(降序)" }, "ungrouped-mode-view": { "active-for": "有效时长:<1>{{duration}}", @@ -3001,8 +3076,8 @@ "title-permission-required": "需要权限" }, "view-mode-options": { - "label-list": "", - "label-stat": "" + "label-list": "列表", + "label-stat": "统计" } }, "annolist": { @@ -3015,38 +3090,39 @@ "annotation-list-item": { "tooltip-created-by": "创建人:<1>{{email}}" }, - "category-annotation-query": "", - "category-display": "", - "category-link-behaviour": "", - "description-tags": "", + "category-annotation-query": "注释查询", + "category-display": "显示", + "category-link-behaviour": "关联行为", + "description-tags": "匹配注释标记", "link-target-options": { - "label-dashboard": "", - "label-panel": "" + "label-dashboard": "数据面板", + "label-panel": "面板" }, - "name-limit": "", - "name-link-target": "", - "name-query-filter": "", - "name-show-tags": "", - "name-show-time": "", - "name-show-user": "", - "name-tags": "", - "name-time-after": "", - "name-time-before": "", - "name-time-range": "", + "name-limit": "限制", + "name-link-target": "关联目标", + "name-query-filter": "查询过滤条件", + "name-show-tags": "显示标记", + "name-show-time": "显示时间", + "name-show-user": "显示用户", + "name-tags": "标记", + "name-time-after": "此后的时间", + "name-time-before": "此前的时间", + "name-time-range": "时间范围", "query-filter-options": { - "label-all-dashboards": "", - "label-this-dashboard": "" + "label-all-dashboards": "所有数据面板", + "label-this-dashboard": "此数据面板" }, "time-range-options": { - "label-none": "", - "label-this-dashboard": "" + "label-none": "无", + "label-this-dashboard": "此数据面板" } }, "annotations": { "annotation-field-mapper": { "annotation": "注释", "first-value": "第一个值", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "添加注释查询", @@ -3054,6 +3130,16 @@ "info-box-content-2": "查看<2>注释文档以获取更多信息。", "title": "尚未添加自定义注释查询" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} 个事件(来自 {{numFields}} 个字段)", "no-events-found": "未找到事件", @@ -3129,6 +3215,9 @@ "define-allowed-teams-ids-label": "定义允许的团队 ID", "display-name-description": "将在登录页面上显示为“使用... 登录”。如果您使用多个身份提供程序或 SSO 协议,这将很有帮助。", "display-name-label": "显示名称", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "用于在 OAuth2 ID 令牌的属性映射中查找用户电子邮箱的密钥的名称。", "email-attribute-name-label": "电子邮箱属性名称", "email-attribute-path-description": "用于从用户信息中查找用户电子邮箱的 JMESPath 表达式。", @@ -3180,7 +3269,7 @@ "team-ids-github": "团队 ID 的整数列表。", "team-ids-label": "团队 ID", "team-ids-numbers": "团队 ID 必须是数字。", - "team-ids-other": "团队 ID 的字符串列表。", + "team-ids-other": "", "team-ids-placeholder": "输入团队 ID 并按 Enter 键添加", "teams-url-description": "用于查询团队 ID 的网址。如果未设置,则默认值为 /teams。", "teams-url-description-oauth": "如果配置了“{{ teamsURLLabel }}”,则还必须配置“{{ teamIDsAttributePathLabel }}”。", @@ -3215,8 +3304,8 @@ } }, "provider-card": { - "saml-learn-more": "", - "scim-learn-more": "", + "saml-learn-more": "使用 SAML 进行单点登录 (SSO)。", + "scim-learn-more": " 使用 SCIM 同步用户和团队。", "text-badge-enabled": "已启用", "text-badge-not-enabled": "未启用" }, @@ -3224,6 +3313,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "重置为默认值" }, + "confirmText-reset": "", "disable": "禁用", "disabling": "正在禁用…", "discard": "丢弃", @@ -3252,99 +3342,99 @@ }, "bar-chart": { "warn": { - "missing-numeric": "", - "missing-series": "" + "missing-numeric": "未找到数字字段", + "missing-series": "条形图需要字符串或时间字段" } }, "barchart": { "config": { - "category-thresholds": "", - "description-color-by-field": "", - "description-x-label-max-length": "", - "name-bar-radius": "", - "name-bar-width": "", - "name-color-by-field": "", - "name-fill-opacity": "", - "name-full-highlight": "", - "name-gradient-mode": "", - "name-group-width": "", - "name-line-width": "", - "name-orientation": "", - "name-rotate-x-labels": "", - "name-show-thresholds": "", - "name-show-values": "", - "name-stacking": "", - "name-transform": "", - "name-x-axis": "", - "name-x-label-max-length": "", - "name-x-label-min-spacing": "", + "category-thresholds": "阈值", + "description-color-by-field": "使用同级字段的颜色值为每个条形值着色。", + "description-x-label-max-length": "X 轴标签将被截断为提供的长度", + "name-bar-radius": "条形圆角半径", + "name-bar-width": "条形宽度", + "name-color-by-field": "按字段着色", + "name-fill-opacity": "填充不透明度", + "name-full-highlight": "悬停时突出显示整个区域", + "name-gradient-mode": "渐变模式", + "name-group-width": "组宽度", + "name-line-width": "线宽度", + "name-orientation": "朝向", + "name-rotate-x-labels": "旋转 x 轴刻度标签", + "name-show-thresholds": "显示阈值", + "name-show-values": "显示值", + "name-stacking": "堆叠", + "name-transform": "转换", + "name-x-axis": "X 轴", + "name-x-label-max-length": "X 轴刻度标签最大长度", + "name-x-label-min-spacing": "X 轴标签最小间距", "orientation-options": { - "label-auto": "", - "label-horizontal": "", - "label-line-vertical": "" + "label-auto": "自动", + "label-horizontal": "水平", + "label-line-vertical": "垂直" }, - "placeholder-x-axis": "", - "placeholder-x-label-max-length": "", + "placeholder-x-axis": "第一个字符串或时间字段", + "placeholder-x-label-max-length": "无", "show-values-options": { - "label-always": "", - "label-auto": "", - "label-never": "" + "label-always": "始终", + "label-auto": "自动", + "label-never": "从不" }, "transform-options": { - "description-constant": "", - "description-negative-y": "", - "label-constant": "", - "label-negative-y": "" + "description-constant": "第一个值将显示为一条固定线", + "description-negative-y": "将结果翻转为 y 轴上的负值", + "label-constant": "固定", + "label-negative-y": "负 Y" } }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "需要右侧的空间", "gaps-options": { - "description-large": "", - "description-medium": "", - "description-none": "", - "description-small": "", - "label-large": "", - "label-medium": "", - "label-none": "", - "label-small": "" + "description-large": "需要 {{spacing}} 间距", + "description-medium": "需要 {{spacing}} 间距", + "description-none": "显示所有刻度标记", + "description-small": "需要 {{spacing}} 间距", + "label-large": "大", + "label-medium": "中", + "label-none": "无", + "label-small": "小" }, "label-rtl": "从右到左" } }, "bargauge": { "bar-size-options": { - "label-auto": "", - "label-manual": "" + "label-auto": "自动", + "label-manual": "手动" }, - "category-bar-gauge": "", - "description-max-height": "", - "description-min-height": "", - "description-min-width": "", - "description-show-unfilled-area": "", + "category-bar-gauge": "条形仪表", + "description-max-height": "最大行高(水平方向)", + "description-min-height": "最小行高(水平方向)", + "description-min-width": "最小列宽(垂直方向)", + "description-show-unfilled-area": "启用后,未填充区域将渲染为灰色", "display-mode-options": { - "label-basic": "", - "label-gradient": "", - "label-retro": "" + "label-basic": "基础", + "label-gradient": "渐变", + "label-retro": "复古液晶显示屏" }, - "name-bar-size": "", - "name-display-mode": "", - "name-max-height": "", - "name-min-height": "", - "name-min-width": "", - "name-name-placement": "", + "name-bar-size": "栏大小", + "name-display-mode": "显示模式", + "name-max-height": "最大高度", + "name-min-height": "最小高度", + "name-min-width": "最小宽度", + "name-name-placement": "名称放置", "name-placement-options": { - "label-auto": "", - "label-hidden": "", - "label-left": "", - "label-top": "" + "label-auto": "自动", + "label-hidden": "隐藏", + "label-left": "左侧", + "label-top": "顶部" }, - "name-show-unfilled-area": "", - "name-value-display": "", + "name-show-unfilled-area": "显示未填充区域", + "name-value-display": "值显示", "value-display-options": { - "label-hidden": "", - "label-text-color": "", - "label-value-color": "" + "label-hidden": "隐藏", + "label-text-color": "文字颜色", + "label-value-color": "值颜色" } }, "bookmarks-page": { @@ -3367,7 +3457,7 @@ "delete-modal-restore-dashboards-text": "此操作将立即删除所选文件夹,但所选数据面板将被标记为在 30 天后删除。您的组织管理员可以在 30 天期限内随时还原数据面板。文件夹无法还原。", "delete-modal-text": "此操作将删除以下内容:", "delete-modal-title": "删除", - "delete-provisioned-folder": "", + "delete-provisioned-folder": "删除已预置的文件夹", "deleting": "正在删除...", "manage-permissions-button": "管理权限", "move-button": "移动", @@ -3404,12 +3494,12 @@ "tags-column": "标签" }, "delete-provisioned-folder-form": { - "alert-folder-deleted-successfully": "", - "api-error": "", - "button-cancel": "", - "button-delete": "", - "button-deleting": "", - "delete-warning": "" + "alert-folder-deleted-successfully": "文件夹已成功删除", + "api-error": "删除文件夹失败", + "button-cancel": "取消", + "button-delete": "删除", + "button-deleting": "正在删除...", + "delete-warning": "这将删除此文件夹及其所有子文件夹。总体而言,这将影响:" }, "descendant-count": { "title-unable-to-retrieve-descendant-information": "无法检索后代信息" @@ -3455,11 +3545,11 @@ "button-create": "创建", "button-creating": "正在创建...", "cancel": "取消", - "error-invalid-folder-name": "文件夹名称无效", "error-required": "文件夹名称为必填项", "folder-name-input-placeholder-enter-folder-name": "输入文件夹名称", "label-folder-name": "文件夹名称", "text-pull-request-created": "已创建拉取请求,对此文件夹进行更改:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "拉取请求已创建", "title-this-repository-is-read-only": "此存储库为只读" }, @@ -3468,52 +3558,52 @@ "text": "未找到与您的查询相关的结果" }, "restore": { - "success": "" + "success": "数据面板 {{name}} 已恢复" }, "text-this-repository-is-read-only": "如果您可以直接访问目标,请复制 JSON 并将其粘贴到那里。", "trash-state-manager": { "label": { - "alphabetically-az": "", - "alphabetically-za": "" + "alphabetically-az": "按字母顺序 (A-Z)", + "alphabetically-za": "按字母顺序 (Z-A)" } } }, "candlestick": { "additional-fields-options": { - "label-ignore": "", - "label-include": "" + "label-ignore": "忽略", + "label-include": "包括" }, "candle-style-options": { - "label-candles": "", - "label-ohlc-bars": "" + "label-candles": "K 线", + "label-ohlc-bars": "OHLC 条" }, - "category-candlestick": "", + "category-candlestick": "K 线图", "color-strategy-options": { - "label-since-open": "", - "label-since-prior-close": "" + "label-since-open": "自开盘", + "label-since-prior-close": "自上次收盘" }, - "description-additional-fields": "", - "description-close": "", - "description-high": "", - "description-low": "", - "description-open": "", - "description-volume": "", + "description-additional-fields": "使用标准时间序列选项来配置上面未映射的任何字段", + "description-close": "期末值", + "description-high": "期内最大值", + "description-low": "期内最小值", + "description-open": "期初值", + "description-volume": "期内样本数", "mode-options": { - "label-both": "", - "label-candles": "", - "label-volume": "" + "label-both": "两个", + "label-candles": "K 线", + "label-volume": "成交量" }, - "name-additional-fields": "", - "name-candle-style": "", - "name-close": "", - "name-color-strategy": "", - "name-down-color": "", - "name-high": "", - "name-low": "", - "name-mode": "", - "name-open": "", - "name-up-color": "", - "name-volume": "" + "name-additional-fields": "其他字段", + "name-candle-style": "K 线样式", + "name-close": "关闭", + "name-color-strategy": "颜色策略", + "name-down-color": "下降的颜色", + "name-high": "高", + "name-low": "低", + "name-mode": "模式", + "name-open": "打开", + "name-up-color": "上部颜色", + "name-volume": "成交量" }, "canvas": { "apieditor": { @@ -3528,18 +3618,18 @@ } }, "button-item": { - "category-button": "", + "category-button": "按钮", "label": { "center": "中心", "left": "左侧", "right": "右侧" }, - "name-align-text": "", - "name-api": "", - "name-style": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", + "name-align-text": "对齐文字", + "name-api": "API", + "name-style": "样式", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", "placeholder": { "auto": "自动" } @@ -3566,20 +3656,20 @@ } } }, - "category-arrow-direction": "", - "category-background": "", - "category-border": "", - "category-canvas": "", - "category-color": "", - "category-data-links": "", - "category-line-style": "", - "category-metric-value": "", - "category-radius": "", - "category-selected-connection": "", - "category-selected-element": "", - "category-size": "", + "category-arrow-direction": "箭头方向", + "category-background": "背景", + "category-border": "边框", + "category-canvas": "画布", + "category-color": "颜色", + "category-data-links": "数据关联和操作", + "category-line-style": "线样式", + "category-metric-value": "指标值", + "category-radius": "半径", + "category-selected-connection": "所选连接", + "category-selected-element": "所选元素 ({{element}})", + "category-size": "大小", "cloud-item": { - "category-cloud": "", + "category-cloud": "Cloud", "label": { "bottom": "底部", "center": "中心", @@ -3588,48 +3678,48 @@ "right": "右侧", "top": "顶部" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "placeholder": { "auto": "自动" } }, - "description-experimental-types": "", - "description-infinite-panning": "", - "description-inline-editing": "", - "description-pan-zoom": "", + "description-experimental-types": "启用实验元素类型的选择", + "description-infinite-panning": "启用无限平移 - 适用于内容宽广的画布。警告:这是一项实验性功能,目前仅适用于受顶部/左侧约束的元素", + "description-inline-editing": "启用直接编辑面板", + "description-pan-zoom": "启用平移和缩放", "direction-options": { - "label-both": "", - "label-forward": "", - "label-none": "", - "label-reverse": "" + "label-both": "两个", + "label-forward": "前进", + "label-none": "无", + "label-reverse": "反转" }, "drone-front-item": { - "category-drone-front": "", - "name-roll-angle": "" + "category-drone-front": "无人机正面", + "name-roll-angle": "横滚角" }, "drone-side-item": { - "category-drone-side": "", - "name-pitch-angle": "" + "category-drone-side": "无人机侧面", + "name-pitch-angle": "俯仰角" }, "drone-top-item": { - "category-drone-top": "", - "name-back-left-rotor-rpm": "", - "name-back-right-rotor-rpm": "", - "name-front-left-rotor-rpm": "", - "name-front-right-rotor-rpm": "", - "name-yaw-angle": "" + "category-drone-top": "无人机顶部", + "name-back-left-rotor-rpm": "左后旋翼转速", + "name-back-right-rotor-rpm": "右后旋翼转速", + "name-front-left-rotor-rpm": "左前旋翼转速", + "name-front-right-rotor-rpm": "右前旋翼转速", + "name-yaw-angle": "偏航角" }, "element-editor": { - "category-layout": "", - "description-unsupported": "", - "name-quick-placement": "" + "category-layout": "布局", + "description-unsupported": "当前设置不支持所选元素类型。请启用高级元素类型。", + "name-quick-placement": "快速放置" }, "ellipse-item": { - "category-ellipse": "", + "category-ellipse": "椭圆", "label": { "bottom": "底部", "center": "中心", @@ -3638,50 +3728,50 @@ "right": "右侧", "top": "顶部" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "placeholder": { "auto": "自动" } }, "icon-item": { - "category-icon": "", - "name-fill-color": "", - "name-svg-path": "" + "category-icon": "图标", + "name-fill-color": "填充颜色", + "name-svg-path": "SVG 路径" }, "inline-edit": { "canvas-inline-editor": "画布内联编辑器", "tooltip-close-inline-editor": "关闭内联编辑器" }, "inline-edit-body": { - "category-selected-element": "", + "category-selected-element": "所选元素 ({{element}})", "label-add-item": "添加项目", "please-select-an-element": "请选择一个元素" }, - "label-actions": "", - "label-color": "", - "label-direction": "", - "label-image": "", - "label-image-size": "", - "label-line-style": "", - "label-links": "", - "label-radius": "", - "label-size": "", - "label-width": "", + "label-actions": "操作", + "label-color": "颜色", + "label-direction": "方向", + "label-image": "图片", + "label-image-size": "图片大小", + "label-line-style": "线样式", + "label-links": "关联", + "label-radius": "半径", + "label-size": "大小", + "label-width": "宽度", "layer-editor": { - "category-layer": "", - "category-layout": "", - "name-constraints": "", - "name-elements": "" + "category-layer": "图层", + "category-layout": "布局", + "name-constraints": "约束条件", + "name-elements": "元素" }, "line-style-editor": { "label-animate": "动画" }, "line-style-options": { - "label-solid": "" + "label-solid": "纯色" }, "metric-value-item": { "label": { @@ -3696,15 +3786,15 @@ "auto": "自动" } }, - "name-align-text": "", - "name-color": "", - "name-experimental-types": "", - "name-infinite-panning": "", - "name-inline-editing": "", - "name-pan-zoom": "", - "name-text": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-color": "文字颜色", + "name-experimental-types": "实验性元素类型", + "name-infinite-panning": "无限平移", + "name-inline-editing": "嵌入式编辑", + "name-pan-zoom": "平移和缩放", + "name-text": "文字", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "not-found-display": { "not-found": "<0>未找到:" }, @@ -3717,7 +3807,7 @@ "zoom-scroll-wheel": "缩放:滚轮" }, "parallelogram-item": { - "category-parallelogram": "", + "category-parallelogram": "平行四边形", "label": { "bottom": "底部", "center": "中心", @@ -3726,11 +3816,11 @@ "right": "右侧", "top": "顶部" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "placeholder": { "auto": "自动" } @@ -3743,21 +3833,21 @@ }, "placement-editor": { "horizontal-options": { - "label-center": "", - "label-left": "", - "label-left-and-right": "", - "label-right": "", - "label-scale": "" + "label-center": "中心", + "label-left": "左侧", + "label-left-and-right": "左和右", + "label-right": "右侧", + "label-scale": "比例" }, "label-constraints": "约束条件", "label-position": "位置", "loading": "加载中...", "vertical-options": { - "label-bottom": "", - "label-center": "", - "label-scale": "", - "label-top": "", - "label-top-and-bottom": "" + "label-bottom": "底部", + "label-center": "中心", + "label-scale": "比例", + "label-top": "顶部", + "label-top-and-bottom": "顶部和底部" } }, "quick-positioning": { @@ -3769,7 +3859,7 @@ "tooltip-align-vertical-centers": "垂直居中对齐" }, "rectangle-item": { - "category-rectangle": "", + "category-rectangle": "矩形", "label": { "bottom": "底部", "center": "中心", @@ -3778,33 +3868,33 @@ "right": "右侧", "top": "顶部" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "placeholder": { "auto": "自动" } }, "server-item": { - "category-server": "", - "name-blink-rate": "", - "name-bulb-color": "", - "name-status-color": "", - "name-type": "", + "category-server": "服务器", + "name-blink-rate": "闪烁频率 [hz](0 = 关闭)", + "name-bulb-color": "灯泡颜色", + "name-status-color": "状态颜色", + "name-type": "类型", "type-options": { - "label-database": "", - "label-single": "", - "label-stack": "", - "label-terminal": "" + "label-database": "数据库", + "label-single": "单一", + "label-stack": "堆栈", + "label-terminal": "终端" } }, "text-display": { "double-click-to-set": "双击以设置文本" }, "text-item": { - "category-text": "", + "category-text": "文字", "label": { "bottom": "底部", "center": "中心", @@ -3813,11 +3903,11 @@ "right": "右侧", "top": "顶部" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "placeholder": { "auto": "自动" } @@ -3839,7 +3929,7 @@ "tooltip-remove": "删除" }, "triangle-item": { - "category-triangle": "", + "category-triangle": "三角形", "label": { "bottom": "底部", "center": "中心", @@ -3848,18 +3938,18 @@ "right": "右侧", "top": "顶部" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "对齐文字", + "name-text": "文字", + "name-text-color": "文字颜色", + "name-text-size": "文字大小", + "name-vertical-align": "垂直对齐", "placeholder": { "auto": "自动" } }, "wind-turbine-item": { - "category-wind-turbine": "", - "name-rpm": "" + "category-wind-turbine": "风轮机", + "name-rpm": "转速" } }, "carousel": { @@ -3876,7 +3966,8 @@ "close-button": { "tooltip": "关闭" }, - "cloud-enterprise-feature-badge": "", + "cloud-enterprise-feature-badge": "Cloud 和 Enterprise", + "cloud-feature-badge": "Cloud", "combobox": { "async": { "error": "加载选项时发生错误。" @@ -4145,8 +4236,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "通过将时间范围除以指定的计数来动态计算间隔" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4308,12 +4399,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "继续前往外部网站?" } }, "render-left-actions": { - "text-public": "公开", "tooltip-view-as-scene": "作为场景查看" } }, @@ -4404,7 +4497,7 @@ } }, "description": { - "action": "" + "action": "更改数据面板描述" }, "dynamic-config-value-editor": { "render-label": { @@ -4413,12 +4506,12 @@ }, "edit-actions": { "add": "添加{{typeName}}", - "move": "", + "move": "移动{{typeName}}", "panel-background": "更改面板背景", "panel-description": "更改面板描述", - "panel-max-repeats-per-row": "", + "panel-max-repeats-per-row": "每行最大重复次数", "panel-repeat-direction": "重复方向", - "panel-repeat-variable": "", + "panel-repeat-variable": "面板重复方式", "panel-title": "更改面板标题", "paste-panel": "粘贴面板", "remove": "移除{{typeName}}", @@ -4506,6 +4599,14 @@ "errors": { "failed-to-load": "加载数据面板失败" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "获取数据面板失败" @@ -4522,6 +4623,13 @@ "editable": "可编辑", "readonly": "只读" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4537,7 +4645,7 @@ "get-field-override-categories": { "label-add-field-override": "添加字段覆盖", "label-add-override-property": "添加覆盖属性", - "override-name": "", + "override-name": "覆盖 {{overrideNum}}", "title": { "add-button": "添加按钮" } @@ -4571,6 +4679,18 @@ "transform-data": "转换数据" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4749,7 +4869,7 @@ "title-matched_other": "匹配了 {{count}}/{{totalCount}} 个选项" }, "outline": { - "repeated-item": "", + "repeated-item": "重复", "tree-item": { "empty": "(空缺)", "no-title": "< 无标题 >" @@ -4825,6 +4945,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "您确定要将数据面板还原到版本吗 {{version}}?所有未保存的更改都将丢失。", + "confirmText-restore-version": "", "title-restore-version": "恢复版本" }, "row-options-button": { @@ -4875,6 +4996,9 @@ "title-not-unique": "此标题并不唯一" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "另存为" }, @@ -4903,12 +5027,13 @@ "label-changes": "变更", "label-details": "详情", "title": "保存仪表板", - "title-copy": "", - "title-provisioned": "" + "title-copy": "保存数据面板副本", + "title-provisioned": "已预置的数据面板" }, "save-dashboard-error-proxy": { "body-name-exists": "所选文件夹中已存在具有相同名称的数据面板。<1><2>您仍然要保存此数据面板吗?。", "body-version-mismatch": "其他人已更新此数据面板<1><2>您仍然要保存此数据面板吗?。", + "confirmText-save-and-overwrite": "", "title-name-exists": "冲突", "title-version-mismatch": "冲突" }, @@ -4980,7 +5105,7 @@ } }, "title": { - "action": "" + "action": "更改数据面板标题" }, "toolbar": { "add": "添加", @@ -5074,7 +5199,6 @@ "playlist-next": "前往下一个仪表板", "playlist-previous": "前往上一个仪表板", "playlist-stop": "停止播放列表", - "public-dashboard": "公开", "refresh": "刷新仪表板", "save": "保存仪表板", "save-dashboard": { @@ -5105,7 +5229,9 @@ "label-apply-transformation-to": "将转换应用于" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "调试", "title-disable-transformation": "禁用转换", "title-filter": "筛选条件", @@ -5125,12 +5251,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "搜索转换", "show-images": "显示图像", - "title-add-another-transformation": "添加其他转换" + "title-add-another-transformation": "添加其他转换", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "添加其他转换" }, + "confirmText-delete-all": "", "delete-all-transformations": "删除所有转换", "title-delete-all-transformations": "删除所有转换?", "tooltip-clear-search": "清除搜索", @@ -5167,6 +5298,7 @@ "version-history-table": { "aria-label-toggle-selection": "切换版本 {{version}} 的选择", "date": "日期", + "name-latest": "", "notes": "备注", "restore": "还原", "updated-by": "更新人", @@ -5243,7 +5375,8 @@ "description-enables-users-custom-values": "允许用户向列表中添加自定义值", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "以 CSV 格式提供维度:{{name}}, {{value}}", "label-data-source": "数据源", - "label-use-static-key-dimensions": "使用静态键维度" + "label-use-static-key-dimensions": "使用静态键维度", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5316,6 +5449,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "撤销公共网址" } @@ -5327,6 +5463,7 @@ }, "custom-variable-form": { "custom-options": "自定义选项", + "name-values-separated-comma": "", "selection-options": "选择内容选项" }, "dashboard-edit-pane-renderer": { @@ -5345,6 +5482,12 @@ "label-type": "类型", "label-url": "URL", "label-with-tags": "带标记", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "打开数据面板" }, "dashboard-link-list": { @@ -5357,15 +5500,9 @@ "type": "类型" }, "dashboard-preview-banner": { - "not-saved": "该值尚未保存在 Grafana 数据库中", "not-yet-saved": "该值未保存在 Grafana 数据库中", - "open-pull-request-in-git-hub": "在 GitHub 中打开拉取请求", - "title-dashboard-loaded-branch-git-hub": "此数据面板从 GitHub 的分支加载。", "title-dashboard-loaded-external-repository": "此数据面板从外部存储库加载", - "title-dashboard-loaded-request-git-hub": "此数据面板从 GitHub 的拉取请求加载。", - "title-error-loading-dashboard": "加载数据面板时出错", - "value-not-saved": "该值尚未保存在 Grafana 数据库中", - "view-pull-request-in-git-hub": "在 GitHub 中查看拉取请求" + "title-error-loading-dashboard": "加载数据面板时出错" }, "dashboard-scene": { "text": { @@ -5391,6 +5528,8 @@ "data-source-options": "数据源选项", "description-instance-name-filter": "正则表达式筛选器,用于在变量值列表中选择数据源实例。保留所有为空。", "example-instance-name-filter": "示例:", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "选择内容选项" }, "default-grid-layout-manager": { @@ -5401,12 +5540,12 @@ } }, "delete-provisioned-dashboard-form": { - "api-error": "", + "api-error": "删除数据面板失败", "cancel-action": "取消", "delete-action": "删除数据面板", - "delete-read-only-file-message": "", + "delete-read-only-file-message": "此数据面板无法直接从 Grafana 删除,因为存储库是只读的。要删除此数据面板,请从 Git 存储库中移除相应文件。", "deleting": "正在删除...", - "drawer-title": "", + "drawer-title": "删除已预置的数据面板", "title-this-repository-is-read-only": "此存储库为只读" }, "description-label": { @@ -5420,7 +5559,7 @@ } }, "edit-provisioned-dashboard-form": { - "success": "" + "success": "数据面板更改已成功保存" }, "email-list": { "aria-label-emailmenu": "切换电子邮件菜单" @@ -5436,12 +5575,61 @@ "empty-transformations-message": { "add-transformation": "添加转换" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "列选项", "row-height-options": "行高度选项" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5466,7 +5654,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "以 CSV 格式提供维度:{{name}}, {{value}}", "group-by-options": "分组选项", "label-data-source": "数据源", - "label-use-static-group-by-dimensions": "使用静态组维度" + "label-use-static-group-by-dimensions": "使用静态组维度", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "复制到剪贴板", @@ -5502,9 +5691,14 @@ "apply": "应用" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "计算值不会低于此阈值", "description-step-count": "计算值时,应将当前时间范围除以多少倍", - "interval-options": "间隔选项" + "interval-options": "间隔选项", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5523,11 +5717,28 @@ "last-edited": "{{timeAgo}}由 ", "usage-count_other": "用于 {{count}} 个数据面板" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "所选文件夹中已存在具有相同名称的数据面板。您仍然要保存此数据面板吗?", "title-name-already-exists": "名称已存在" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "继续前往外部网站?" } @@ -5541,7 +5752,7 @@ } }, "panel-data-alerting-tab": { - "tab-label": "" + "tab-label": "警报" }, "panel-data-alerting-tab-rendered": { "alert": { @@ -5552,17 +5763,19 @@ "title-dashboard-not-saved": "数据面板未保存" }, "panel-data-queries-tab": { - "tab-label": "" + "tab-label": "查询" }, "panel-data-queries-tab-rendered": { "add-query": "添加查询", "expression": "表达式" }, "panel-data-transformations-tab": { - "tab-label": "" + "tab-label": "转换" }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "添加其他转换", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "删除所有转换", "title-delete-all-transformations": "删除所有转换?" }, @@ -5615,9 +5828,13 @@ "description-examples": "命名捕获组可用于分隔显示文本和值(<1>参见示例)。", "description-optional": "可选,如果您想要提取序列名称或指标节点段的一部分。", "label-data-source": "数据源", + "label-static-options-sort": "", "label-target-data-source": "目标数据源", + "label-use-static-options": "", + "name-regex": "", "query-options": "查询选项", - "selection-options": "选择内容选项" + "selection-options": "选择内容选项", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5630,6 +5847,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "您确定要将数据面板还原到版本吗 {{version}}?所有未保存的更改都将丢失。", + "confirmText-restore-version": "", "title-restore-version": "恢复版本" }, "save-button": { @@ -5647,15 +5865,16 @@ "label-folder": "文件夹", "render-footer": { "title-failed-to-save-dashboard": "保存数据面板失败" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { "label-changes": "变更", "label-details": "详情", "title": "保存仪表板", - "title-copy": "", - "title-provisioned": "" + "title-copy": "保存数据面板副本", + "title-provisioned": "已预置的数据面板" } }, "save-dashboard-form": { @@ -5722,7 +5941,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "允许同时选择多个值", "description-enables-option-include-variables": "启用选项以包含所有值", - "description-enables-users-custom-values": "允许用户向列表中添加自定义值" + "description-enables-users-custom-values": "允许用户向列表中添加自定义值", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "切换共享菜单" @@ -5742,6 +5965,9 @@ "copy-to-clipboard-failed": "复制到剪贴板失败" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(可选)", "text-options": "文本选项" @@ -5765,6 +5991,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "您确定要取消关联此面板吗?" }, "unsaved-changes-modal": { @@ -5781,6 +6009,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "此变量未被任何变量或数据面板引用。", "aria-label-variable-referenced-other-variables-dashboard": "此变量被其他变量或数据面板引用。", @@ -5790,10 +6021,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "变量编辑器表单", "back-to-list": "回到列表", + "confirmText": { + "delete-variable": "" + }, "delete": "删除", "description-optional-display-name": "可选显示名称", "description-template-variable-characters": "模板变量的名称。(最多 50 个字符)", "general": "概况", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "描述性文本", "placeholder-label-name": "标签名称", "placeholder-variable-name": "变量名称", @@ -5808,13 +6045,25 @@ "variable": "变量" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "删除变量", "tooltip-duplicate-variable": "复制变量", "tooltip-remove-variable": "移除变量" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "隐藏" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "显示使用情况:{{variableId}}", "tooltip-show-usages": "显示使用情况" @@ -5841,6 +6090,7 @@ "version-history-table": { "aria-label-toggle-selection": "切换版本 {{version}} 的选择", "date": "日期", + "name-latest": "", "notes": "备注", "restore": "还原", "updated-by": "更新人", @@ -5947,18 +6197,18 @@ } }, "dashlist": { - "category-dashboard-list": "", - "name-folder": "", - "name-include-current-template-variables": "", - "name-include-current-time-range": "", - "name-max-items": "", - "name-query": "", - "name-recently-viewed": "", - "name-search": "", - "name-show-folder-names": "", - "name-show-headings": "", - "name-starred": "", - "name-tags": "" + "category-dashboard-list": "数据面板列表", + "name-folder": "文件夹", + "name-include-current-template-variables": "包含当前模板变量值", + "name-include-current-time-range": "包含当前时间范围", + "name-max-items": "最大项目数", + "name-query": "查询", + "name-recently-viewed": "近期查看", + "name-search": "搜索", + "name-show-folder-names": "显示文件夹名称", + "name-show-headings": "显示标题", + "name-starred": "已加星标", + "name-tags": "标记" }, "data-source-list": { "empty-state": { @@ -6220,15 +6470,16 @@ "dimensions": { "background-size-editor": { "options": { - "label-contain": "", - "label-cover": "", - "label-fill": "", - "label-original": "", - "label-tile": "" + "label-contain": "包含", + "label-cover": "覆盖", + "label-fill": "填充", + "label-original": "原始", + "label-tile": "图块" } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "固定颜色", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "上传" @@ -6242,10 +6493,10 @@ "placeholder-search": "搜索" }, "resource-dimension-editor": { - "description-field": "", - "description-fixed": "", + "description-field": "使用字符串字段结果", + "description-fixed": "固定值", "label-field": "字段", - "label-fixed": "", + "label-fixed": "固定", "label-mappings": "映射", "label-source": "源" }, @@ -6262,27 +6513,34 @@ }, "scalar-dimension-editor": { "fixed-value-options": { - "label-fixed-values": "" + "label-fixed-values": "固定值" }, "label-limit": "限制", "label-value": "值", + "noOptionsMessage-no-fields-found": "", "scalar-options": { - "description-clamped": "", - "description-mod": "", - "label-clamped": "", - "label-mod": "" + "description-clamped": "使用字段值,并限制在最大值与最小值之间", + "description-mod": "使用字段值,并从最大值取模", + "label-clamped": "限制", + "label-mod": "取模" } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "最高", "label-min": "最小", - "label-value": "值" + "label-value": "值", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { - "description-field": "", - "description-fixed": "", + "description-field": "显示字段值", + "description-fixed": "固定值", "label-field": "字段", - "label-fixed": "", + "label-fixed": "固定", "label-source": "源", "label-template": "模板", "label-value": "值" @@ -6296,16 +6554,16 @@ "description-percentage-means-thresholds-relative": "百分比表示阈值相对于最小值和最大值的比例", "modes": { "description": { - "threshold-based-percent-between-minmax": "", - "thresholds-based-absolute-values": "" + "threshold-based-percent-between-minmax": "根据最小/最大值之间的百分比选择阈值", + "thresholds-based-absolute-values": "根据绝对值选择阈值" }, "label": { - "absolute": "", - "percentage": "" + "absolute": "绝对", + "percentage": "百分比" } }, "thresholds-mode": "阈值模式", - "value-base": "" + "value-base": "基本" }, "urlpicker-tab": { "label-preview": "预览" @@ -6848,6 +7106,8 @@ "aria-label-select-service-name-operator": "选择服务名称运算符", "aria-label-select-span-name": "选择跨度名称", "aria-label-select-span-name-operator": "选择跨度名称运算符", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "跨度筛选器", "label-duration": "持续时间", "label-service-name": "服务名称", @@ -6918,6 +7178,8 @@ "split-widen": "宽窗格" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "提供反馈", "label-copied": "已复制!", "label-export": "导出", @@ -7025,6 +7287,7 @@ "when": "满足以下条件时" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "运算" }, "math": { @@ -7055,6 +7318,7 @@ }, "folder-filter": { "clear-folder-button": "清除文件夹", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "文件夹筛选", "select-placeholder": "按文件夹筛选" }, @@ -7100,30 +7364,76 @@ "title": "离开页面?" }, "gauge": { - "category-gauge": "", - "description-min-height": "", - "description-min-width": "", - "description-neutral": "", - "description-show-threshold-labels": "", - "description-show-threshold-markers": "", + "category-gauge": "仪表", + "description-min-height": "最小行高(水平方向)", + "description-min-width": "最小列宽(垂直方向)", + "description-neutral": "留空以使用最小值作为中性点", + "description-show-threshold-labels": "在仪表条周围渲染阈值", + "description-show-threshold-markers": "将阈值渲染为外部条", "gauge-size-options": { - "label-auto": "", - "label-manual": "" + "label-auto": "自动", + "label-manual": "手动" }, - "name-gauge-size": "", - "name-min-height": "", - "name-min-width": "", - "name-neutral": "", - "name-show-threshold-labels": "", - "name-show-threshold-markers": "", - "placeholder-neutral": "" + "name-gauge-size": "仪表大小", + "name-min-height": "最小高度", + "name-min-width": "最小宽度", + "name-neutral": "中性", + "name-show-threshold-labels": "显示阈值标签", + "name-show-threshold-markers": "显示阈值标记", + "placeholder-neutral": "自动" }, "gen-ai": { "apply-suggestion": "应用", "incomplete-request-error": "很抱歉,我无法完成您的请求。请重试。", "send-custom-feedback": "发送" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "纬度", "label-longitude": "经度" @@ -7132,6 +7442,14 @@ "center": "居中:", "zoom": "缩放:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "图层" @@ -7154,6 +7472,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "添加地理图样式规则" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "添加图层", "no-layers": "没有图层?" @@ -7164,16 +7490,38 @@ "label-zoom": "缩放", "use-current-map-settings": "使用当前地图设置" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "符号" }, "measure-overlay": { "tooltip-show-measure-tools": "显示测量工具" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "基础地图图层由服务器管理员配置。" }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "对齐", "label-baseline": "基线", "label-color": "颜色", @@ -7187,7 +7535,14 @@ "label-symbol-vertical-align": "符号垂直对齐", "label-text-label": "文本标签", "label-x-offset": "X 轴偏移量", - "label-y-offset": "Y 轴偏移量" + "label-y-offset": "Y 轴偏移量", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "比较运算符", @@ -7198,6 +7553,15 @@ "placeholder-feature-property": "功能属性", "placeholder-numeric-value": "数字值", "placeholder-value": "值" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7350,85 +7714,85 @@ }, "builder": { "axis": { - "category-axis": "", - "color-label": "", + "category-axis": "轴", + "color-label": "颜色", "color-options": { - "label-series": "", - "label-text": "" + "label-series": "序列", + "label-text": "文字" }, "grid-line-options": { - "label-auto": "", - "label-off": "", - "label-on": "" + "label-auto": "自动", + "label-off": "关", + "label-on": "开" }, - "name-centered-zero": "", - "name-grid-lines": "", - "name-label": "", - "name-placement": "", - "name-scale": "", - "name-show-border": "", - "name-soft-max": "", - "name-soft-min": "", - "name-width": "", - "placeholder-label": "", - "placeholder-soft-max": "", - "placeholder-soft-min": "", - "placeholder-width": "", + "name-centered-zero": "居中零", + "name-grid-lines": "显示栅格线", + "name-label": "标签", + "name-placement": "放置", + "name-scale": "比例", + "name-show-border": "显示边框", + "name-soft-max": "软上限", + "name-soft-min": "软下限", + "name-width": "宽度", + "placeholder-label": "可选文字", + "placeholder-soft-max": "请参阅:标准选项 > 最大", + "placeholder-soft-min": "请参阅:标准选项 > 最小", + "placeholder-width": "自动", "scale-distribution-editor": { "distribution-options": { - "label-linear": "", - "label-log": "", - "label-symlog": "" + "label-linear": "线性", + "label-log": "对数图", + "label-symlog": "对称对数" } } }, "legend": { - "category": "", - "description-values": "", + "category": "图例", + "description-values": "选择要在图例中显示的值或计算", "mode-options": { - "label-list": "", - "label-table": "" + "label-list": "列表", + "label-table": "表格" }, - "name-mode": "", - "name-placement": "", - "name-values": "", - "name-visibility": "", - "name-width": "", + "name-mode": "模式", + "name-placement": "放置", + "name-values": "值", + "name-visibility": "可见度", + "name-width": "宽度", "placement-options": { - "label-bottom": "", - "label-right": "" + "label-bottom": "底部", + "label-right": "右侧" } }, "stacking": { - "name-stack-series": "" + "name-stack-series": "堆叠序列" }, "text": { - "category-text-size": "", - "name-percent-change": "", - "name-title": "", - "name-value": "", - "placeholder-percent-change": "", - "placeholder-title": "", - "placeholder-value": "" + "category-text-size": "文字大小", + "name-percent-change": "百分比变化", + "name-title": "标题", + "name-value": "值", + "placeholder-percent-change": "自动", + "placeholder-title": "自动", + "placeholder-value": "自动" }, "tooltip": { - "category": "", - "description-hover-proximity": "", + "category": "工具提示", + "description-hover-proximity": "光标必须多么靠近某个点才能触发工具提示,以像素为单位", "modeOptions": { - "label-all": "", - "label-hidden": "", - "label-single": "" + "label-all": "全部", + "label-hidden": "隐藏", + "label-single": "单一" }, - "name-hide-zeros": "", - "name-hover-proximity": "", - "name-max-height": "", - "name-max-width": "", - "name-tooltip-mode": "", - "name-values-sort-order": "", + "name-hide-zeros": "隐藏零", + "name-hover-proximity": "悬停接近", + "name-max-height": "最大高度", + "name-max-width": "最大宽度", + "name-tooltip-mode": "工具提示模式", + "name-values-sort-order": "值排序顺序", "sortOptions": { - "label-ascending": "", - "label-descending": "", - "label-none": "" + "label-ascending": "升序", + "label-descending": "降序", + "label-none": "无" } } }, @@ -7447,7 +7811,8 @@ "aria-label-selected-color": "{{colorLabel}} 颜色" }, "confirm-button": { - "cancel": "取消" + "cancel": "取消", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "键入“{{confirmPromptText}}”以确认" @@ -7589,19 +7954,19 @@ "tooltip-expand-all": "展开所有行" }, "matchers-ui": { - "description-field-name-by-regex-matcher": "", - "description-fields-by-query": "", - "description-fields-with-name": "", - "description-fields-with-type": "", - "description-fields-with-value": "", + "description-field-name-by-regex-matcher": "为名称与正则表达式匹配的字段设置属性", + "description-fields-by-query": "为特定查询中的字段设置属性", + "description-fields-with-name": "为特定字段设置属性", + "description-fields-with-type": "为特定类型(数字、字符串、布尔值)的字段设置属性", + "description-fields-with-value": "为具有缩减条件的字段设置属性", "field-name-picker": { - "placeholder": "" + "placeholder": "选择字段" }, - "name-field-name-by-regex-matcher": "", - "name-fields-by-query": "", - "name-fields-with-name": "", - "name-fields-with-type": "", - "name-fields-with-value": "" + "name-field-name-by-regex-matcher": "名称匹配正则表达式的字段", + "name-fields-by-query": "查询返回的字段", + "name-fields-with-name": "带名称的字段", + "name-fields-with-type": "带类型的字段", + "name-fields-with-value": "带值的字段" }, "menu-item": { "keyboard-shortcut-label": "键盘快捷键" @@ -7629,6 +7994,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "切换折叠面板", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "取消查询", "tooltip-cancel-loading": "取消查询", "tooltip-stop-streaming": "停止流媒体传输", @@ -7695,6 +8062,9 @@ "filter-popup-match-case": "区分大小写", "inspect-drawer-title": "检查值", "inspect-menu-label": "检查值", + "nested-table": { + "no-data": "" + }, "no-values-label": "没有值", "pagination-summary": "第 {{itemsRangeStart}} - {{displayedEnd}} 行,共 {{numRows}} 行" }, @@ -7703,6 +8073,7 @@ }, "tags-input": { "add": "添加", + "placeholder-new-tag": "", "remove": "移除标记:{{name}}" }, "time-sync-button": { @@ -7719,55 +8090,55 @@ "config": { "get-graph-field-options": { "axis-placement": { - "description-auto": "", - "label-auto": "", - "label-hidden": "", - "label-left": "", - "label-right": "" + "description-auto": "第一个字段在左侧,其他全部靠右排列", + "label-auto": "自动", + "label-hidden": "隐藏", + "label-left": "左侧", + "label-right": "右侧" }, "bar-alignment": { - "description-after": "", - "description-before": "", - "description-center": "" + "description-after": "之后", + "description-before": "之前", + "description-center": "中心" }, "draw-style": { - "label-bars": "", - "label-lines": "", - "label-points": "" + "label-bars": "条", + "label-lines": "线", + "label-points": "点" }, "fill-gradient": { - "description-hue": "", - "description-opacity": "", - "description-scheme": "", - "label-hue": "", - "label-none": "", - "label-opacity": "", - "label-scheme": "" + "description-hue": "小色调渐变", + "description-opacity": "启用填充不透明度渐变", + "description-scheme": "使用配色方案定义渐变", + "label-hue": "色调", + "label-none": "无", + "label-opacity": "不透明度", + "label-scheme": "方案" }, "line-interpolation": { - "description-linear": "", - "description-smooth": "", - "description-step-after": "", - "description-step-before": "" + "description-linear": "线性", + "description-smooth": "顺滑", + "description-step-after": "后续步骤", + "description-step-before": "前导步骤" }, "show-points": { - "description-auto": "", - "label-always": "", - "label-auto": "", - "label-never": "" + "description-auto": "密度低时显示点", + "label-always": "始终", + "label-auto": "自动", + "label-never": "从不" }, "stacking": { - "label-100": "", - "label-normal": "", - "label-off": "" + "label-100": "100%", + "label-normal": "正常", + "label-off": "关" }, "thresholds-display-mode": { - "label-dashed-lines": "", - "label-filled-regions": "", - "label-filled-regions-and-dashed-lines": "", - "label-filled-regions-and-lines": "", - "label-lines": "", - "label-off": "" + "label-dashed-lines": "作为线(虚线)", + "label-filled-regions": "作为填充区域", + "label-filled-regions-and-dashed-lines": "作为填充区域和线(虚线)", + "label-filled-regions-and-lines": "作为填充区域和线", + "label-lines": "作为线", + "label-off": "关" } } } @@ -7796,6 +8167,12 @@ "footer-click-to-action": "点击以 {{actionTitle}}", "footer-click-to-navigate": "点击打开 {{linkTitle}}", "timestamp": "时间戳" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -7808,78 +8185,78 @@ }, "heatmap": { "calculate-from-data-options": { - "label-no": "", - "label-yes": "" + "label-no": "否", + "label-yes": "是" }, - "category-cell-display": "", - "category-colors": "", - "category-exemplars": "", - "category-heatmap": "", - "category-legend": "", - "category-tooltip": "", - "category-y-axis": "", + "category-cell-display": "单元格显示", + "category-colors": "颜色", + "category-exemplars": "范例", + "category-heatmap": "热力图", + "category-legend": "图例", + "category-tooltip": "工具提示", + "category-y-axis": "Y 轴", "mode-options": { - "label-opacity": "", - "label-scheme": "" + "label-opacity": "不透明度", + "label-scheme": "方案" }, - "name-axis-label": "", - "name-axis-width": "", - "name-calculate-from-data": "", - "name-cell-gap": "", - "name-color": "", - "name-decimals": "", - "name-end-color-at-value": "", - "name-exponent": "", - "name-hide-cells-gt": "", - "name-hide-cells-lt": "", - "name-max-height": "", - "name-max-value": "", - "name-max-width": "", - "name-min-value": "", - "name-mode": "", - "name-placement": "", - "name-reverse": "", - "name-scale": "", - "name-scheme": "", - "name-show-color-scale": "", - "name-show-histogram": "", - "name-show-legend": "", - "name-start-color-from-value": "", - "name-steps": "", - "name-tick-alignment": "", - "name-tooltip-mode": "", - "name-unit": "", - "name-value-name": "", - "name-y-axis-scale": "", - "placeholder-axis-label": "", - "placeholder-axis-width": "", - "placeholder-decimals": "", - "placeholder-end-color-at-value": "", - "placeholder-hide-cells-gt": "", - "placeholder-hide-cells-lt": "", - "placeholder-max-value": "", - "placeholder-min-value": "", - "placeholder-start-color-from-value": "", - "placeholder-value-name": "", + "name-axis-label": "轴标签", + "name-axis-width": "轴宽度", + "name-calculate-from-data": "从数据计算", + "name-cell-gap": "单元格间隙", + "name-color": "颜色", + "name-decimals": "小数点位数", + "name-end-color-at-value": "在该值处结束颜色比例", + "name-exponent": "指数", + "name-hide-cells-gt": "隐藏大于或等于该值的单元格", + "name-hide-cells-lt": "隐藏小于或等于该值的单元格", + "name-max-height": "最大高度", + "name-max-value": "最大值", + "name-max-width": "最大宽度", + "name-min-value": "最小值", + "name-mode": "模式", + "name-placement": "放置", + "name-reverse": "反转", + "name-scale": "比例", + "name-scheme": "方案", + "name-show-color-scale": "显示颜色比例", + "name-show-histogram": "显示直方图(Y轴)", + "name-show-legend": "显示图例", + "name-start-color-from-value": "从该值处开始颜色比例", + "name-steps": "步骤", + "name-tick-alignment": "刻度对齐", + "name-tooltip-mode": "工具提示模式", + "name-unit": "单位", + "name-value-name": "值名称", + "name-y-axis-scale": "Y 轴比例", + "placeholder-axis-label": "自动", + "placeholder-axis-width": "自动", + "placeholder-decimals": "自动", + "placeholder-end-color-at-value": "自动(最大)", + "placeholder-hide-cells-gt": "无", + "placeholder-hide-cells-lt": "无", + "placeholder-max-value": "自动", + "placeholder-min-value": "自动", + "placeholder-start-color-from-value": "自动(最小)", + "placeholder-value-name": "值", "placement-options": { - "label-hidden": "", - "label-left": "", - "label-right": "" + "label-hidden": "隐藏", + "label-left": "左侧", + "label-right": "右侧" }, "scale-options": { - "label-exponential": "", - "label-linear": "" + "label-exponential": "指数", + "label-linear": "线性" }, "tick-alignment-options": { - "label-auto": "", - "label-bottom": "", - "label-middle": "", - "label-top": "" + "label-auto": "自动", + "label-bottom": "底部 (GE)", + "label-middle": "中部", + "label-top": "顶部 (LE)" }, "tooltip-mode-options": { - "label-all": "", - "label-hidden": "", - "label-single": "" + "label-all": "全部", + "label-hidden": "隐藏", + "label-single": "单一" } }, "help-modal": { @@ -7939,14 +8316,14 @@ "troubleshooting-help": "要请求故障排除帮助,请将此面板的快照发送给 Grafana Labs 技术支持。快照包含查询响应数据和面板设置。" }, "histogram": { - "category-histogram": "", + "category-histogram": "直方图", "histogram-panel": { "no-histogram-found-in-response": "回复中未找到直方图" }, - "name-fill-opacity": "", - "name-gradient-mode": "", - "name-line-width": "", - "name-stacking": "" + "name-fill-opacity": "填充不透明度", + "name-gradient-mode": "渐变模式", + "name-line-width": "线宽度", + "name-stacking": "堆叠" }, "inspector": { "inspect-data-tab": { @@ -8172,6 +8549,10 @@ "add-library-panel-modal": { "title-create-library-panel": "创建库面板" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "要删除这个面板吗?" }, @@ -8194,7 +8575,7 @@ "usage-count_other": "用于 {{count}} 个数据面板" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "按名称或描述搜索" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "正在加载库面板..." @@ -8242,9 +8623,7 @@ "share-panel": { "config-description": "使用以下自定义设置,创建一个个性化的直接链接,以便在您的组织内共享面板:", "download-image": "下载图像", - "render-image": "生成图像", - "render-image-error": "渲染面板图像失败", - "render-image-error-description": "生成图像时发生错误" + "render-image": "生成图像" } }, "live": { @@ -8320,19 +8699,28 @@ } }, "logs": { - "category-logs": "", + "category-logs": "日志", "deduplication-options": { - "label-exact": "", - "label-none": "", - "label-numbers": "", - "label-signature": "" + "label-exact": "确切", + "label-none": "无", + "label-numbers": "数字", + "label-signature": "签名" + }, + "description-enable-infinite-scrolling": "实验性。通过滚动到日志列表底部来请求更多结果。", + "description-enable-syntax-highlighting": "使用预定义的语法着色规则来突出显示日志行的相关部分", + "description-show-controls": "显示控件以跳转到最后一行或第一行日志,并按日志级别筛选", + "fields": { + "type": { + "loki": { + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" + } + } }, - "description-enable-infinite-scrolling": "", - "description-enable-syntax-highlighting": "", - "description-show-controls": "", "font-size-options": { - "label-default": "", - "label-small": "" + "label-default": "默认", + "label-small": "小" }, "get-dataframe-fields": { "vars": { @@ -8353,7 +8741,6 @@ "label-wrap-lines": "多行显示" }, "log-details": { - "close": "关闭日志详情", "fields": "字段", "links": "链接", "log-line": "日志行", @@ -8379,6 +8766,42 @@ "show-more": "展开", "tooltip-error": "错误:{{errorMessage}}" }, + "log-line-details": { + "clear-search": "清除", + "close": "关闭日志详情", + "copy-shortlink": "复制短链接", + "copy-to-clipboard": "复制到剪贴板", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "临时统计数据", + "copy-value-to-clipboard": "将值复制到剪贴板", + "filter-out": "筛除值", + "filter-out-query": "在查询 {{query}} 中筛除值", + "toggle-field-button": { + "field-instead-message": "显示此字段而不是消息", + "hide-this-field": "隐藏此字段" + } + }, + "fields-section": "字段", + "hide-log-line": "隐藏日志行", + "inline-mode": "", + "links-section": "关联", + "log-line-field": "", + "log-line-section": "日志行", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "没有要显示的字段。", + "pin-line": "固定日志", + "remove-displayed-field": "", + "search": { + "no-results": "没有要显示的结果。" + }, + "search-placeholder": "搜索字段名称和值", + "show-context": "显示上下文", + "show-log-line": "显示日志行", + "sidebar-mode": "", + "unpin-line": "取消固定日志" + }, "log-line-menu": { "copy-link": "将链接复制到日志行", "copy-log": "复制日志行", @@ -8390,12 +8813,12 @@ "unpin-from-outline": "取消固定日志" }, "log-list-search": { - "close": "", - "filter": "", - "info": "", - "input-placeholder": "", - "next": "", - "prev": "" + "close": "关闭搜索", + "filter": "筛选匹配日志", + "info": "在显示的日志中搜索客户端字符串。不要与查询筛选条件混淆。使用此组件在日志结果中搜索特定字符串。", + "input-placeholder": "在日志中搜索", + "next": "下一个结果", + "prev": "上一个结果" }, "log-row-context-modal": { "error-loading-log-more-logs": "加载更多日志时出错。", @@ -8450,7 +8873,7 @@ "escape-newlines": "修复日志行中错误转义的换行和制表符序列", "font-size-default": "使用小字号", "font-size-small": "使用默认字号", - "hide-search": "", + "hide-search": "关闭搜索", "hide-timestamps": "隐藏时间戳", "hide-unique-labels": "隐藏唯一标签", "newest-first": "先显示最新日志 - 点击以先显示最旧日志", @@ -8459,7 +8882,7 @@ "remove-escaping": "移除转义", "scroll-bottom": "滚动到底部", "scroll-top": "滚动到顶部", - "show-search": "", + "show-search": "在日志结果中搜索", "show-timestamps": "显示时间戳", "show-unique-labels": "显示唯一标签", "unwrap-lines": "取消多行显示", @@ -8477,21 +8900,26 @@ "common-labels": "常用标签:" } }, - "name-common-labels": "", - "name-deduplication": "", - "name-enable-infinite-scrolling": "", - "name-enable-log-details": "", - "name-enable-syntax-highlighting": "", - "name-font-size": "", - "name-order": "", - "name-prettify-json": "", - "name-show-controls": "", - "name-time": "", - "name-unique-labels": "", - "name-wrap-lines": "", + "name-common-labels": "常用标签", + "name-deduplication": "去重", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, + "name-enable-infinite-scrolling": "启用无限滚动", + "name-enable-log-details": "启用日志详情", + "name-enable-syntax-highlighting": "启用语法高亮", + "name-font-size": "字体大小", + "name-order": "顺序", + "name-prettify-json": "美化 JSON", + "name-show-controls": "显示控件", + "name-time": "时间", + "name-unique-labels": "唯一标签", + "name-wrap-lines": "多行显示", "order-options": { - "label-newest-first": "", - "label-oldest-first": "" + "label-newest-first": "最新靠前", + "label-oldest-first": "最旧靠前" }, "out-of-range-message": { "end-of-the-selected-time-range": "所选时间范围的结束。" @@ -8505,6 +8933,7 @@ "un-themed-log-details": { "aria-label-data-links": "数据关联", "aria-label-fields": "字段", + "aria-label-line": "日志行", "aria-label-log-level": "日志级别", "aria-label-no-details": "没有详情" }, @@ -8525,27 +8954,27 @@ } }, "logs-new": { - "category-logs": "", + "category-logs": "日志", "deduplication-options": { - "label-exact": "", - "label-none": "", - "label-numbers": "", - "label-signature": "" + "label-exact": "确切", + "label-none": "无", + "label-numbers": "数字", + "label-signature": "签名" }, - "description-infinite-scrolling": "", - "description-show-controls": "", - "description-syntax-highlighting": "", - "name-deduplication": "", - "name-enable-log-details": "", - "name-infinite-scrolling": "", - "name-order": "", - "name-show-controls": "", - "name-syntax-highlighting": "", - "name-time": "", - "name-wrap-lines": "", + "description-infinite-scrolling": "实验性。通过滚动到日志列表底部来请求更多结果。", + "description-show-controls": "显示控件以跳转到最后一行或第一行日志,并按日志级别筛选", + "description-syntax-highlighting": "使用预定义的语法着色规则来突出显示日志行的相关部分", + "name-deduplication": "去重", + "name-enable-log-details": "启用日志详情", + "name-infinite-scrolling": "启用无限滚动", + "name-order": "顺序", + "name-show-controls": "显示控件", + "name-syntax-highlighting": "启用语法高亮", + "name-time": "时间", + "name-wrap-lines": "多行显示", "order-options": { - "label-newest-first": "", - "label-oldest-first": "" + "label-newest-first": "最新靠前", + "label-oldest-first": "最旧靠前" } }, "manage-dashbaords": { @@ -8572,6 +9001,8 @@ "updated-on": "更新日期" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "删除" }, "unthemed-dashboard-import": { @@ -8581,8 +9012,17 @@ "text": { "import-dashboard": "导入仪表板" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "此工具可以将某些资源从此安装迁移到您的云堆栈。要开始使用,您需要创建此安装的快照。创建快照通常需要不到两分钟的时间。快照与此 Grafana 安装一起存储。", @@ -8731,6 +9171,8 @@ "resource-details": { "dismiss-button": "好", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "数据面板已由 Grafana 在云实例中配置和管理。我们建议您今后使用已配置的数据面板。如果您仍然希望将数据面板复制到云实例,请更改数据面板 JSON 中的数据面板 ID,保存新的快照并再次上传。", "datasource-already-managed": "数据源已由 Grafana 在云实例中配置和管理。如果这是其他资源,请设置另一个 UID,然后重试。", "datasource-invalid-url": "有一个数据源的 URL 无效。请提供有效的 URL,然后重试。", @@ -9230,12 +9672,12 @@ "rss-button": "最新博客" }, "news": { - "category-news": "", - "description-show-image": "", - "description-url": "", + "category-news": "新闻", + "description-show-image": "控制新闻项目社交 (og:image) 图片是否显示在文本内容上方", + "description-url": "支持 RSS 和 Atom 源", "link-title": "前往 Grafana Labs 博客", - "name-show-image": "", - "name-url": "", + "name-show-image": "显示图片", + "name-url": "URL", "news-panel": { "body-error-loading-rss-feed": "请确保订阅源网址正确无误,并且在服务器上正确配置了 CORS。请参阅<2>新闻面板文档。", "loading": "加载中...", @@ -9244,27 +9686,27 @@ "title": "最新博客" }, "node-graph": { - "category-edges": "", - "category-node-graph": "", - "category-nodes": "", + "category-edges": "Edge", + "category-node-graph": "节点图", + "category-nodes": "节点", "layout-algorithm-options": { - "description-force": "", - "description-grid": "", - "description-layered": "", - "label-force": "", - "label-grid": "", - "label-layered": "" + "description-force": "使用力导向布局", + "description-grid": "使用网格布局", + "description-layered": "使用分层布局", + "label-force": "力", + "label-grid": "网格", + "label-layered": "分层" }, - "name-arc-sections": "", - "name-layout-algorithm": "", - "name-main-stat-unit": "", - "name-secondary-stat-unit": "", - "name-zoom-mode": "", + "name-arc-sections": "弧形部分", + "name-layout-algorithm": "布局算法", + "name-main-stat-unit": "主要统计单位", + "name-secondary-stat-unit": "次要统计单位", + "name-zoom-mode": "缩放模式", "zoom-mode-options": { - "description-cooperative": "", - "description-greedy": "", - "label-cooperative": "", - "label-greedy": "" + "description-cooperative": "让您正常滚动页面", + "description-greedy": "对所有缩放手势做出反应", + "label-cooperative": "配合", + "label-greedy": "贪心" } }, "nodeGraph": { @@ -9340,40 +9782,40 @@ }, "registry": { "get-all-option-editors": { - "description-time-zone": "", - "name-time-zone": "" + "description-time-zone": "时区选择", + "name-time-zone": "时区" }, "standard-field-condigs": { - "category-data-links": "" + "category-data-links": "数据关联和操作" }, "standard-field-configs": { - "category": "", - "category-thresholds": "", - "category-value-mappings": "", - "description-display-name": "", - "description-field-min-max": "", - "description-max": "", - "description-min": "", - "description-no-value": "", - "description-value-mappings": "", - "name-actions": "", - "name-ad-hoc": "", - "name-color-scheme": "", - "name-data-links": "", - "name-decimals": "", - "name-display-name": "", - "name-field-min-max": "", - "name-max": "", - "name-min": "", - "name-no-value": "", - "name-thresholds": "", - "name-unit": "", - "name-value-mappings": "", - "placeholder-decimals": "", - "placeholder-display-name": "", - "placeholder-max": "", - "placeholder-min": "", - "placeholder-unit": "" + "category": "标准选项", + "category-thresholds": "阈值", + "category-value-mappings": "值映射", + "description-display-name": "更改字段或序列名称", + "description-field-min-max": "按字段计算最小值和最大值", + "description-max": "留空以根据所有值计算", + "description-min": "留空以根据所有值计算", + "description-no-value": "没有值时要显示的内容", + "description-value-mappings": "根据输入值修改显示文字", + "name-actions": "操作", + "name-ad-hoc": "临时可筛选", + "name-color-scheme": "配色方案", + "name-data-links": "数据关联", + "name-decimals": "小数点位数", + "name-display-name": "显示名称", + "name-field-min-max": "字段最小/最大值", + "name-max": "最大", + "name-min": "最小", + "name-no-value": "无值", + "name-thresholds": "阈值", + "name-unit": "单位", + "name-value-mappings": "值映射", + "placeholder-decimals": "自动", + "placeholder-display-name": "无", + "placeholder-max": "自动", + "placeholder-min": "自动", + "placeholder-unit": "无" } }, "units": { @@ -9418,6 +9860,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "选择组织" }, "page": { @@ -9481,13 +9924,13 @@ }, "panel-data-error-view": { "missing-value": { - "number": "", - "string": "", - "time": "", - "unknown": "" + "number": "数据缺少一个数字字段", + "string": "数据缺少一个字符串字段", + "time": "数据缺少一个时间字段", + "unknown": "无法可视化数据" }, "no-value": { - "default": "" + "default": "无数据" }, "open-visualization-suggestions": "打开可视化建议", "switch-to-table": "切换至表格" @@ -9527,25 +9970,25 @@ "select-placeholder": "按类型筛选" }, "piechart": { - "category-legend": "", - "category-pie-chart": "", - "description-labels": "", - "description-pie-chart-type": "", + "category-legend": "图例", + "category-pie-chart": "饼图", + "description-labels": "选择要在饼图中显示的标签", + "description-pie-chart-type": "饼图的渲染方式", "labels-options": { - "label-name": "", - "label-percent": "", - "label-value": "" + "label-name": "名称", + "label-percent": "百分比", + "label-value": "值" }, "legend-values-options": { - "label-percent": "", - "label-value": "" + "label-percent": "百分比", + "label-value": "值" }, - "name-labels": "", - "name-legend-values": "", - "name-pie-chart-type": "", + "name-labels": "标签", + "name-legend-values": "图例值", + "name-pie-chart-type": "饼图类型", "pie-chart-type-options": { - "label-donut": "", - "label-pie": "" + "label-donut": "圆环图", + "label-pie": "饼图" } }, "playlist": { @@ -9640,6 +10083,7 @@ "permission": "您无权查看此页面。", "title-access-denied": "访问被拒绝" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "找不到根应用页面组件" }, "browse": { @@ -9683,8 +10127,7 @@ "update-status-text": "插件已更新" }, "versions": { - "confirmation-text-1": "您确定要降级版本吗", - "confirmation-text-2": "您通常不应该这样做", + "confirmation-text": "", "downgrade-confirm": "降级", "downgrade-title": "降级插件版本" } @@ -9738,6 +10181,10 @@ "empty-state": { "message": "找不到插件" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "好" @@ -9771,11 +10218,15 @@ "updating": "正在更新" }, "install-controls-button": { - "title-uninstall-modal": "卸载 {{plugin}}" + "title-uninstall-modal": "卸载 {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "此插件未发布到 <2>grafana.com/plugins,无法通过目录管理。", - "enterprise-plugin-info": "", + "enterprise-plugin-info": "此插件仅在 Grafana Cloud 和 Grafana Enterprise 中可用。", "learn-more": "了解更多", "no-valid-grafana-enterprise-license-detected": "未检测到有效的 Grafana 企业版许可证。", "title-dev-alert": "这是插件的开发版本,无法卸载。", @@ -9933,15 +10384,24 @@ }, "provisioned-resource-form": { "save-or-delete-resource-shared-fields": { - "comment-placeholder-describe-changes-optional": "", - "description-branch-name-in-git-hub": "", + "comment-placeholder-describe-changes-optional": "添加备注以描述您的更改(可选)", + "description-branch-name-in-git-hub": "GitHub 中的分支名称", "description-inside-repository": "", - "label-branch": "", - "label-comment": "", - "label-path": "", - "label-workflow": "" + "label-branch": "分支", + "label-comment": "评论", + "label-path": "路径", + "label-workflow": "工作流程" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "此功能目前正在积极开发中。为了获得最佳体验和最新改进,我们建议您使用 Grafana 的<2>夜间版本。" @@ -10802,6 +11262,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "服务帐户选取器", "select-placeholder": "开始键入以搜索服务账户" }, @@ -10847,6 +11308,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "添加服务账户令牌", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "删除服务账户", "disable-service-account": "禁用服务账户", "enable-service-account": "启用服务账户", @@ -10873,6 +11338,7 @@ "used-by": "使用方" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "编辑" }, "service-account-role-row": { @@ -10886,10 +11352,15 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "添加服务账户", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "按名称搜索服务账户", "sub-title": "服务账户及其令牌可用于对 Grafana API 进行身份验证。不妨在我们的<2>文档中了解更多信息。", "title-delete-service-account": "删除服务账户", - "title-disable-service-account": "禁用服务账户" + "title-disable-service-account": "禁用服务账户", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "此令牌已过期", @@ -10903,6 +11374,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "导出为 JSON", "share-externally-title": "在外部共享", "share-internally-title": "在内部共享", @@ -10938,6 +11410,19 @@ "view-button": "查看 JSON", "view-button-yaml": "查看 YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "创建库面板。" }, @@ -10946,7 +11431,7 @@ "info-text": "创建指向此仪表板或面板的直接链接,通过以下选项自定义。", "link-url": "链接网址", "render-alert": "未安装图像渲染器插件", - "render-instructions": "要渲染面板图像,您必须安装 <2>Grafana 图像渲染器插件。请与您的 Grafana 管理员联系以安装插件。", + "render-instructions": "", "rendered-image": "直接链接至渲染后的图像", "save-alert": "仪表板未保存", "save-dashboard": "要渲染面板图像,需要先保存仪表板。", @@ -11014,6 +11499,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "面板预览" }, @@ -11173,112 +11664,112 @@ }, "stat": { "add-orientation-option": { - "description-orientation": "", - "name-orientation": "", + "description-orientation": "布局朝向", + "name-orientation": "朝向", "orientation-options": { - "label-auto": "", - "label-horizontal": "", - "label-vertical": "" + "label-auto": "自动", + "label-horizontal": "水平", + "label-vertical": "垂直" } }, "add-standard-data-reduce-options": { - "category-value-options": "", - "description-calculation": "", - "description-fields": "", - "description-limit": "", - "description-show": "", + "category-value-options": "值选项", + "description-calculation": "选择一个归约器函数/计算", + "description-fields": "选择应包含在面板中的字段", + "description-limit": "可显示的最大行数", + "description-show": "计算每列或序列的单个值或显示每一行", "fields-options": { - "label-all-fields": "", - "label-numeric-fields": "" + "label-all-fields": "所有字段", + "label-numeric-fields": "数字字段" }, - "name-calculation": "", - "name-fields": "", - "name-limit": "", - "name-show": "", + "name-calculation": "计算", + "name-fields": "字段", + "name-limit": "限制", + "name-show": "显示", "show-options": { - "label-all-values": "", - "label-calculate": "" + "label-all-values": "所有值", + "label-calculate": "计算" } }, - "category-stat-styles": "", + "category-stat-styles": "统计样式", "color-mode-options": { - "label-background-gradient": "", - "label-background-solid": "", - "label-none": "", - "label-value": "" + "label-background-gradient": "背景渐变", + "label-background-solid": "背景纯色", + "label-none": "无", + "label-value": "值" }, - "description-graph-mode": "", - "description-text-mode": "", + "description-graph-mode": "统计面板图形/火花线模式", + "description-text-mode": "控制是显示名称和值还是仅显示名称", "graph-mode": { "options": { - "label-area": "", - "label-none": "" + "label-area": "面积", + "label-none": "无" } }, "name-color-modcolor-mode-options": { - "label": "" + "label": "颜色模式" }, - "name-graph-mode": "", - "name-show-percent-change": "", - "name-text-alignment": "", - "name-text-mode": "", - "name-wide-layout": "", - "percent-change-color-mode": "", + "name-graph-mode": "图形模式", + "name-show-percent-change": "显示百分比变化", + "name-text-alignment": "文字对齐", + "name-text-mode": "文字模式", + "name-wide-layout": "宽布局", + "percent-change-color-mode": "百分比变化颜色模式", "percent-change-color-mode-options": { - "label-inverted": "", - "label-same-as-value": "", - "label-standard": "" + "label-inverted": "反转", + "label-same-as-value": "与值相同", + "label-standard": "标准" }, "text-alignment-options": { - "label-auto": "", - "label-center": "" + "label-auto": "自动", + "label-center": "中心" }, "text-mode-options": { - "label-auto": "", - "label-name": "", - "label-none": "", - "label-value": "", - "label-value-and-name": "" + "label-auto": "自动", + "label-name": "名称", + "label-none": "无", + "label-value": "值", + "label-value-and-name": "值和名称" }, "wide-layout-options": { - "label-off": "", - "label-on": "" + "label-off": "关", + "label-on": "开" } }, "state-timeline": { "align-values-options": { - "label-center": "", - "label-left": "", - "label-right": "" + "label-center": "中心", + "label-left": "左侧", + "label-right": "右侧" }, - "category-state-timeline": "", - "name-align-values": "", - "name-connect-null-values": "", - "name-disconnect-values": "", - "name-fill-opacity": "", - "name-line-width": "", - "name-merge-equal-consecutive-values": "", - "name-page-size": "", - "name-row-height": "", - "name-show-values": "", + "category-state-timeline": "状态时间线", + "name-align-values": "对齐值", + "name-connect-null-values": "连接空值", + "name-disconnect-values": "断开值", + "name-fill-opacity": "填充不透明度", + "name-line-width": "线宽度", + "name-merge-equal-consecutive-values": "合并相等的连续值", + "name-page-size": "页面大小(启用分页)", + "name-row-height": "行高", + "name-show-values": "显示值", "show-values-options": { - "label-always": "", - "label-auto": "", - "label-never": "" + "label-always": "始终", + "label-auto": "自动", + "label-never": "从不" } }, "status-history": { - "category-status-history": "", - "name-column-width": "", - "name-fill-opacity": "", - "name-line-width": "", - "name-page-size": "", - "name-row-height": "", - "name-show-values": "", + "category-status-history": "状态历史记录", + "name-column-width": "列宽度", + "name-fill-opacity": "填充不透明度", + "name-line-width": "线宽度", + "name-page-size": "页面大小(启用分页)", + "name-row-height": "行高", + "name-show-values": "显示值", "show-values-options": { - "label-always": "", - "label-auto": "", - "label-never": "" + "label-always": "始终", + "label-auto": "自动", + "label-never": "从不" }, "status-history-panel": { "too-many-points_other": "数据点过多,无法正常可视化。<1>请更新查询以返回更少的数据点。<3>(已接收 {{count}} 个数据点)" @@ -11317,13 +11808,13 @@ "label-gauge-display-mode": "仪表显示模式", "label-value-display": "值显示" }, - "category-cell-options": "", - "category-table": "", - "category-table-footer": "", + "category-cell-options": "单元格选项", + "category-table": "表格", + "category-table-footer": "表格页脚", "cell-height-options": { - "label-large": "", - "label-medium": "", - "label-small": "" + "label-large": "大", + "label-medium": "中", + "label-small": "小" }, "color-background-cell-options-editor": { "description-apply-to-entire-row": "如果选中,整行将按此单元格的颜色显示。", @@ -11337,82 +11828,84 @@ "wrap-text": "文本换行" }, "column-alignment-options": { - "label-auto": "", - "label-center": "", - "label-left": "", - "label-right": "" + "label-auto": "自动", + "label-center": "中心", + "label-left": "左侧", + "label-right": "右侧" }, "container": { "content": "在单个表中显示太多列可能会影响性能,并使数据难以阅读。请考虑优化您的查询。", "show-all-series": "显示所有列", "show-only-series": "仅显示 {{MAX_NUMBER_OF_COLUMNS}} 列" }, - "description-calculation": "", - "description-cell-value-inspect": "", - "description-column-filter": "", - "description-count-rows": "", - "description-fields": "", - "description-min-column-width": "", + "description-calculation": "选择一个归约器函数/计算", + "description-cell-value-inspect": "在模态窗口中启用单元格值检查", + "description-column-filter": "启用/禁用表格中的字段筛选条件", + "description-count-rows": "显示所有数据行的单个计数", + "description-fields": "选择应计算的字段", + "description-min-column-width": "列自动调整大小的最小宽度", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "在图片无法显示或用户使用屏幕阅读器时,将显示的替代文本", "description-title-text": "光标悬停在图片上时将显示的文本", "label-alt-text": "替代文本", "label-title-text": "标题文本" }, - "name-calculation": "", - "name-cell-height": "", - "name-cell-type": "", - "name-cell-value-inspect": "", - "name-column-alignment": "", - "name-column-filter": "", - "name-column-width": "", - "name-count-rows": "", - "name-enable-paginations": "", - "name-fields": "", - "name-hide-in-table": "", - "name-min-column-width": "", - "name-show-table-footer": "", - "name-show-table-header": "", - "placeholder-column-width": "", - "placeholder-fields": "" + "name-calculation": "计算", + "name-cell-height": "单元格高度", + "name-cell-type": "单元格类型", + "name-cell-value-inspect": "单元格值检查", + "name-column-alignment": "列对齐", + "name-column-filter": "列筛选", + "name-column-width": "列宽度", + "name-count-rows": "计算行", + "name-enable-paginations": "启用分页", + "name-fields": "字段", + "name-hide-in-table": "在表格中隐藏", + "name-min-column-width": "最小列宽", + "name-show-table-footer": "显示表格页脚", + "name-show-table-header": "显示表格表头", + "name-wrap-header-text": "表头文字换行", + "placeholder-column-width": "自动", + "placeholder-fields": "所有数字字段" }, "table-new": { - "category-cell-options": "", - "category-table": "", - "category-table-footer": "", + "category-cell-options": "单元格选项", + "category-table": "表格", + "category-table-footer": "表格页脚", "cell-height-options": { - "label-large": "", - "label-medium": "", - "label-small": "" + "label-large": "大", + "label-medium": "中", + "label-small": "小" }, "column-alignment-options": { - "label-auto": "", - "label-center": "", - "label-left": "", - "label-right": "" + "label-auto": "自动", + "label-center": "中心", + "label-left": "左侧", + "label-right": "右侧" }, - "description-calculation": "", - "description-cell-value-inspect": "", - "description-column-filter": "", - "description-count-rows": "", - "description-fields": "", - "description-min-column-width": "", - "name-calculation": "", - "name-cell-height": "", - "name-cell-type": "", - "name-cell-value-inspect": "", - "name-column-alignment": "", - "name-column-filter": "", - "name-column-width": "", - "name-count-rows": "", - "name-enable-pagination": "", - "name-fields": "", - "name-hide-in-table": "", - "name-min-column-width": "", - "name-show-table-footer": "", - "name-show-table-header": "", - "placeholder-column-width": "", - "placeholder-fields": "" + "description-calculation": "选择一个归约器函数/计算", + "description-cell-value-inspect": "在模态窗口中启用单元格值检查", + "description-column-filter": "启用/禁用表格中的字段筛选条件", + "description-count-rows": "显示所有数据行的单个计数", + "description-fields": "选择应计算的字段", + "description-min-column-width": "列自动调整大小的最小宽度", + "name-calculation": "计算", + "name-cell-height": "单元格高度", + "name-cell-type": "单元格类型", + "name-cell-value-inspect": "单元格值检查", + "name-column-alignment": "列对齐", + "name-column-filter": "列筛选", + "name-column-width": "列宽度", + "name-count-rows": "计算行", + "name-enable-pagination": "启用分页", + "name-fields": "字段", + "name-hide-in-table": "在表格中隐藏", + "name-min-column-width": "最小列宽", + "name-show-table-footer": "显示表格页脚", + "name-show-table-header": "显示表格表头", + "placeholder-column-width": "自动", + "placeholder-fields": "所有数字字段" }, "tag-filter": { "clear-button": "清除标记", @@ -11423,6 +11916,7 @@ "tag-option-label": "标记选项" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "团队选择器", "select-placeholder": "选择一个团队" }, @@ -11496,17 +11990,17 @@ } }, "text": { - "category-text": "", + "category-text": "文字", "mode-options": { - "label-code": "", - "label-html": "", - "label-markdown": "" + "label-code": "代码", + "label-html": "HTML", + "label-markdown": "Markdown" }, - "name-content": "", - "name-language": "", - "name-mode": "", - "name-show-line-numbers": "", - "name-show-mini-map": "" + "name-content": "内容", + "name-language": "语言", + "name-mode": "模式", + "name-show-line-numbers": "显示行号", + "name-show-mini-map": "显示小地图" }, "theme-preview": { "breadcrumbs": { @@ -11554,7 +12048,7 @@ "apply-button": "应用时间范围", "default-error": "请输入过去的日期或“{{now}}”", "fiscal-year": "财政年度:{{from}} - {{to}}", - "from-input": "", + "from-input": "来自", "open-input-calendar": "打开日历", "range-error": "“发件人”不能在“收件人”之后", "to-input": "结束" @@ -11585,8 +12079,8 @@ }, "timeline": { "missing-field": { - "all": "", - "time": "" + "all": "无可绘制字段", + "time": "数据缺少时间字段" } }, "timeseries": { @@ -11604,54 +12098,54 @@ "tooltip-delete": "删除", "tooltip-edit": "编辑" }, - "category-axis": "", + "category-axis": "轴", "config": { "get-graph-field-config": { - "aria-label-bar-width-factor": "", - "aria-label-fill-opacity": "", - "aria-label-line-width": "", - "aria-label-point-size": "", - "category-graph-styles": "", - "category-thresholds": "", - "name-bar-alignment": "", - "name-bar-width-factor": "", - "name-connect-nulls": "", - "name-disconnect-values": "", - "name-fill-below-to": "", - "name-fill-opacity": "", - "name-gradient-mode": "", - "name-line-interpolation": "", - "name-line-style": "", - "name-line-width": "", - "name-point-size": "", - "name-show-points": "", - "name-show-thresholds": "", - "name-style": "", - "name-transform": "", + "aria-label-bar-width-factor": "条宽度系数", + "aria-label-fill-opacity": "填充不透明度", + "aria-label-line-width": "线宽度", + "aria-label-point-size": "点大小", + "category-graph-styles": "图表样式", + "category-thresholds": "阈值", + "name-bar-alignment": "条对齐", + "name-bar-width-factor": "条宽度系数", + "name-connect-nulls": "连接空值", + "name-disconnect-values": "断开值", + "name-fill-below-to": "将以下内容填充至", + "name-fill-opacity": "填充不透明度", + "name-gradient-mode": "渐变模式", + "name-line-interpolation": "线插值", + "name-line-style": "线样式", + "name-line-width": "线宽度", + "name-point-size": "点大小", + "name-show-points": "显示点", + "name-show-thresholds": "显示阈值", + "name-style": "样式", + "name-transform": "转换", "transform-options": { - "description-constant": "", - "description-style": "", - "label-constant": "", - "label-style": "" + "description-constant": "第一个值将显示为一条固定线", + "description-style": "将结果翻转为 y 轴上的负值", + "label-constant": "固定", + "label-style": "负 Y" } } }, "insert-nulls-editor": { "disconnect-options": { - "label-never": "", - "label-threshold": "" + "label-never": "从不", + "label-threshold": "阈值" } }, "line-style-editor": { "line-fill-options": { - "label-dash": "", - "label-dots": "", - "label-solid": "" + "label-dash": "控制板", + "label-dots": "点", + "label-solid": "纯色" }, "title-the-input-expects-a-segment-list": "输入应为一个线段列表", "tooltip-help": "帮助" }, - "name-time-zone": "", + "name-time-zone": "时区", "nulls-threshold-input": { "placeholder-never": "从不" }, @@ -11661,9 +12155,9 @@ }, "span-nulls-editor": { "gaps-options": { - "label-always": "", - "label-never": "", - "label-threshold": "" + "label-always": "始终", + "label-never": "从不", + "label-threshold": "阈值" } }, "timezones-editor": { @@ -11672,17 +12166,17 @@ } }, "traces": { - "category-span-filters": "", - "name-critical-path-only": "", - "name-find-in-trace": "", - "name-max-duration": "", - "name-min-duration": "", - "name-service-name": "", - "name-service-name-operator": "", - "name-show-matches-only": "", - "name-span-name": "", - "name-span-name-operator": "", - "name-tags": "", + "category-span-filters": "跨度筛选条件", + "name-critical-path-only": "仅显示关键路径", + "name-find-in-trace": "在追踪中查找", + "name-max-duration": "最大持续时间", + "name-min-duration": "最小持续时间", + "name-service-name": "服务名称", + "name-service-name-operator": "服务名称操作符", + "name-show-matches-only": "仅显示匹配项", + "name-span-name": "跨度名称", + "name-span-name-operator": "跨度名称操作符", + "name-tags": "标记", "traces-panel": { "no-data-found-in-response": "回复中未找到数据" } @@ -11697,20 +12191,20 @@ "axis-editor": { "log-mode-options": { "description": { - "split-the-buckets-based-on-size": "" + "split-the-buckets-based-on-size": "根据大小拆分桶" }, "label": { - "split": "" + "split": "拆分" } }, "mode-options": { "description": { - "split-the-buckets-based-on-count": "", - "split-the-buckets-based-on-size": "" + "split-the-buckets-based-on-count": "根据数量拆分桶", + "split-the-buckets-based-on-size": "根据大小拆分桶" }, "label": { - "count": "", - "size": "" + "count": "数量", + "size": "大小" } }, "placeholder-auto": "自动" @@ -11724,20 +12218,39 @@ "placeholder-fields-or-number": "字段或数字" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "别名", "label-mode": "模式", "label-replace-all-fields": "替换所有字段" }, "calculate-heatmap": { "add-heatmap-calculation-options": { - "name-x-bucket": "", - "name-y-bucket": "", - "name-y-bucket-scale": "" + "name-x-bucket": "X 桶", + "name-y-bucket": "Y 桶", + "name-y-bucket-scale": "Y 桶比例" } }, "concatenate-transformer-editor": { "label-label": "标签", "label-name": "名称", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "帧" }, "config-from-query-transformer-editor": { @@ -11748,6 +12261,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "添加转换字段类型转换器", "aria-label-remove-convert-field-type-transformer": "移除转换字段类型转换器", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "浏览器", "utc": "UTC" @@ -11758,7 +12275,13 @@ "label-input-format": "输入格式", "label-join-with": "结合对象", "label-set-timezone": "设置时区", + "name": { + "convert-field-type": "" + }, "placeholder-type": "类型", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "合并数组值时使用显式分隔符", "tooltip-input-format": "指定输入字段的格式,以便 Grafana 可以正确解析日期字符串。", "tooltip-specify-the-output-format": "指定输出格式。", @@ -11776,10 +12299,23 @@ "tooltip-enable": "启用" }, "draggable-sort-order": { - "asc": "", - "desc": "", + "asc": "升序", + "desc": "降序", "off": "关" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "添加枚举值", "generate-enum-values-from-data": "从数据生成枚举值" @@ -11790,11 +12326,22 @@ "remove-enum-row-tooltip-delete": "删除" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "分隔符", "label-format": "格式", "label-keep-time": "保持时间", "label-replace-all-fields": "替换所有字段", "label-source": "源", + "name": { + "extract-fields": "" + }, "placeholder-auto": "自动", "placeholder-select-delimiter": "选择分隔符...", "tooltip-regexp": "示例:{{regexExample}}" @@ -11803,8 +12350,22 @@ "aria-label-threshold-color": "阈值颜色" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "字段", - "label-lookup": "查阅" + "label-lookup": "查阅", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "附加设置", @@ -11813,12 +12374,26 @@ "use-as": "使用方式" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "从变量", "label-identifier": "标识符", "label-use-variable": "使用变量", "label-variable": "变量", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "正则表达式模式" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "字段", "label-match": "匹配", @@ -11828,14 +12403,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "添加条件", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "条件", - "label-filter-type": "筛选器类型" + "label-filter-type": "筛选器类型", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "字段", "label-format": "格式", "label-substring-range": "子字符串范围" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "格式", "label-set-timezone": "设置时区", @@ -11843,6 +12452,70 @@ "tooltip-format": "指定为 <2>Moment.js 格式字符串的字段的输出格式。", "tooltip-timezone-manually": "手动设置日期的时区" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "浏览器", @@ -11870,22 +12543,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "已忽略", "placeholder-select-stats": "选择统计" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "如果没有对字段进行分组,则计算不会产生效果" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "如果启用,嵌套表会将字段名称显示为表头", "label-show-field-names-in-nested-tables": "在嵌套表中显示字段名称", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "如果没有对字段进行分组,则计算不会产生效果。" }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "单元格值", "label-column": "列", "label-empty-value": "空值", "label-row": "行", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "布尔值假值", @@ -11904,6 +12601,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "自动", "placeholder-default": "默认:30", "placeholder-none": "无" @@ -11912,7 +12615,16 @@ "label-as-percentile": "作为百分位数", "tooltip-transform-index-percentile": "将行索引转换为百分位数。" }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "输入不包含任何标签", "no-input-or-labels-found": "找不到输入(或标签)", @@ -11934,10 +12646,30 @@ "label-labels": "标签", "label-mode": "模式", "label-value-field-name": "值字段名称", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(可选)选择标签", "tooltip-replace-value-field-label": "将值字段名称替换为标签" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "值或变量" }, "long-info": { @@ -11948,7 +12680,13 @@ "string-values-separate": "字符串值以单独的字段表示,而不是标签" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "合并功能在单帧上应用时没有效果。" + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "合并功能在单帧上应用时没有效果。", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "所有值都是数字", @@ -11958,10 +12696,16 @@ "time-in-ascending-order": "时间按升序排列" }, "organize-fields-transformer-editor": { - "field-order": "", + "description": { + "reorder-hide-or-rename-fields": "" + }, + "field-order": "字段顺序", "field-order-auto": "自动", - "field-order-manual": "", - "first-frame-warning": "组织字段仅适用于单个帧。请考虑先应用连接转换或筛选输入。" + "field-order-manual": "手动", + "first-frame-warning": "组织字段仅适用于单个帧。请考虑先应用连接转换或筛选输入。", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "设置所选字段名称的显示方式。因为帧名称通常更适合表格数据" @@ -11986,9 +12730,41 @@ "partition-values-works-single-frame": "按值分区仅适用于单个帧。", "tooltip-keeps-partition-fields-frames": "在帧中保留分区字段" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "格式", - "label-info": "信息" + "label-info": "信息", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "与", @@ -12000,6 +12776,9 @@ "label-operation": "运算" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "计算", "label-include-time": "包含时间", "label-labels-to-fields": "标签到字段", @@ -12014,12 +12793,23 @@ "series-to-rows": "序列到行" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "选择统计" }, "regex-matcher-editor": { "placeholder-value-or-variable": "值或变量", "variable-regex-documentation": "将多值变量格式化为正则表达式字符串" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "三次型", @@ -12038,9 +12828,17 @@ "polynomial": "多项式" } }, - "regression": "", + "regression": "回归", "tooltip-number-of-xy-points-to-predict": "要预测的 X,Y 点的数量" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "匹配", "label-replace": "替换", @@ -12049,11 +12847,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "字段", - "label-mode": "模式" + "label-mode": "模式", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "字段", "label-reverse": "反转", + "name": { + "sort-by": "" + }, "placeholder-select-field": "选择字段" }, "supplier": { @@ -12073,6 +12897,17 @@ "transform": "转换" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "统计", "label-time-field": "时间字段", @@ -12086,10 +12921,34 @@ "placeholder-value": "值", "tooltip-name-for-value-fields": "值字段的名称" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "运算", "placeholder-field": "字段" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "任何类型的多个值字段", "single-frame": "单个帧", @@ -12122,10 +12981,10 @@ } }, "trend": { - "category-x-axis": "", - "description-x-field": "", - "name-x-field": "", - "placeholder-x-field": "" + "category-x-axis": "X 轴", + "description-x-field": "递增的数值", + "name-x-field": "X 字段", + "placeholder-x-field": "第一个数值" }, "upgrade-box": { "discovery-text": "您发现了一项专业功能!", @@ -12142,6 +13001,7 @@ "title": "组织" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "用户选取器", "select-placeholder": "开始键入以搜索用户" }, @@ -12227,6 +13087,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "删除变量" }, "create-ad-hoc-variable-adapter": { @@ -12275,9 +13137,34 @@ "label-refresh": "刷新" }, "query-variable-sort-select": { - "description-values-variable": "如何对此变量的值进行排序" + "description-values-variable": "如何对此变量的值进行排序", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "默认值(如有)", "text-options": "文本选项" }, @@ -12306,6 +13193,8 @@ "description-optional-display-name": "可选显示名称", "description-template-variable-characters": "模板变量的名称。(最多 50 个字符)", "general": "概况", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "描述性文本", "placeholder-label-name": "标签名称", "placeholder-variable-name": "变量名称", @@ -12320,9 +13209,15 @@ "tooltip-duplicate-variable": "复制变量", "tooltip-remove-variable": "移除变量" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "切换所有值" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "显示使用情况" }, @@ -12352,20 +13247,20 @@ } }, "xychart": { - "category-xychart": "", - "name-fill-opacity": "", - "name-line-style": "", - "name-line-width": "", - "name-max-point-size": "", - "name-min-point-size": "", - "name-point-shape": "", - "name-point-size": "", - "name-point-stroke-width": "", - "name-series-mapping": "", - "name-show": "", + "category-xychart": "XY 图表", + "name-fill-opacity": "填充不透明度", + "name-line-style": "线样式", + "name-line-width": "线宽度", + "name-max-point-size": "最大点大小", + "name-min-point-size": "最小点大小", + "name-point-shape": "点形状", + "name-point-size": "点大小", + "name-point-stroke-width": "点描边宽度", + "name-series-mapping": "序列映射", + "name-show": "显示", "point-shape-options": { - "label-circle": "", - "label-square": "" + "label-circle": "圆形", + "label-square": "方形" }, "series-editor": { "add-series": "添加序列", @@ -12377,18 +13272,18 @@ "label-y-field": "Y 字段", "placeholder-all-frames": "所有帧", "placeholder-select-frame": "选定帧", - "placeholder-x-field": "", - "placeholder-y-field": "", + "placeholder-x-field": "每个帧中的第一个数字或时间字段", + "placeholder-y-field": "每个帧中其余的数字字段", "tooltip-delete-series": "刪除序列" }, "series-mapping-options": { - "label-auto": "", - "label-manual": "" + "label-auto": "自动", + "label-manual": "手动" }, "show-options": { - "label-both": "", - "label-lines": "", - "label-points": "" + "label-both": "两个", + "label-lines": "线", + "label-points": "点" } } } \ No newline at end of file diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 826ce308fed..a664a5b9c64 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -84,6 +84,7 @@ "admin-feature-toggles-table": { "confirm-modal-body-1": "部分功能穩定 (GA) 且預設為啟用,有些功能目前則處於初步 Beta 階段,可供早期採用。", "confirm-modal-body-2": "建議您在進行修改前了解各項功能變更的影響。", + "confirmText-save-changes": "", "get-stage-cell": { "beta": "Beta", "content-general-availability": "一般可用性", @@ -96,14 +97,21 @@ }, "admin-orgs-table": { "aria-label-delete-org": "刪除組織", + "confirmText-delete": "", "title-delete": "刪除" }, "anon-users": { "not-found": "未找到匿名使用者。" }, "base-user-sessions": { + "body-force-logout-from-all-devices": "", + "confirmText-confirm-logout": "", + "confirmText-force-logout": "", "title-force-logout-from-all-devices": "強制從所有裝置登出" }, + "change-org-button": { + "confirmText-save": "" + }, "edit-org": { "access-denied": "您沒有權限查看此組織中的使用者。若要更新此組織,請聯絡您的伺服器管理員。", "heading": "編輯組織", @@ -111,10 +119,10 @@ "users-heading": "組織使用者" }, "enterprise-auth-features-card": { - "dismiss": "", - "heading": "", - "learn-more-link": "", - "text": "" + "dismiss": "關閉", + "heading": "企業版驗證", + "learn-more-link": "了解詳情", + "text": "使用 <1>SAML、<3>SCIM、<6>LDAP 和 <8>RBAC 自動管理使用者、團隊和權限 - 在 Grafana Cloud 和 Enterprise 中可以使用。" }, "feature-listing": { "title-auditing": "稽核", @@ -208,9 +216,11 @@ "not-editable": "無法編輯此使用者的角色,因為其角色來自您的驗證提供者同步設定。詳情請參閱 <1>Grafana 身分驗證文件。" }, "org-users-table": { + "body-delete": "", "columns": { "aria-label-role": "角色" }, + "confirmText-delete": "", "delete-aria-label": "刪除使用者:{{name}}", "title-delete": "刪除" }, @@ -241,6 +251,9 @@ "settings": { "info-description": "這些系統設定在 grafana.ini 或 custom.ini 中定義(或在 ENV 變數中覆寫)。若要變更這些設定,您目前需要重新啟動 Grafana。" }, + "un-themed-org-row": { + "confirmText-confirm-removal": "" + }, "upgrade-info": { "title": "企業版授權" }, @@ -293,12 +306,17 @@ }, "user-permissions": { "change-button": "變更", + "confirmText-change": "", "grafana-admin-key": "Grafana 管理員", "grafana-admin-no": "否", "grafana-admin-yes": "是", "title": "權限" }, "user-profile": { + "body-delete": "", + "body-disable": "", + "confirmText-delete-user": "", + "confirmText-disable-user": "", "delete-button": "刪除使用者", "disable-button": "停用使用者", "edit-button": "編輯", @@ -312,6 +330,9 @@ "title-delete-user": "刪除使用者", "title-disable-user": "停用使用者" }, + "user-profile-row": { + "confirmText-save": "" + }, "user-sessions": { "browser-column": "瀏覽器和作業系統", "force-logout-all-button": "強制從所有裝置登出", @@ -352,6 +373,10 @@ "active-timing-fields": { "am-active-timing-select-label-active-timings": "啟用時間" }, + "ad": { + "close": "關閉", + "learn-more": "了解詳情" + }, "add-button": { "add-more": "新增更多" }, @@ -370,12 +395,12 @@ "missing-series-resolve": "缺少要解析的系列評估", "no-annotations": "沒有註釋", "notification-configuration": { - "active-timings": "", + "active-timings": "啟用時間間隔", "contact-point": "聯絡點", "group-by": "分組依據", - "group-title": "", + "group-title": "通知設定", "mute-timings": "靜音時間", - "timing-options": "" + "timing-options": "時間" }, "pending-period": "待處理期間", "rule": "規則", @@ -453,6 +478,9 @@ "label-muting-grouping-and-timings-optional": "靜音、分組和時間(選填)", "title-muting-grouping-and-timings": "靜音、分組和時間" }, + "alert-manager-picker": { + "noOptionsMessage-no-datasources-found": "" + }, "alert-menu": { "copy-link": "複製網址", "duplicate": "重複", @@ -476,6 +504,9 @@ } } }, + "alert-rule": { + "term": "" + }, "alert-rule-form": { "action-buttons": { "edit-yaml": "編輯 YAML", @@ -542,6 +573,7 @@ "view-configuration": "檢視設定" }, "alertmanager-config": { + "confirmText-yes-reset-configuration": "", "gma-manual-configuration-description": "無法手動變更內部 Grafana Alertmanager 設定。若要變更此設定,請透過使用者介面編輯個別資源。", "gma-manual-configuration-is-not-supported": "不支援手動設定變更", "message": { @@ -556,11 +588,13 @@ "title-resetting-alertmanager-configuration": "正在重設 Alertmanager 設定" }, "alertmanager-configuration-version-manager": { + "body-restore-configuration-version-unsaved-changes": "", "columns": { "compare": "比較", "restore": "還原", "text-latest": "最新" }, + "confirmText-yes-restore-configuration": "", "loading": "正在載入…", "no-previous-configurations": "沒有之前的設定", "this-might-take-a-while": "這可能需要一段時間...", @@ -840,8 +874,10 @@ }, "contact-point-header": { "aria-label-more-actions": "聯絡點「{{contactPointName}}」的更多動作", + "ariaLabel-delete": "", "button-edit": "編輯", "button-view": "檢視", + "export-ariaLabel-export": "", "export-label-export": "匯出", "label-delete": "刪除", "label-manage-permissions": "管理權限", @@ -912,7 +948,7 @@ }, "contactPoints": { "validation": { - "notFound": "" + "notFound": "找不到聯絡點「{{contactPoint}}」" } }, "continue-matching-indicator": { @@ -921,7 +957,7 @@ "copy-to-clipboard": "將「{{label}}」複製到剪貼簿", "create-metadata": { "label": { - "contact-point": "", + "contact-point": "通知已傳送至", "dashboard": "儀表板", "dashboard-and-panel": "儀表板和面板", "evaluation-interval": "評估間隔", @@ -1069,7 +1105,7 @@ "content-show-all-errors": "顯示所有錯誤" }, "errors": { - "failedWith": "" + "failedWith": "{{-config}} 失敗,顯示{{status}}:{{-message}}" }, "evaluate-every-validation-options": { "message": { @@ -1374,6 +1410,7 @@ "label-disable-resolved-message": "停用已解決的訊息" }, "grafana-evaluation-behavior-step": { + "loadingMessage-loading-groups": "", "message": { "must-be-a-positive-integer": "必須為正整數。", "must-enter-a-group-name": "必須輸入群組名稱" @@ -1465,15 +1502,12 @@ "group-details": { "ds-features-error": "載入資料來源詳細資料時發生錯誤", "edit": "編輯", - "evaluations-to-fire": "待觸發的評估週期", "export": "匯出", "folder": "資料夾", "group-loading-error": "載入群組時發生錯誤", "interval": "間隔", "namespace": "命名空間", - "pending-period": "待處理期間", - "recording": "錄製", - "rule-name": "規則名稱" + "new": "" }, "group-edit": { "ds-error": "載入資料來源詳細資料時發生錯誤", @@ -1519,8 +1553,20 @@ } }, "home": { + "irm-card-description": "使用 Grafana Cloud IRM 統一隨叫隨到、警報和事件回應。", + "irm-card-item-1": "使用行事曆或 Terraform 管理隨叫隨到排程。", + "irm-card-item-2": "透過網頁、應用程式、Slack 或其他管道回應事件。", + "irm-card-item-3": "使用 AI 支援的 Grafana SIFT 找出根本原因。", + "irm-card-item-4": "分析過去的事件,以改善回應和復原力。", + "irm-card-title": "事件回應與管理", "label-get-started": "開始使用", "label-insights": "深入分析", + "synthetic-monitoring-card-description": "從全球位置在外部監控關鍵使用者流程、網站和 API。", + "synthetic-monitoring-card-item-1": "透過瀏覽器檢查模擬端到端使用者歷程。", + "synthetic-monitoring-card-item-2": "在每個網路層執行 ping、DNS、HTTP/S 和 TCP 檢查。", + "synthetic-monitoring-card-item-3": "在防火牆後使用 20 多個全球探測器或私人探測器。", + "synthetic-monitoring-card-item-4": "使用內建的 Prometheus 風格警報追蹤 SLO - 直接從使用者介面操作。", + "synthetic-monitoring-card-title": "綜合監控", "title-alerting": "警報" }, "import-to-gma": { @@ -1535,7 +1581,7 @@ "no-rules-body-yaml": "尚無要匯入的規則。請選擇其他 yaml 檔案。", "no-rules-title": "沒有要匯入的規則", "not-using-rules-managed-by-integrations-or-plugins": { - "text": "", + "text": "不應將整合或外掛程式管理的規則匯入 Grafana 管理的規則。", "title": "資訊" }, "plugin-rules-warning": { @@ -1819,7 +1865,11 @@ "other-data-sources": "其他資料來源" } } - } + }, + "noOptionsMessage-no-datasources-found": "" + }, + "mute-timing-actions-button": { + "body-delete-mute-timing": "" }, "mute-timing-actions-buttons": { "text-disabled": "已停用", @@ -1910,7 +1960,7 @@ }, "notification-policies-filter": { "label-search-by-contact-point": "按聯絡點搜尋", - "placeholder-search-by-contact-point": "", + "placeholder-search-by-contact-point": "選擇聯絡點", "search-query-input-placeholder-search": "搜尋" }, "notification-policies-list": { @@ -2069,9 +2119,11 @@ "update-errors": { "conflict": "通知政策樹已由其他使用者更新。", "error-code": "錯誤訊息:「{{error}}」", - "fallback": "更新通知政策時發生錯誤。", + "routes": { + "conflictingMatchers": "" + }, "suffix": "請重新整理此頁面並再試一次。", - "title": "儲存通知政策時發生錯誤" + "title": "" }, "n-more-policies_other": "{{count}} 個其他政策" }, @@ -2126,6 +2178,7 @@ "query-and-expressions-step": { "add-query": "新增查詢", "body-queries-expressions-configured": "建立至少一個查詢或表達式以便收到警報", + "confirmText-deactivate": "", "expressions": "表達式", "loading-data-sources": "正在載入資料來源…", "manipulate-returned-queries-other-operations": "使用數學和其他運算來處理查詢返回的資料。", @@ -2175,6 +2228,9 @@ "label-export-all": "匯出全部" } }, + "recording-rule": { + "term": "" + }, "recording-rule-editor": { "error-no-query-editor": "由於以下原因,無法載入查詢編輯器:{{errorMessage}}" }, @@ -2193,6 +2249,7 @@ "redirect-to-clone-rule": { "body-evaluation-group": "您需要為複製的規則設定新的評估群組,因為原始群組已佈建,無法用於在使用者介面中建立的規則。", "body-not-provisioned": "新規則將<1>不會標記為已佈建的規則。", + "confirmText-copy": "", "title-copy-provisioned-alert-rule": "複製已設定的警報規則" }, "redirect-to-rule-viewer": { @@ -2389,8 +2446,6 @@ "title-inspect-alert-rule": "檢查警報規則" }, "rule-list": { - "cannot-find-rule-details-for": "找不到「UID {{uid}}」的規則詳細資料", - "cannot-load-rule-details-for": "無法載入「UID {{uid}}」的規則詳細資料", "configure-datasource": "設定", "draft-new-rule": "撰寫新規則", "ds-error": { @@ -2413,7 +2468,7 @@ "new-badge": "全新!", "text": "匯入至 Grafana 管理的規則" }, - "loading-more-groups": "", + "loading-more-groups": "正在載入更多群組…", "more": "更多", "new-alert-rule": "新的警報規則", "new-datasource-recording-rule": "新的資料來源錄製規則", @@ -2585,6 +2640,11 @@ "selecting-data-source-tooltip": { "tooltip-content": "找不到您想要的資料來源嗎?某些資料來源不支援警報。按一下該圖示了解更多資訊。" }, + "settings": { + "tabs": { + "alert-managers": "" + } + }, "settings-content": { "add-new-alertmanager": "新增 Alertmanager", "builtin-alertmanager": "內建的 Alertmanager", @@ -2737,6 +2797,9 @@ "existing-templates-selector-placeholder-choose-notification-template": "選擇通知範本", "loading": "正在載入…", "template-options": { + "ariaLabel": { + "select-notification-template": "" + }, "label": { "select-notification-template": "選取通知範本" } @@ -2763,6 +2826,8 @@ }, "templates-table": { "actions": "動作", + "body-delete-template-group": "", + "confirmText-yes-delete": "", "no-templates-defined": "未定義任何範本。", "template-group": "範本群組", "title-delete-template-group": "刪除範本群組" @@ -2815,6 +2880,11 @@ "timestamp": { "time-ago": "({{time}} 前)" }, + "titles": { + "group-view": { + "subtitle": "" + } + }, "to-gma": { "confirm-modal": { "body": "目標資料夾不為空,某些規則可能會被覆寫或移除。確定要將這些警報規則匯入至 Grafana 管理的規則嗎?", @@ -2890,6 +2960,11 @@ "title-delete-contact-point": "刪除聯絡點" } }, + "use-delete-modal": { + "modal": { + "confirmText-yes-delete": "" + } + }, "use-delete-policy-modal": { "modal-element": { "title-delete-notification-policy": "刪除通知政策" @@ -2941,51 +3016,51 @@ } }, "alertlist": { - "category-alert-state-filter": "", - "category-filter": "", - "category-options": "", - "descriptino-alerts-linked-to-dashboard": "", - "description-alert-instance-label": "", - "description-alert-name": "", - "description-datasource": "", - "description-folder": "", - "description-group-by": "", - "description-group-mode": "", - "description-max-items": "", - "description-show-inactive-alerts": "", - "description-sort-order": "", - "description-view-mode": "", + "category-alert-state-filter": "警報狀態篩選條件", + "category-filter": "篩選", + "category-options": "選項", + "descriptino-alerts-linked-to-dashboard": "僅顯示連結至此儀表板的警報", + "description-alert-instance-label": "使用標籤查詢篩選警報執行個體,例如:{severity=\"critical\", instance=~\"cluster-us-.+\"}", + "description-alert-name": "篩選包含此文字的警報", + "description-datasource": "從警報來源篩選", + "description-folder": "篩選所選資料夾中的警報(僅適用於 Grafana 管理的警報規則)", + "description-group-by": "使用標籤查詢篩選警報", + "description-group-mode": "警報執行個體應如何分組", + "description-max-items": "可顯示的最大警報數", + "description-show-inactive-alerts": "包含具有 0(零)個執行個體的警報規則。由於這些規則沒有執行個體,因此如果設定了警報執行個體標籤篩選器,它們將保持隱藏。", + "description-sort-order": "警報和警報執行個體的排序順序", + "description-view-mode": "在清單檢視和統計資料檢視之間切換", "group-by": { "aria-label-group-by-label-keys": "根據標籤鍵分組", "placeholder-group-by": "分組依據" }, "group-mode-options": { - "label-custom-grouping": "", - "label-default-grouping": "" + "label-custom-grouping": "自訂分組", + "label-default-grouping": "預設分組" }, - "name-alert-instance-label": "", - "name-alert-name": "", - "name-alerts-linked-to-dashboard": "", - "name-datasource": "", - "name-error": "", - "name-firing": "", - "name-folder": "", - "name-group-by": "", - "name-group-mode": "", - "name-max-items": "", - "name-no-data": "", - "name-normal": "", - "name-pending": "", - "name-recovering": "", - "name-show-inactive-alerts": "", - "name-sort-order": "", - "name-view-mode": "", + "name-alert-instance-label": "警報執行個體標籤", + "name-alert-name": "警報名稱", + "name-alerts-linked-to-dashboard": "連結至此儀表板的警報", + "name-datasource": "資料來源", + "name-error": "錯誤", + "name-firing": "警報/觸發", + "name-folder": "資料夾", + "name-group-by": "分組依據", + "name-group-mode": "群組模式", + "name-max-items": "最大項目數", + "name-no-data": "無資料", + "name-normal": "一般", + "name-pending": "待處理", + "name-recovering": "正在還原", + "name-show-inactive-alerts": "顯示具有 0 個執行個體的警報", + "name-sort-order": "排序順序", + "name-view-mode": "視圖模式", "sort-order-options": { - "label-alphabetical-asc": "", - "label-alphabetical-desc": "", - "label-importance": "", - "label-time-asc": "", - "label-time-desc": "" + "label-alphabetical-asc": "字母(升序)", + "label-alphabetical-desc": "字母(降序)", + "label-importance": "重要性", + "label-time-asc": "時間(升序)", + "label-time-desc": "時間(降序)" }, "ungrouped-mode-view": { "active-for": "針對 <1>{{duration}} ", @@ -3001,8 +3076,8 @@ "title-permission-required": "需要權限" }, "view-mode-options": { - "label-list": "", - "label-stat": "" + "label-list": "清單", + "label-stat": "統計資料" } }, "annolist": { @@ -3015,38 +3090,39 @@ "annotation-list-item": { "tooltip-created-by": "建立者:<1>{{email}}" }, - "category-annotation-query": "", - "category-display": "", - "category-link-behaviour": "", - "description-tags": "", + "category-annotation-query": "註解查詢", + "category-display": "顯示", + "category-link-behaviour": "連結行為", + "description-tags": "匹配註解標籤", "link-target-options": { - "label-dashboard": "", - "label-panel": "" + "label-dashboard": "儀表板", + "label-panel": "面板" }, - "name-limit": "", - "name-link-target": "", - "name-query-filter": "", - "name-show-tags": "", - "name-show-time": "", - "name-show-user": "", - "name-tags": "", - "name-time-after": "", - "name-time-before": "", - "name-time-range": "", + "name-limit": "限制", + "name-link-target": "連結目標", + "name-query-filter": "查詢篩選條件", + "name-show-tags": "顯示標籤", + "name-show-time": "顯示時間", + "name-show-user": "顯示使用者", + "name-tags": "標籤", + "name-time-after": "下列時間之後的時間", + "name-time-before": "下列時間之前的時間", + "name-time-range": "時間範圍", "query-filter-options": { - "label-all-dashboards": "", - "label-this-dashboard": "" + "label-all-dashboards": "所有儀表板", + "label-this-dashboard": "此儀表板" }, "time-range-options": { - "label-none": "", - "label-this-dashboard": "" + "label-none": "無", + "label-this-dashboard": "此儀表板" } }, "annotations": { "annotation-field-mapper": { "annotation": "註解", "first-value": "第一項數值", - "from": "" + "from": "", + "noOptionsMessage-unknown-field-names": "" }, "empty-state": { "button-title": "新增注釋查詢", @@ -3054,6 +3130,16 @@ "info-box-content-2": "查看<2>註釋文件,了解更多資訊。", "title": "尚未新增自訂注釋查詢" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "", + "results-split-on-comma": "" + }, + "placeholder": { + "text-or-the-first-field": "", + "time-or-the-first-field": "" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} 個事件(來自 {{numFields}} 個欄位)", "no-events-found": "找不到事件", @@ -3129,6 +3215,9 @@ "define-allowed-teams-ids-label": "定義允許的團隊 ID", "display-name-description": "將在登入頁面上顯示為「使用...登入」。如果您使用多個身分提供者或 SSO 協定,這項功能會很有幫助。", "display-name-label": "顯示名稱", + "domain-hint-description": "", + "domain-hint-label": "", + "domain-hint-valid-domain": "", "email-attribute-name-description": "設定用來在 OAuth2 ID 權杖的屬性對應中查找使用者電子郵件的金鑰名稱。", "email-attribute-name-label": "電子郵件屬性名稱", "email-attribute-path-description": "用來從使用者資訊中查找使用者電子郵件的 JMESPath 表達式。", @@ -3180,7 +3269,7 @@ "team-ids-github": "團隊 ID 的整數清單。", "team-ids-label": "團隊 ID", "team-ids-numbers": "團隊 ID 必須為數字。", - "team-ids-other": "團隊識別碼的字串清單。", + "team-ids-other": "", "team-ids-placeholder": "輸入團隊 ID,然後按 Enter 新增", "teams-url-description": "用於查詢團隊 ID 的網址。如未設定,則預設值為 /teams。", "teams-url-description-oauth": "若您設定了「{{ teamsURLLabel }}」,則必須同時設定「{{ teamIDsAttributePathLabel }}」。", @@ -3215,8 +3304,8 @@ } }, "provider-card": { - "saml-learn-more": "", - "scim-learn-more": "", + "saml-learn-more": "使用 SAML 進行單一登入 (SSO)。", + "scim-learn-more": " 使用 SCIM 同步使用者和團隊。", "text-badge-enabled": "已啟用", "text-badge-not-enabled": "未啟用" }, @@ -3224,6 +3313,7 @@ "additional-actions-menu": { "label-reset-to-default-values": "重設為預設值" }, + "confirmText-reset": "", "disable": "停用", "disabling": "正在停用…", "discard": "捨棄", @@ -3252,99 +3342,99 @@ }, "bar-chart": { "warn": { - "missing-numeric": "", - "missing-series": "" + "missing-numeric": "找不到數值欄位", + "missing-series": "長條圖需要字串或時間欄位" } }, "barchart": { "config": { - "category-thresholds": "", - "description-color-by-field": "", - "description-x-label-max-length": "", - "name-bar-radius": "", - "name-bar-width": "", - "name-color-by-field": "", - "name-fill-opacity": "", - "name-full-highlight": "", - "name-gradient-mode": "", - "name-group-width": "", - "name-line-width": "", - "name-orientation": "", - "name-rotate-x-labels": "", - "name-show-thresholds": "", - "name-show-values": "", - "name-stacking": "", - "name-transform": "", - "name-x-axis": "", - "name-x-label-max-length": "", - "name-x-label-min-spacing": "", + "category-thresholds": "閾值", + "description-color-by-field": "使用同層欄位的色彩值為每個長條值著色。", + "description-x-label-max-length": "X 軸標籤將被截斷為提供的長度", + "name-bar-radius": "長條半徑", + "name-bar-width": "長條寬度", + "name-color-by-field": "按欄位著色", + "name-fill-opacity": "填充不透明度", + "name-full-highlight": "懸停時突顯整個區域", + "name-gradient-mode": "漸變模式", + "name-group-width": "群組寬度", + "name-line-width": "線寬", + "name-orientation": "方向", + "name-rotate-x-labels": "旋轉 x 軸刻度標籤", + "name-show-thresholds": "顯示閾值", + "name-show-values": "顯示值", + "name-stacking": "堆疊", + "name-transform": "轉變", + "name-x-axis": "X 軸", + "name-x-label-max-length": "X 軸刻度標籤最大長度", + "name-x-label-min-spacing": "X 軸標籤最小間距", "orientation-options": { - "label-auto": "", - "label-horizontal": "", - "label-line-vertical": "" + "label-auto": "自動", + "label-horizontal": "橫向", + "label-line-vertical": "縱向" }, - "placeholder-x-axis": "", - "placeholder-x-label-max-length": "", + "placeholder-x-axis": "第一個字串或時間欄位", + "placeholder-x-label-max-length": "無", "show-values-options": { - "label-always": "", - "label-auto": "", - "label-never": "" + "label-always": "總是", + "label-auto": "自動", + "label-never": "從不" }, "transform-options": { - "description-constant": "", - "description-negative-y": "", - "label-constant": "", - "label-negative-y": "" + "description-constant": "第一個值將顯示為常數線", + "description-negative-y": "將結果翻轉為 Y 軸上的負值", + "label-constant": "常數", + "label-negative-y": "負 Y" } }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "需要在右側保留空間", "gaps-options": { - "description-large": "", - "description-medium": "", - "description-none": "", - "description-small": "", - "label-large": "", - "label-medium": "", - "label-none": "", - "label-small": "" + "description-large": "需要 {{spacing}} 間距", + "description-medium": "需要 {{spacing}} 間距", + "description-none": "顯示所有刻度標記", + "description-small": "需要 {{spacing}} 間距", + "label-large": "大", + "label-medium": "中", + "label-none": "無", + "label-small": "小" }, "label-rtl": "由右至左" } }, "bargauge": { "bar-size-options": { - "label-auto": "", - "label-manual": "" + "label-auto": "自動", + "label-manual": "手動" }, - "category-bar-gauge": "", - "description-max-height": "", - "description-min-height": "", - "description-min-width": "", - "description-show-unfilled-area": "", + "category-bar-gauge": "長條儀表", + "description-max-height": "最大列高(橫向)", + "description-min-height": "最小列高(橫向)", + "description-min-width": "最小欄寬(縱向)", + "description-show-unfilled-area": "啟用後,未填充的區域將顯示為灰色", "display-mode-options": { - "label-basic": "", - "label-gradient": "", - "label-retro": "" + "label-basic": "基本", + "label-gradient": "漸變", + "label-retro": "復古 LCD" }, - "name-bar-size": "", - "name-display-mode": "", - "name-max-height": "", - "name-min-height": "", - "name-min-width": "", - "name-name-placement": "", + "name-bar-size": "長條尺寸", + "name-display-mode": "顯示模式", + "name-max-height": "最大高度", + "name-min-height": "最小高度", + "name-min-width": "最小寬度", + "name-name-placement": "名稱位置", "name-placement-options": { - "label-auto": "", - "label-hidden": "", - "label-left": "", - "label-top": "" + "label-auto": "自動", + "label-hidden": "隱藏", + "label-left": "左側", + "label-top": "頂端" }, - "name-show-unfilled-area": "", - "name-value-display": "", + "name-show-unfilled-area": "顯示未填充區域", + "name-value-display": "數值顯示", "value-display-options": { - "label-hidden": "", - "label-text-color": "", - "label-value-color": "" + "label-hidden": "隱藏", + "label-text-color": "文字色彩", + "label-value-color": "數值色彩" } }, "bookmarks-page": { @@ -3367,7 +3457,7 @@ "delete-modal-restore-dashboards-text": "此動作會立即刪除選取的資料夾,但選取的儀表板會標記在 30 天後刪除。您的組織管理員可以在 30 天到期之前隨時還原儀表板。資料夾無法還原。", "delete-modal-text": "此動作將刪除以下內容:", "delete-modal-title": "刪除", - "delete-provisioned-folder": "", + "delete-provisioned-folder": "刪除已佈建的資料夾", "deleting": "正在刪除…", "manage-permissions-button": "管理權限", "move-button": "移動", @@ -3404,12 +3494,12 @@ "tags-column": "標籤" }, "delete-provisioned-folder-form": { - "alert-folder-deleted-successfully": "", - "api-error": "", - "button-cancel": "", - "button-delete": "", - "button-deleting": "", - "delete-warning": "" + "alert-folder-deleted-successfully": "已成功刪除資料夾", + "api-error": "無法刪除資料夾", + "button-cancel": "取消", + "button-delete": "刪除", + "button-deleting": "正在刪除…", + "delete-warning": "這將刪除此資料夾及其所有子資料夾。總體而言,這會影響:" }, "descendant-count": { "title-unable-to-retrieve-descendant-information": "無法擷取子系資訊" @@ -3455,11 +3545,11 @@ "button-create": "建立", "button-creating": "正在建立…", "cancel": "取消", - "error-invalid-folder-name": "無效的資料夾名稱", "error-required": "資料夾名稱為必填", "folder-name-input-placeholder-enter-folder-name": "輸入資料夾名稱", "label-folder-name": "資料夾名稱", "text-pull-request-created": "已建立拉取請求,其中包含對此資料夾的變更:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "已建立拉取請求", "title-this-repository-is-read-only": "此存放庫為唯讀" }, @@ -3468,52 +3558,52 @@ "text": "未找到您的查詢結果" }, "restore": { - "success": "" + "success": "儀表板 {{name}} 已復原" }, "text-this-repository-is-read-only": "如果您可以直接存取目標,請複製 JSON 並將其貼上至該處。", "trash-state-manager": { "label": { - "alphabetically-az": "", - "alphabetically-za": "" + "alphabetically-az": "按字母順序 (A–Z)", + "alphabetically-za": "按字母順序 (Z–A)" } } }, "candlestick": { "additional-fields-options": { - "label-ignore": "", - "label-include": "" + "label-ignore": "略過", + "label-include": "包含" }, "candle-style-options": { - "label-candles": "", - "label-ohlc-bars": "" + "label-candles": "蠟燭", + "label-ohlc-bars": "OHLC 條" }, - "category-candlestick": "", + "category-candlestick": "K 線", "color-strategy-options": { - "label-since-open": "", - "label-since-prior-close": "" + "label-since-open": "自開盤以來", + "label-since-prior-close": "自上次收盤以來" }, - "description-additional-fields": "", - "description-close": "", - "description-high": "", - "description-low": "", - "description-open": "", - "description-volume": "", + "description-additional-fields": "使用標準時間序列選項來設定上方未對應的欄位", + "description-close": "期間結束時的值", + "description-high": "期間內的最大值", + "description-low": "期間內的最小值", + "description-open": "期間開始時的值", + "description-volume": "期間內的樣本數", "mode-options": { - "label-both": "", - "label-candles": "", - "label-volume": "" + "label-both": "兩者", + "label-candles": "蠟燭", + "label-volume": "量" }, - "name-additional-fields": "", - "name-candle-style": "", - "name-close": "", - "name-color-strategy": "", - "name-down-color": "", - "name-high": "", - "name-low": "", - "name-mode": "", - "name-open": "", - "name-up-color": "", - "name-volume": "" + "name-additional-fields": "其他欄位", + "name-candle-style": "蠟燭樣式", + "name-close": "關閉", + "name-color-strategy": "色彩策略", + "name-down-color": "調暗色彩", + "name-high": "高", + "name-low": "低", + "name-mode": "模式", + "name-open": "開啟", + "name-up-color": "調亮色彩", + "name-volume": "量" }, "canvas": { "apieditor": { @@ -3528,18 +3618,18 @@ } }, "button-item": { - "category-button": "", + "category-button": "按鈕", "label": { "center": "置中", "left": "靠左", "right": "靠右" }, - "name-align-text": "", - "name-api": "", - "name-style": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", + "name-align-text": "對齊文字", + "name-api": "API", + "name-style": "樣式", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", "placeholder": { "auto": "自動" } @@ -3566,20 +3656,20 @@ } } }, - "category-arrow-direction": "", - "category-background": "", - "category-border": "", - "category-canvas": "", - "category-color": "", - "category-data-links": "", - "category-line-style": "", - "category-metric-value": "", - "category-radius": "", - "category-selected-connection": "", - "category-selected-element": "", - "category-size": "", + "category-arrow-direction": "箭頭方向", + "category-background": "背景", + "category-border": "邊框", + "category-canvas": "畫布", + "category-color": "色彩", + "category-data-links": "資料連結和動作", + "category-line-style": "線條樣式", + "category-metric-value": "指標值", + "category-radius": "半徑", + "category-selected-connection": "所選連線", + "category-selected-element": "所選元素 ({{element}})", + "category-size": "大小", "cloud-item": { - "category-cloud": "", + "category-cloud": "雲端", "label": { "bottom": "底部", "center": "置中", @@ -3588,48 +3678,48 @@ "right": "靠右", "top": "頂端" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "placeholder": { "auto": "自動" } }, - "description-experimental-types": "", - "description-infinite-panning": "", - "description-inline-editing": "", - "description-pan-zoom": "", + "description-experimental-types": "啟用實驗元素類型的選擇", + "description-infinite-panning": "啟用無限平移 - 適用於寬敞的畫布。警告:這是一項實驗功能,目前僅適用於頂端/左側受限的元素", + "description-inline-editing": "啟用直接編輯面板", + "description-pan-zoom": "啟用平移和縮放", "direction-options": { - "label-both": "", - "label-forward": "", - "label-none": "", - "label-reverse": "" + "label-both": "兩者", + "label-forward": "正向", + "label-none": "無", + "label-reverse": "反向" }, "drone-front-item": { - "category-drone-front": "", - "name-roll-angle": "" + "category-drone-front": "無人機前面", + "name-roll-angle": "滾動角度" }, "drone-side-item": { - "category-drone-side": "", - "name-pitch-angle": "" + "category-drone-side": "無人機側面", + "name-pitch-angle": "傾斜角度" }, "drone-top-item": { - "category-drone-top": "", - "name-back-left-rotor-rpm": "", - "name-back-right-rotor-rpm": "", - "name-front-left-rotor-rpm": "", - "name-front-right-rotor-rpm": "", - "name-yaw-angle": "" + "category-drone-top": "無人機頂端", + "name-back-left-rotor-rpm": "左後轉子 RPM", + "name-back-right-rotor-rpm": "右後轉子 RPM", + "name-front-left-rotor-rpm": "左前轉子 RPM", + "name-front-right-rotor-rpm": "右前轉子 RPM", + "name-yaw-angle": "偏轉角度" }, "element-editor": { - "category-layout": "", - "description-unsupported": "", - "name-quick-placement": "" + "category-layout": "版面配置", + "description-unsupported": "目前設定不支援所選元素類型。請啟用進階元素類型。", + "name-quick-placement": "快速放置" }, "ellipse-item": { - "category-ellipse": "", + "category-ellipse": "橢圓形", "label": { "bottom": "底部", "center": "置中", @@ -3638,50 +3728,50 @@ "right": "靠右", "top": "頂端" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "placeholder": { "auto": "自動" } }, "icon-item": { - "category-icon": "", - "name-fill-color": "", - "name-svg-path": "" + "category-icon": "圖示", + "name-fill-color": "填充色彩", + "name-svg-path": "SVG 路徑" }, "inline-edit": { "canvas-inline-editor": "Canvas 內嵌編輯器", "tooltip-close-inline-editor": "關閉內嵌編輯器" }, "inline-edit-body": { - "category-selected-element": "", + "category-selected-element": "所選元素 ({{element}})", "label-add-item": "新增項目", "please-select-an-element": "請選取一個元件" }, - "label-actions": "", - "label-color": "", - "label-direction": "", - "label-image": "", - "label-image-size": "", - "label-line-style": "", - "label-links": "", - "label-radius": "", - "label-size": "", - "label-width": "", + "label-actions": "動作", + "label-color": "色彩", + "label-direction": "方向", + "label-image": "影像", + "label-image-size": "影像大小", + "label-line-style": "線條樣式", + "label-links": "連結", + "label-radius": "半徑", + "label-size": "大小", + "label-width": "寬度", "layer-editor": { - "category-layer": "", - "category-layout": "", - "name-constraints": "", - "name-elements": "" + "category-layer": "圖層", + "category-layout": "版面配置", + "name-constraints": "限制條件", + "name-elements": "元素" }, "line-style-editor": { "label-animate": "動畫" }, "line-style-options": { - "label-solid": "" + "label-solid": "實體" }, "metric-value-item": { "label": { @@ -3696,15 +3786,15 @@ "auto": "自動" } }, - "name-align-text": "", - "name-color": "", - "name-experimental-types": "", - "name-infinite-panning": "", - "name-inline-editing": "", - "name-pan-zoom": "", - "name-text": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-color": "文字色彩", + "name-experimental-types": "實驗性元素類型", + "name-infinite-panning": "無限平移", + "name-inline-editing": "直接編輯", + "name-pan-zoom": "平移和縮放", + "name-text": "文字", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "not-found-display": { "not-found": "<0>未找到:" }, @@ -3717,7 +3807,7 @@ "zoom-scroll-wheel": "縮放:使用滑鼠滾輪" }, "parallelogram-item": { - "category-parallelogram": "", + "category-parallelogram": "平行四邊形", "label": { "bottom": "底部", "center": "置中", @@ -3726,11 +3816,11 @@ "right": "靠右", "top": "頂端" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "placeholder": { "auto": "自動" } @@ -3743,21 +3833,21 @@ }, "placement-editor": { "horizontal-options": { - "label-center": "", - "label-left": "", - "label-left-and-right": "", - "label-right": "", - "label-scale": "" + "label-center": "置中", + "label-left": "左側", + "label-left-and-right": "左與右", + "label-right": "右側", + "label-scale": "縮放" }, "label-constraints": "限制條件", "label-position": "位置", "loading": "正在載入…", "vertical-options": { - "label-bottom": "", - "label-center": "", - "label-scale": "", - "label-top": "", - "label-top-and-bottom": "" + "label-bottom": "底部", + "label-center": "置中", + "label-scale": "縮放", + "label-top": "頂端", + "label-top-and-bottom": "頂端與底部" } }, "quick-positioning": { @@ -3769,7 +3859,7 @@ "tooltip-align-vertical-centers": "垂直居中對齊" }, "rectangle-item": { - "category-rectangle": "", + "category-rectangle": "長方形", "label": { "bottom": "底部", "center": "置中", @@ -3778,33 +3868,33 @@ "right": "靠右", "top": "頂端" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "placeholder": { "auto": "自動" } }, "server-item": { - "category-server": "", - "name-blink-rate": "", - "name-bulb-color": "", - "name-status-color": "", - "name-type": "", + "category-server": "伺服器", + "name-blink-rate": "閃爍速率 [hz](0 = 關閉)", + "name-bulb-color": "燈泡色彩", + "name-status-color": "狀態色彩", + "name-type": "類型", "type-options": { - "label-database": "", - "label-single": "", - "label-stack": "", - "label-terminal": "" + "label-database": "資料庫", + "label-single": "單一", + "label-stack": "堆疊", + "label-terminal": "終端" } }, "text-display": { "double-click-to-set": "按兩下以設定文字" }, "text-item": { - "category-text": "", + "category-text": "文字", "label": { "bottom": "底部", "center": "置中", @@ -3813,11 +3903,11 @@ "right": "靠右", "top": "頂端" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "placeholder": { "auto": "自動" } @@ -3839,7 +3929,7 @@ "tooltip-remove": "移除" }, "triangle-item": { - "category-triangle": "", + "category-triangle": "三角形", "label": { "bottom": "底部", "center": "置中", @@ -3848,18 +3938,18 @@ "right": "靠右", "top": "頂端" }, - "name-align-text": "", - "name-text": "", - "name-text-color": "", - "name-text-size": "", - "name-vertical-align": "", + "name-align-text": "對齊文字", + "name-text": "文字", + "name-text-color": "文字色彩", + "name-text-size": "文字大小", + "name-vertical-align": "垂直對齊", "placeholder": { "auto": "自動" } }, "wind-turbine-item": { - "category-wind-turbine": "", - "name-rpm": "" + "category-wind-turbine": "風力發動機", + "name-rpm": "RPM" } }, "carousel": { @@ -3876,7 +3966,8 @@ "close-button": { "tooltip": "關閉" }, - "cloud-enterprise-feature-badge": "", + "cloud-enterprise-feature-badge": "雲端與企業版", + "cloud-feature-badge": "雲端", "combobox": { "async": { "error": "載入選項時發生錯誤。" @@ -4145,8 +4236,8 @@ } }, "dashbaord-scene": { - "interval-variable-form": { - "description-auto-option": "透過將時間範圍除以指定的計數來動態計算間隔" + "variable-editor-form": { + "run-query": "" } }, "dashboard": { @@ -4308,12 +4399,14 @@ }, "dash-nav": { "on-open-snapshot-original": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "是否繼續前往外部網站?" } }, "render-left-actions": { - "text-public": "公共", "tooltip-view-as-scene": "以場景形式檢視" } }, @@ -4404,7 +4497,7 @@ } }, "description": { - "action": "" + "action": "變更儀表板描述" }, "dynamic-config-value-editor": { "render-label": { @@ -4413,12 +4506,12 @@ }, "edit-actions": { "add": "新增 {{typeName}}", - "move": "", + "move": "移動 {{typeName}}", "panel-background": "變更面板背景", "panel-description": "變更面板描述", - "panel-max-repeats-per-row": "", + "panel-max-repeats-per-row": "每列最大重複次數", "panel-repeat-direction": "重複方向", - "panel-repeat-variable": "", + "panel-repeat-variable": "面板重複方式", "panel-title": "變更面板標題", "paste-panel": "貼上面板", "remove": "移除 {{typeName}}", @@ -4506,6 +4599,14 @@ "errors": { "failed-to-load": "無法載入儀表板" }, + "export": { + "button": { + "label": "" + }, + "menu": { + "label": "" + } + }, "fetch-dashboard": { "message": { "failed-to-fetch-dashboard": "擷取儀表板失敗" @@ -4522,6 +4623,13 @@ "editable": "可编辑", "readonly": "唯讀" } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } } }, "get-debug-dashboard": { @@ -4537,7 +4645,7 @@ "get-field-override-categories": { "label-add-field-override": "新增欄位覆寫", "label-add-override-property": "新增覆寫屬性", - "override-name": "", + "override-name": "覆寫 {{overrideNum}}", "title": { "add-button": "增加按鈕" } @@ -4571,6 +4679,18 @@ "transform-data": "轉換資料" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -4749,7 +4869,7 @@ "title-matched_other": "匹配的 {{count}}/{{totalCount}} 個選項" }, "outline": { - "repeated-item": "", + "repeated-item": "重複", "tree-item": { "empty": "(空)", "no-title": "<無標題>" @@ -4825,6 +4945,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "確定要將儀表板還原到版本{{version}}嗎?所有未儲存變更將會遺失。", + "confirmText-restore-version": "", "title-restore-version": "還原版本" }, "row-options-button": { @@ -4875,6 +4996,9 @@ "title-not-unique": "此標題不是唯一" } }, + "save-dashboard": { + "message-dashboard-saved": "" + }, "save-dashboard-as-button": { "save-as": "另存為" }, @@ -4903,12 +5027,13 @@ "label-changes": "變更", "label-details": "詳細資訊", "title": "儲存儀表板", - "title-copy": "", - "title-provisioned": "" + "title-copy": "儲存儀表板副本", + "title-provisioned": "已佈建儀表板" }, "save-dashboard-error-proxy": { "body-name-exists": "所選資料夾中已存在名稱相同的儀表板。<1><2>仍要儲存此儀表板嗎?", "body-version-mismatch": "其他人已更新此儀表板<1><2>仍要儲存此儀表板嗎?", + "confirmText-save-and-overwrite": "", "title-name-exists": "衝突", "title-version-mismatch": "衝突" }, @@ -4980,7 +5105,7 @@ } }, "title": { - "action": "" + "action": "變更儀表板標題" }, "toolbar": { "add": "新增", @@ -5074,7 +5199,6 @@ "playlist-next": "前往下一個儀表板", "playlist-previous": "前往上一個儀表板", "playlist-stop": "停止播放清單", - "public-dashboard": "公共", "refresh": "重新整理儀表板", "save": "儲存儀表板", "save-dashboard": { @@ -5105,7 +5229,9 @@ "label-apply-transformation-to": "套用轉換至" }, "transformation-operation-row": { + "body-delete": "", "render-actions": { + "confirmText-delete": "", "title-debug": "除錯", "title-disable-transformation": "停用轉換", "title-filter": "篩選", @@ -5125,12 +5251,17 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "搜尋轉換", "show-images": "顯示圖片", - "title-add-another-transformation": "新增另一個轉換" + "title-add-another-transformation": "新增另一個轉換", + "view-all": "" + }, + "un-theme-transformations-editor": { + "body-delete-all-transformations": "" }, "un-themed-transformations-editor": { "actions": { "add-another-transformation": "新增另一個轉換" }, + "confirmText-delete-all": "", "delete-all-transformations": "刪除所有轉換", "title-delete-all-transformations": "是否要刪除所有轉換?", "tooltip-clear-search": "清除搜尋", @@ -5167,6 +5298,7 @@ "version-history-table": { "aria-label-toggle-selection": "切換{{version}}版本選擇", "date": "日期", + "name-latest": "", "notes": "備註", "restore": "還原", "updated-by": "更新者", @@ -5243,7 +5375,8 @@ "description-enables-users-custom-values": "使用者能夠將自訂值新增至清單", "description-provide-dimensions-as-csv-dimension-name-dimension-id": "以 CSV 格式提供維度:{{name}},{{value}}", "label-data-source": "資料來源", - "label-use-static-key-dimensions": "使用靜態金鑰維度" + "label-use-static-key-dimensions": "使用靜態金鑰維度", + "name-allow-custom-values": "" }, "add-to-dashboard": { "message": { @@ -5316,6 +5449,9 @@ } }, "config-public-dashboard": { + "confirmText": { + "revoke-public-url": "" + }, "title": { "revoke-public-url": "撤銷公共網址" } @@ -5327,6 +5463,7 @@ }, "custom-variable-form": { "custom-options": "自訂選項", + "name-values-separated-comma": "", "selection-options": "選擇選項" }, "dashboard-edit-pane-renderer": { @@ -5345,6 +5482,12 @@ "label-type": "類型", "label-url": "網址", "label-with-tags": "使用標記", + "link-type-options": { + "label": { + "dashboards": "", + "link": "" + } + }, "placeholder-open-dashboard": "打開儀表板" }, "dashboard-link-list": { @@ -5357,15 +5500,9 @@ "type": "類型" }, "dashboard-preview-banner": { - "not-saved": "該數值尚未儲存在 Grafana 資料庫中", "not-yet-saved": "該數值未儲存在 Grafana 資料庫中", - "open-pull-request-in-git-hub": "在 GitHub 中開啟拉取請求", - "title-dashboard-loaded-branch-git-hub": "此儀表板是從 GitHub 的分支載入。", "title-dashboard-loaded-external-repository": "此儀表板是從外部存放庫載入", - "title-dashboard-loaded-request-git-hub": "此儀表板是從 GitHub 的拉取請求載入。", - "title-error-loading-dashboard": "載入控制面板發生錯誤", - "value-not-saved": "該數值尚未儲存在 Grafana 資料庫中", - "view-pull-request-in-git-hub": "在 GitHub 中檢視拉取請求" + "title-error-loading-dashboard": "載入控制面板發生錯誤" }, "dashboard-scene": { "text": { @@ -5391,6 +5528,8 @@ "data-source-options": "資料來源選項", "description-instance-name-filter": "在變數值清單中選擇資料來源執行個體的正規表達式篩選器。全部留空。", "example-instance-name-filter": "範例:", + "name-instance-name-filter": "", + "name-type": "", "selection-options": "選擇選項" }, "default-grid-layout-manager": { @@ -5401,12 +5540,12 @@ } }, "delete-provisioned-dashboard-form": { - "api-error": "", + "api-error": "無法刪除儀表板", "cancel-action": "取消", "delete-action": "刪除儀表板", - "delete-read-only-file-message": "", + "delete-read-only-file-message": "由於儲存庫為唯讀,因此無法直接從 Grafana 刪除此儀表板。若要刪除此儀表板,請從您的 Git 儲存庫中移除該檔案。", "deleting": "正在刪除…", - "drawer-title": "", + "drawer-title": "刪除已佈建的儀表板", "title-this-repository-is-read-only": "此存放庫為唯讀" }, "description-label": { @@ -5420,7 +5559,7 @@ } }, "edit-provisioned-dashboard-form": { - "success": "" + "success": "儀表板變更已成功儲存" }, "email-list": { "aria-label-emailmenu": "切換電子郵件選單" @@ -5436,12 +5575,61 @@ "empty-transformations-message": { "add-transformation": "新增轉換" }, + "general-settings-edit-view": { + "editable_options": { + "label": { + "editable": "", + "readonly": "" + } + }, + "graph_tooltip_options": { + "label": { + "default": "", + "shared-crosshair": "", + "shared-tooltip": "" + } + } + }, "get-edit-options": { "title": { "column-options": "欄位選項", "row-height-options": "資料列高度選項" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "", + "dynamically-switch-source-multiple-panels": "", + "group": "", + "hidden-constant-variable": "", + "users-enter-arbitrary-strings-textbox": "", + "values-are-static-and-defined-manually": "", + "values-fetched-source-query": "", + "values-timespans": "" + }, + "name": { + "ad-hoc-filters": "", + "constant": "", + "custom": "", + "data-source": "", + "group-by": "", + "interval": "", + "query": "", + "textbox": "" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "", + "annotation-panels-support-annotations": "", + "annotations-explicitly-listed-panels": "" + }, + "label": { + "all-panels": "", + "all-panels-except": "", + "selected-panels": "" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5466,7 +5654,8 @@ "description-provide-dimensions-as-csv-dimension-name-dimension-id": "以 CSV 格式提供維度:{{name}},{{value}}", "group-by-options": "按選項分組", "label-data-source": "資料來源", - "label-use-static-group-by-dimensions": "使用靜態群組維度" + "label-use-static-group-by-dimensions": "使用靜態群組維度", + "name-allow-custom-values": "" }, "help-wizard": { "copy-to-clipboard": "複製至剪貼簿", @@ -5502,9 +5691,14 @@ "apply": "套用" }, "interval-variable-form": { + "description-auto-option": "", "description-calculated-value-below-threshold": "計算值不會低於此閾值", "description-step-count": "應該將目前的時間範圍除以次數來計算數值", - "interval-options": "間隔選項" + "interval-options": "間隔選項", + "name-auto-option": "", + "name-min-interval": "", + "name-step-count": "", + "name-values": "" }, "json-model-edit-view": { "cancel-button": { @@ -5523,11 +5717,28 @@ "last-edited": "在 {{timeAgo}}前", "usage-count_other": "用於 {{count}} 個儀表板" }, + "move-provisioned-dashboard-form": { + "api-error": "", + "cancel-action": "", + "current-file-not-found": "", + "drawer-title": "", + "file-load-error": "", + "loading-file-data": "", + "move-action": "", + "move-read-only-message": "", + "moving": "", + "partial-failure-warning": "", + "target-path-label": "", + "title-this-repository-is-read-only": "" + }, "name-already-exists-error": { "body-name-already-exists": "所選資料夾中已存在名稱相同的儀表板。仍要儲存此儀表板嗎?", "title-name-already-exists": "名稱已存在" }, "on-open-snapshot-original-dashboard": { + "confirmText": { + "proceed": "" + }, "title": { "proceed-to-external-site": "是否繼續前往外部網站?" } @@ -5541,7 +5752,7 @@ } }, "panel-data-alerting-tab": { - "tab-label": "" + "tab-label": "警報" }, "panel-data-alerting-tab-rendered": { "alert": { @@ -5552,17 +5763,19 @@ "title-dashboard-not-saved": "儀表板未儲存" }, "panel-data-queries-tab": { - "tab-label": "" + "tab-label": "查詢" }, "panel-data-queries-tab-rendered": { "add-query": "新增查詢", "expression": "表達式" }, "panel-data-transformations-tab": { - "tab-label": "" + "tab-label": "轉換" }, "panel-data-transformations-tab-rendered": { "add-another-transformation": "新增另一個轉換", + "body-delete-all-transformations": "", + "confirmText-delete-all": "", "delete-all-transformations": "刪除所有轉換", "title-delete-all-transformations": "是否要刪除所有轉換?" }, @@ -5615,9 +5828,13 @@ "description-examples": "已命名的擷取群組可用於分隔顯示文字與數值(<1>請參閱範例)。", "description-optional": "若想擷取系列名稱或指標節點區段的一部分,則為可選。", "label-data-source": "資料來源", + "label-static-options-sort": "", "label-target-data-source": "目標資料來源", + "label-use-static-options": "", + "name-regex": "", "query-options": "查詢選項", - "selection-options": "選擇選項" + "selection-options": "選擇選項", + "static-options-legend": "" }, "resource-export": { "label": { @@ -5630,6 +5847,7 @@ }, "revert-dashboard-modal": { "body-restore-version": "確定要將儀表板還原到版本{{version}}嗎?所有未儲存變更將會遺失。", + "confirmText-restore-version": "", "title-restore-version": "還原版本" }, "save-button": { @@ -5647,15 +5865,16 @@ "label-folder": "資料夾", "render-footer": { "title-failed-to-save-dashboard": "無法儲存儀表板" - } + }, + "required": "" }, "save-dashboard-drawer": { "tabs": { "label-changes": "變更", "label-details": "詳細資訊", "title": "儲存儀表板", - "title-copy": "", - "title-provisioned": "" + "title-copy": "儲存儀表板副本", + "title-provisioned": "已佈建儀表板" } }, "save-dashboard-form": { @@ -5722,7 +5941,11 @@ "selection-options-form": { "description-enables-multiple-values-selected": "啟用同時選取多個值", "description-enables-option-include-variables": "啟用選項以包含所有值", - "description-enables-users-custom-values": "使用者能夠將自訂值新增至清單" + "description-enables-users-custom-values": "使用者能夠將自訂值新增至清單", + "name-allow-custom-values": "", + "name-custom-all-value": "", + "name-include-all-option": "", + "name-multi-value": "" }, "share-button": { "aria-label-sharedropdownmenu": "切換分享選單" @@ -5742,6 +5965,9 @@ "copy-to-clipboard-failed": "複製到剪貼簿失敗" } }, + "text-box-variable": { + "name-default-value": "" + }, "text-box-variable-form": { "placeholder-default-value-if-any": "(選填)", "text-options": "文字選項" @@ -5765,6 +5991,8 @@ } }, "unlink-modal": { + "body-unlink-panel": "", + "confirmText-yes-unlink": "", "title-really-unlink-panel": "確定要取消連結此面板嗎?" }, "unsaved-changes-modal": { @@ -5781,6 +6009,9 @@ } } }, + "use-save-dashboard": { + "message-dashboard-saved": "" + }, "variable-check-indicator": { "aria-label-variable-referenced-dashboard": "此變數未被任何變數或儀表板參照使用。", "aria-label-variable-referenced-other-variables-dashboard": "此變數被其他變數或儀表板參照使用。", @@ -5790,10 +6021,16 @@ "variable-editor-form": { "aria-label-variable-editor-form": "變數編輯器表單", "back-to-list": "返回清單", + "confirmText": { + "delete-variable": "" + }, "delete": "刪除", "description-optional-display-name": "可選的顯示名稱", "description-template-variable-characters": "範本變數的名稱。(最多 50 個字元)", "general": "一般", + "name-description": "", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "描述性文字", "placeholder-label-name": "標籤名稱", "placeholder-variable-name": "變數名稱", @@ -5808,13 +6045,25 @@ "variable": "變數" }, "variable-editor-list-row": { + "body-delete-variable": "", + "confirmText-delete-variable": "", "title-delete-variable": "刪除變數", "tooltip-duplicate-variable": "複製變數", "tooltip-remove-variable": "移除變數" }, "variable-hide-select": { + "hide_options": { + "label": { + "label": "", + "nothing": "", + "variable": "" + } + }, "label": "隱藏" }, + "variable-type-select": { + "name-variable-type": "" + }, "variable-usages-button": { "title-show-usages": "顯示以下項目的使用情況:{{variableId}}", "tooltip-show-usages": "顯示使用情況" @@ -5841,6 +6090,7 @@ "version-history-table": { "aria-label-toggle-selection": "切換{{version}}版本選擇", "date": "日期", + "name-latest": "", "notes": "備註", "restore": "還原", "updated-by": "更新者", @@ -5947,18 +6197,18 @@ } }, "dashlist": { - "category-dashboard-list": "", - "name-folder": "", - "name-include-current-template-variables": "", - "name-include-current-time-range": "", - "name-max-items": "", - "name-query": "", - "name-recently-viewed": "", - "name-search": "", - "name-show-folder-names": "", - "name-show-headings": "", - "name-starred": "", - "name-tags": "" + "category-dashboard-list": "儀表板清單", + "name-folder": "資料夾", + "name-include-current-template-variables": "包含目前的範本變數值", + "name-include-current-time-range": "包含目前的時間範圍", + "name-max-items": "最大項目數", + "name-query": "查詢", + "name-recently-viewed": "最近瀏覽", + "name-search": "搜尋", + "name-show-folder-names": "顯示資料夾名稱", + "name-show-headings": "顯示標題", + "name-starred": "已加星號", + "name-tags": "標籤" }, "data-source-list": { "empty-state": { @@ -6220,15 +6470,16 @@ "dimensions": { "background-size-editor": { "options": { - "label-contain": "", - "label-cover": "", - "label-fill": "", - "label-original": "", - "label-tile": "" + "label-contain": "包含", + "label-cover": "覆蓋", + "label-fill": "填充", + "label-original": "原始", + "label-tile": "並排顯示" } }, "color-dimension-editor": { - "label-fixed-color": "" + "label-fixed-color": "固定色彩", + "noOptionsMessage-no-fields-found": "" }, "file-dropzone-custom-children": { "upload": "上傳" @@ -6242,10 +6493,10 @@ "placeholder-search": "搜尋" }, "resource-dimension-editor": { - "description-field": "", - "description-fixed": "", + "description-field": "使用字串欄位結果", + "description-fixed": "固定值", "label-field": "欄位", - "label-fixed": "", + "label-fixed": "固定", "label-mappings": "對應", "label-source": "來源" }, @@ -6262,27 +6513,34 @@ }, "scalar-dimension-editor": { "fixed-value-options": { - "label-fixed-values": "" + "label-fixed-values": "固定值" }, "label-limit": "限制", "label-value": "數值", + "noOptionsMessage-no-fields-found": "", "scalar-options": { - "description-clamped": "", - "description-mod": "", - "label-clamped": "", - "label-mod": "" + "description-clamped": "使用欄位值,固定最大值和最小值", + "description-mod": "使用欄位值,從最大值模組化", + "label-clamped": "已固定", + "label-mod": "模組" } }, "scale-dimension-editor": { + "fixed-value-option": { + "label": { + "fixed-value": "" + } + }, "label-max": "最大", "label-min": "最小", - "label-value": "數值" + "label-value": "數值", + "noOptionsMessage-no-fields-found": "" }, "text-dimension-editor": { - "description-field": "", - "description-fixed": "", + "description-field": "顯示欄位值", + "description-fixed": "固定值", "label-field": "欄位", - "label-fixed": "", + "label-fixed": "固定", "label-source": "來源", "label-template": "範本", "label-value": "數值" @@ -6296,16 +6554,16 @@ "description-percentage-means-thresholds-relative": "百分比表示相對於最小值和最大值的閾值", "modes": { "description": { - "threshold-based-percent-between-minmax": "", - "thresholds-based-absolute-values": "" + "threshold-based-percent-between-minmax": "根據最小/最大值之間的百分比選擇閾值", + "thresholds-based-absolute-values": "根據絕對值選擇閾值" }, "label": { - "absolute": "", - "percentage": "" + "absolute": "絕對值", + "percentage": "百分比" } }, "thresholds-mode": "閾值模式", - "value-base": "" + "value-base": "基數" }, "urlpicker-tab": { "label-preview": "預覽" @@ -6848,6 +7106,8 @@ "aria-label-select-service-name-operator": "選取服務名稱運算子", "aria-label-select-span-name": "選取範圍名稱", "aria-label-select-span-name-operator": "選擇範圍名稱運算子", + "ariaLabel-select-max-span-duration": "", + "ariaLabel-select-min-span-duration": "", "label-collapse": "範圍篩選器", "label-duration": "持續時間", "label-service-name": "服務名稱", @@ -6918,6 +7178,8 @@ "split-widen": "擴大窗格" }, "trace-page-actions": { + "ariaLabel-copy-trace-id": "", + "ariaLabel-export-trace": "", "give-feedback": "提供意見回饋", "label-copied": "已複製!", "label-export": "匯出", @@ -7025,6 +7287,7 @@ "when": "當" }, "expression-query-editor": { + "helper-text-sql": "", "label-operation": "操作" }, "math": { @@ -7055,6 +7318,7 @@ }, "folder-filter": { "clear-folder-button": "清除資料夾", + "noOptionsMessage-no-folders-found": "", "select-aria-label": "資料夾篩選條件", "select-placeholder": "按資料夾篩選" }, @@ -7100,30 +7364,76 @@ "title": "要離開頁面嗎?" }, "gauge": { - "category-gauge": "", - "description-min-height": "", - "description-min-width": "", - "description-neutral": "", - "description-show-threshold-labels": "", - "description-show-threshold-markers": "", + "category-gauge": "儀表", + "description-min-height": "最小列高(橫向)", + "description-min-width": "最小欄寬(縱向)", + "description-neutral": "留空以使用最小值作為中性點", + "description-show-threshold-labels": "在儀表列周圍渲染閾值", + "description-show-threshold-markers": "將閾值渲染為外部列", "gauge-size-options": { - "label-auto": "", - "label-manual": "" + "label-auto": "自動", + "label-manual": "手動" }, - "name-gauge-size": "", - "name-min-height": "", - "name-min-width": "", - "name-neutral": "", - "name-show-threshold-labels": "", - "name-show-threshold-markers": "", - "placeholder-neutral": "" + "name-gauge-size": "儀表大小", + "name-min-height": "最小高度", + "name-min-width": "最小寬度", + "name-neutral": "中性", + "name-show-threshold-labels": "顯示閾值標籤", + "name-show-threshold-markers": "顯示閾值標記", + "placeholder-neutral": "自動" }, "gen-ai": { "apply-suggestion": "套用", "incomplete-request-error": "很抱歉,我無法完成您的請求。請再試一次。", "send-custom-feedback": "傳送" }, + "geo": { + "get-geometry-field": { + "warning-gazetteer-not-found": "", + "warning-no-geometry": "", + "warning-select-geohash": "", + "warning-select-lat-long": "", + "warning-select-lookup": "", + "warning-unable-to-find": "" + }, + "location-editor": { + "geohash-field": { + "no-fields-message": "" + }, + "latitude-field": { + "no-fields-message": "" + }, + "longitude-field": { + "no-fields-message": "" + }, + "lookup-field": { + "no-fields-message": "" + }, + "name-gazetteer": "", + "name-geohash-field": "", + "name-latitude-field": "", + "name-location-mode": "", + "name-longitude-field": "", + "name-lookup-field": "" + }, + "location-more-editor": { + "mode-options": { + "description-auto": "", + "description-coords": "", + "description-geohash": "", + "description-lookup": "", + "label-auto": "", + "label-coords": "", + "label-geohash": "", + "label-lookup": "" + } + } + }, "geomap": { + "category-basemap-layer": "", + "category-map-controls": "", + "category-map-layers": "", + "category-map-view": "", "coordinates-map-view-editor": { "label-latitude": "緯度", "label-longitude": "經度" @@ -7132,6 +7442,14 @@ "center": "置中:", "zoom": "放大:" }, + "description-initial-view": "", + "description-mouse-wheel-zoom": "", + "description-share-view": "", + "description-show-attribution": "", + "description-show-debug": "", + "description-show-measure": "", + "description-show-scale": "", + "description-show-zoom": "", "fit-map-view-editor": { "all-layers-editor-fragment": { "label-layer": "層次" @@ -7154,6 +7472,14 @@ "geomap-style-rules-editor": { "aria-label-add-geomap-style-rule": "新增地圖樣式規則" }, + "layer-editor": { + "category-base-layer": "", + "description-display-tooltip": "", + "name-data": "", + "name-display-tooltip": "", + "name-layer-type": "", + "name-opacity": "" + }, "layers-editor": { "label-add-layer": "新增圖層", "no-layers": "沒有圖層嗎?" @@ -7164,16 +7490,38 @@ "label-zoom": "縮放", "use-current-map-settings": "使用目前的地圖設定" }, + "markers-layer": { + "description-show-legend": "", + "name-show-legend": "", + "name-styles": "" + }, "markers-legend": { "title-symbol": "符號" }, "measure-overlay": { "tooltip-show-measure-tools": "顯示測量工具" }, + "name-initial-view": "", + "name-mouse-wheel-zoom": "", + "name-share-view": "", + "name-show-attribution": "", + "name-show-debug": "", + "name-show-measure": "", + "name-show-scale": "", + "name-show-zoom": "", + "name-tooltip": "", + "photos-layer": { + "noFieldsMessage-no-string-fields": "" + }, "plugin": { "basemap-layer-configured-server-admin": "底圖圖層由伺服器管理員設定。" }, "style-editor": { + "horizontal-align-options": { + "label-center": "", + "label-left": "", + "label-right": "" + }, "label-align": "對齊", "label-baseline": "基準線", "label-color": "顏色", @@ -7187,7 +7535,14 @@ "label-symbol-vertical-align": "符號垂直對齊", "label-text-label": "文字標籤", "label-x-offset": "X 軸偏移", - "label-y-offset": "Y 軸偏移" + "label-y-offset": "Y 軸偏移", + "placeholderText-select-symbol": "", + "placeholderText-select-symbol-or-add-text": "", + "vertical-align-options": { + "label-bottom": "", + "label-center": "", + "label-top": "" + } }, "style-rule-editor": { "aria-label-comparison-operator": "比較運算子", @@ -7198,6 +7553,15 @@ "placeholder-feature-property": "功能屬性", "placeholder-numeric-value": "數值", "placeholder-value": "值" + }, + "tooltip-options": { + "description-details": "", + "description-none": "", + "label-details": "", + "label-none": "" + }, + "utils": { + "get-next-layer-name": "" } }, "get-enterprise": { @@ -7350,85 +7714,85 @@ }, "builder": { "axis": { - "category-axis": "", - "color-label": "", + "category-axis": "軸", + "color-label": "色彩", "color-options": { - "label-series": "", - "label-text": "" + "label-series": "系列", + "label-text": "文字" }, "grid-line-options": { - "label-auto": "", - "label-off": "", - "label-on": "" + "label-auto": "自動", + "label-off": "關閉", + "label-on": "開啟" }, - "name-centered-zero": "", - "name-grid-lines": "", - "name-label": "", - "name-placement": "", - "name-scale": "", - "name-show-border": "", - "name-soft-max": "", - "name-soft-min": "", - "name-width": "", - "placeholder-label": "", - "placeholder-soft-max": "", - "placeholder-soft-min": "", - "placeholder-width": "", + "name-centered-zero": "置中零", + "name-grid-lines": "顯示網格線", + "name-label": "標籤", + "name-placement": "放置", + "name-scale": "縮放", + "name-show-border": "顯示邊框", + "name-soft-max": "軟最大值", + "name-soft-min": "軟最小值", + "name-width": "寬度", + "placeholder-label": "可選文字", + "placeholder-soft-max": "查看:標準選項 > 最大", + "placeholder-soft-min": "查看:標準選項 > 最小", + "placeholder-width": "自動", "scale-distribution-editor": { "distribution-options": { - "label-linear": "", - "label-log": "", - "label-symlog": "" + "label-linear": "線性", + "label-log": "對數", + "label-symlog": "半對數" } } }, "legend": { - "category": "", - "description-values": "", + "category": "圖例", + "description-values": "選擇要在圖例中顯示的值或計算", "mode-options": { - "label-list": "", - "label-table": "" + "label-list": "清單", + "label-table": "表格" }, - "name-mode": "", - "name-placement": "", - "name-values": "", - "name-visibility": "", - "name-width": "", + "name-mode": "模式", + "name-placement": "放置", + "name-values": "值", + "name-visibility": "可見性", + "name-width": "寬度", "placement-options": { - "label-bottom": "", - "label-right": "" + "label-bottom": "底部", + "label-right": "右側" } }, "stacking": { - "name-stack-series": "" + "name-stack-series": "堆疊序列" }, "text": { - "category-text-size": "", - "name-percent-change": "", - "name-title": "", - "name-value": "", - "placeholder-percent-change": "", - "placeholder-title": "", - "placeholder-value": "" + "category-text-size": "文字大小", + "name-percent-change": "百分比變化", + "name-title": "標題", + "name-value": "值", + "placeholder-percent-change": "自動", + "placeholder-title": "自動", + "placeholder-value": "自動" }, "tooltip": { - "category": "", - "description-hover-proximity": "", + "category": "工具提示", + "description-hover-proximity": "游標必須距離某個點多近(以像素為單位)才能觸發工具提示", "modeOptions": { - "label-all": "", - "label-hidden": "", - "label-single": "" + "label-all": "全部", + "label-hidden": "隱藏", + "label-single": "單一" }, - "name-hide-zeros": "", - "name-hover-proximity": "", - "name-max-height": "", - "name-max-width": "", - "name-tooltip-mode": "", - "name-values-sort-order": "", + "name-hide-zeros": "隱藏零", + "name-hover-proximity": "懸停鄰近度", + "name-max-height": "最大高度", + "name-max-width": "最大寬度", + "name-tooltip-mode": "工具提示模式", + "name-values-sort-order": "值排序順序", "sortOptions": { - "label-ascending": "", - "label-descending": "", - "label-none": "" + "label-ascending": "升序", + "label-descending": "降序", + "label-none": "無" } } }, @@ -7447,7 +7811,8 @@ "aria-label-selected-color": "{{colorLabel}} 顏色" }, "confirm-button": { - "cancel": "取消" + "cancel": "取消", + "confirmText-delete": "" }, "confirm-content": { "placeholder": "輸入「{{confirmPromptText}}」以確認" @@ -7589,19 +7954,19 @@ "tooltip-expand-all": "展開所有列" }, "matchers-ui": { - "description-field-name-by-regex-matcher": "", - "description-fields-by-query": "", - "description-fields-with-name": "", - "description-fields-with-type": "", - "description-fields-with-value": "", + "description-field-name-by-regex-matcher": "為名稱符合正則表達式的欄位設定屬性", + "description-fields-by-query": "為來自特定查詢的欄位設定屬性", + "description-fields-with-name": "為特定欄位設定屬性", + "description-fields-with-type": "為特定類型(數字、字串、布爾值)的欄位設定屬性", + "description-fields-with-value": "為具有縮減條件的欄位設定屬性", "field-name-picker": { - "placeholder": "" + "placeholder": "選擇欄位" }, - "name-field-name-by-regex-matcher": "", - "name-fields-by-query": "", - "name-fields-with-name": "", - "name-fields-with-type": "", - "name-fields-with-value": "" + "name-field-name-by-regex-matcher": "名稱與正則表達式相符的欄位", + "name-fields-by-query": "查詢傳回的欄位", + "name-fields-with-name": "具有名稱的欄位", + "name-fields-with-type": "具有類型的欄位", + "name-fields-with-value": "具有值的欄位" }, "menu-item": { "keyboard-shortcut-label": "鍵盤捷徑" @@ -7629,6 +7994,8 @@ }, "panel-chrome": { "aria-label-toggle-collapse": "切換收闔面板", + "ariaLabel-panel-loading": "", + "ariaLabel-panel-status": "", "tooltip-cancel": "取消查詢", "tooltip-cancel-loading": "取消查詢", "tooltip-stop-streaming": "停止串流", @@ -7695,6 +8062,9 @@ "filter-popup-match-case": "區分大小寫", "inspect-drawer-title": "檢查值", "inspect-menu-label": "檢查值", + "nested-table": { + "no-data": "" + }, "no-values-label": "沒有數值", "pagination-summary": "{{itemsRangeStart}} - 第 {{displayedEnd}} 列,共 {{numRows}} 列" }, @@ -7703,6 +8073,7 @@ }, "tags-input": { "add": "新增", + "placeholder-new-tag": "", "remove": "移除標籤:{{name}}" }, "time-sync-button": { @@ -7719,55 +8090,55 @@ "config": { "get-graph-field-options": { "axis-placement": { - "description-auto": "", - "label-auto": "", - "label-hidden": "", - "label-left": "", - "label-right": "" + "description-auto": "第一個欄位在左側,其他欄位在右側", + "label-auto": "自動", + "label-hidden": "隱藏", + "label-left": "左側", + "label-right": "右側" }, "bar-alignment": { - "description-after": "", - "description-before": "", - "description-center": "" + "description-after": "之後", + "description-before": "之前", + "description-center": "置中" }, "draw-style": { - "label-bars": "", - "label-lines": "", - "label-points": "" + "label-bars": "長條", + "label-lines": "線", + "label-points": "點" }, "fill-gradient": { - "description-hue": "", - "description-opacity": "", - "description-scheme": "", - "label-hue": "", - "label-none": "", - "label-opacity": "", - "label-scheme": "" + "description-hue": "微幅色調漸變", + "description-opacity": "啟用填充不透明度漸變", + "description-scheme": "使用配色方案定義漸變", + "label-hue": "色度", + "label-none": "無", + "label-opacity": "不透明度", + "label-scheme": "方案" }, "line-interpolation": { - "description-linear": "", - "description-smooth": "", - "description-step-after": "", - "description-step-before": "" + "description-linear": "線性", + "description-smooth": "平滑", + "description-step-after": "後續步驟", + "description-step-before": "之前的步驟" }, "show-points": { - "description-auto": "", - "label-always": "", - "label-auto": "", - "label-never": "" + "description-auto": "密度低時顯示點", + "label-always": "總是", + "label-auto": "自動", + "label-never": "從不" }, "stacking": { - "label-100": "", - "label-normal": "", - "label-off": "" + "label-100": "100%", + "label-normal": "一般", + "label-off": "關閉" }, "thresholds-display-mode": { - "label-dashed-lines": "", - "label-filled-regions": "", - "label-filled-regions-and-dashed-lines": "", - "label-filled-regions-and-lines": "", - "label-lines": "", - "label-off": "" + "label-dashed-lines": "作為線條(虛線)", + "label-filled-regions": "作為填充的區域", + "label-filled-regions-and-dashed-lines": "作為填充的區域和線條(虛線)", + "label-filled-regions-and-lines": "作為填充的區域和線條", + "label-lines": "作為線條", + "label-off": "關閉" } } } @@ -7796,6 +8167,12 @@ "footer-click-to-action": "點選以{{actionTitle}}", "footer-click-to-navigate": "點選以開啟「{{linkTitle}}」", "timestamp": "時間戳記" + }, + "week-start-picker": { + "weekStarts-label-default": "", + "weekStarts-label-monday": "", + "weekStarts-label-saturday": "", + "weekStarts-label-sunday": "" } }, "graph": { @@ -7808,78 +8185,78 @@ }, "heatmap": { "calculate-from-data-options": { - "label-no": "", - "label-yes": "" + "label-no": "否", + "label-yes": "是" }, - "category-cell-display": "", - "category-colors": "", - "category-exemplars": "", - "category-heatmap": "", - "category-legend": "", - "category-tooltip": "", - "category-y-axis": "", + "category-cell-display": "儲存格顯示", + "category-colors": "色彩", + "category-exemplars": "範例", + "category-heatmap": "熱圖", + "category-legend": "圖例", + "category-tooltip": "工具提示", + "category-y-axis": "Y 軸", "mode-options": { - "label-opacity": "", - "label-scheme": "" + "label-opacity": "不透明度", + "label-scheme": "方案" }, - "name-axis-label": "", - "name-axis-width": "", - "name-calculate-from-data": "", - "name-cell-gap": "", - "name-color": "", - "name-decimals": "", - "name-end-color-at-value": "", - "name-exponent": "", - "name-hide-cells-gt": "", - "name-hide-cells-lt": "", - "name-max-height": "", - "name-max-value": "", - "name-max-width": "", - "name-min-value": "", - "name-mode": "", - "name-placement": "", - "name-reverse": "", - "name-scale": "", - "name-scheme": "", - "name-show-color-scale": "", - "name-show-histogram": "", - "name-show-legend": "", - "name-start-color-from-value": "", - "name-steps": "", - "name-tick-alignment": "", - "name-tooltip-mode": "", - "name-unit": "", - "name-value-name": "", - "name-y-axis-scale": "", - "placeholder-axis-label": "", - "placeholder-axis-width": "", - "placeholder-decimals": "", - "placeholder-end-color-at-value": "", - "placeholder-hide-cells-gt": "", - "placeholder-hide-cells-lt": "", - "placeholder-max-value": "", - "placeholder-min-value": "", - "placeholder-start-color-from-value": "", - "placeholder-value-name": "", + "name-axis-label": "軸標籤", + "name-axis-width": "軸寬度", + "name-calculate-from-data": "從資料計算", + "name-cell-gap": "儲存格間距", + "name-color": "色彩", + "name-decimals": "小數位數", + "name-end-color-at-value": "在指定數值處結束色彩刻度", + "name-exponent": "指數", + "name-hide-cells-gt": "隱藏值大於等於下列值的儲存格", + "name-hide-cells-lt": "隱藏值小於等於下列值的儲存格", + "name-max-height": "最大高度", + "name-max-value": "最大值", + "name-max-width": "最大寬度", + "name-min-value": "最小值", + "name-mode": "模式", + "name-placement": "放置", + "name-reverse": "反向", + "name-scale": "縮放", + "name-scheme": "方案", + "name-show-color-scale": "顯示色階", + "name-show-histogram": "顯示直方圖(Y 軸)", + "name-show-legend": "顯示圖例", + "name-start-color-from-value": "從指定數值開始色彩刻度", + "name-steps": "步驟", + "name-tick-alignment": "刻度對齊", + "name-tooltip-mode": "工具提示模式", + "name-unit": "單位", + "name-value-name": "值名稱", + "name-y-axis-scale": "Y 軸刻度", + "placeholder-axis-label": "自動", + "placeholder-axis-width": "自動", + "placeholder-decimals": "自動", + "placeholder-end-color-at-value": "自動(最大值)", + "placeholder-hide-cells-gt": "無", + "placeholder-hide-cells-lt": "無", + "placeholder-max-value": "自動", + "placeholder-min-value": "自動", + "placeholder-start-color-from-value": "自動(最小值)", + "placeholder-value-name": "值", "placement-options": { - "label-hidden": "", - "label-left": "", - "label-right": "" + "label-hidden": "隱藏", + "label-left": "左側", + "label-right": "右側" }, "scale-options": { - "label-exponential": "", - "label-linear": "" + "label-exponential": "指數", + "label-linear": "線性" }, "tick-alignment-options": { - "label-auto": "", - "label-bottom": "", - "label-middle": "", - "label-top": "" + "label-auto": "自動", + "label-bottom": "底部 (GE)", + "label-middle": "中間", + "label-top": "頂端 (LE)" }, "tooltip-mode-options": { - "label-all": "", - "label-hidden": "", - "label-single": "" + "label-all": "全部", + "label-hidden": "隱藏", + "label-single": "單一" } }, "help-modal": { @@ -7939,14 +8316,14 @@ "troubleshooting-help": "若要請求疑難排解協助,請將此面板的快照傳送至 Grafana Labs 技術支援。快照包含查詢回應資料和面板設定。" }, "histogram": { - "category-histogram": "", + "category-histogram": "直方圖", "histogram-panel": { "no-histogram-found-in-response": "回應中找不到直方圖" }, - "name-fill-opacity": "", - "name-gradient-mode": "", - "name-line-width": "", - "name-stacking": "" + "name-fill-opacity": "填充不透明度", + "name-gradient-mode": "漸變模式", + "name-line-width": "線寬", + "name-stacking": "堆疊" }, "inspector": { "inspect-data-tab": { @@ -8172,6 +8549,10 @@ "add-library-panel-modal": { "title-create-library-panel": "建立資料庫面板" }, + "change-library-panel-modal": { + "confirmText-change": "", + "confirmText-replace": "" + }, "confirm": { "delete-panel": "要刪除此面板嗎?" }, @@ -8194,7 +8575,7 @@ "usage-count_other": "用於 {{count}} 個儀表板" }, "library-panels-search": { - "placeholder-search-by-name-or-description": "按名稱或描述搜尋" + "placeholder-search-by-name-or-description": "" }, "loading-indicator": { "loading-library-panel": "正在載入資料庫面板…" @@ -8242,9 +8623,7 @@ "share-panel": { "config-description": "使用以下自訂設定,建立個人化的直接連結,以便在組織內分享面板:", "download-image": "下載圖片", - "render-image": "產生圖片", - "render-image-error": "無法呈現面板圖片", - "render-image-error-description": "產生圖片時發生錯誤" + "render-image": "產生圖片" } }, "live": { @@ -8320,19 +8699,28 @@ } }, "logs": { - "category-logs": "", + "category-logs": "紀錄", "deduplication-options": { - "label-exact": "", - "label-none": "", - "label-numbers": "", - "label-signature": "" + "label-exact": "完全符合", + "label-none": "無", + "label-numbers": "數字", + "label-signature": "簽名" + }, + "description-enable-infinite-scrolling": "實驗性。捲動至紀錄清單底部,以請求更多結果。", + "description-enable-syntax-highlighting": "運用預先定義的語法著色規則,來突顯紀錄行中的相關部分", + "description-show-controls": "顯示控制項以跳至最後或第一個紀錄行,並按紀錄層級篩選", + "fields": { + "type": { + "loki": { + "indexed-label_other": "", + "parsedl-label_other": "", + "structured-metadata_other": "" + } + } }, - "description-enable-infinite-scrolling": "", - "description-enable-syntax-highlighting": "", - "description-show-controls": "", "font-size-options": { - "label-default": "", - "label-small": "" + "label-default": "預設", + "label-small": "小" }, "get-dataframe-fields": { "vars": { @@ -8353,7 +8741,6 @@ "label-wrap-lines": "換行" }, "log-details": { - "close": "關閉紀錄詳細資訊", "fields": "欄位", "links": "連結", "log-line": "日誌行", @@ -8379,6 +8766,42 @@ "show-more": "顯示更多", "tooltip-error": "錯誤:{{errorMessage}}" }, + "log-line-details": { + "clear-search": "清除", + "close": "關閉紀錄詳細資訊", + "copy-shortlink": "複製短連結", + "copy-to-clipboard": "複製至剪貼簿", + "displayed-fields-section": "", + "fields": { + "adhoc-statistics": "隨機統計資料", + "copy-value-to-clipboard": "將此值複製到剪貼簿", + "filter-out": "篩選出值", + "filter-out-query": "篩選查詢 {{query}} 中的值", + "toggle-field-button": { + "field-instead-message": "顯示此欄位而非訊息", + "hide-this-field": "隱藏此欄位" + } + }, + "fields-section": "欄位", + "hide-log-line": "隱藏紀錄行", + "inline-mode": "", + "links-section": "連結", + "log-line-field": "", + "log-line-section": "紀錄行", + "move-displayed-field-down": "", + "move-displayed-field-up": "", + "no-details": "沒有要顯示的欄位。", + "pin-line": "固定紀錄", + "remove-displayed-field": "", + "search": { + "no-results": "沒有要顯示的結果。" + }, + "search-placeholder": "搜尋欄位名稱和值", + "show-context": "顯示前後文", + "show-log-line": "顯示紀錄行", + "sidebar-mode": "", + "unpin-line": "取消固定紀錄" + }, "log-line-menu": { "copy-link": "將連結複製到日誌行", "copy-log": "複製日誌行", @@ -8390,12 +8813,12 @@ "unpin-from-outline": "取消釘選日誌" }, "log-list-search": { - "close": "", - "filter": "", - "info": "", - "input-placeholder": "", - "next": "", - "prev": "" + "close": "關閉搜尋", + "filter": "篩選相符的紀錄", + "info": "在顯示的紀錄中進行用戶端字串搜尋。不要與查詢篩選條件混淆。使用此元件在紀錄結果中搜尋特定字串。", + "input-placeholder": "在紀錄中搜尋", + "next": "下一個結果", + "prev": "上一個結果" }, "log-row-context-modal": { "error-loading-log-more-logs": "載入更多紀錄時發生錯誤。", @@ -8450,7 +8873,7 @@ "escape-newlines": "修復紀錄行中新行和分頁序列的錯誤轉義", "font-size-default": "使用較小的字型", "font-size-small": "使用預設的字型大小", - "hide-search": "", + "hide-search": "關閉搜尋", "hide-timestamps": "隱藏時間戳記", "hide-unique-labels": "隱藏唯一標籤", "newest-first": "按最新紀錄排序 - 按一下以顯示最舊紀錄", @@ -8459,7 +8882,7 @@ "remove-escaping": "移除轉義", "scroll-bottom": "滾動到底部", "scroll-top": "捲動回頂部", - "show-search": "", + "show-search": "在紀錄結果中搜尋", "show-timestamps": "顯示時間戳記", "show-unique-labels": "顯示唯一標籤", "unwrap-lines": "取消換行", @@ -8477,21 +8900,26 @@ "common-labels": "通用標籤:" } }, - "name-common-labels": "", - "name-deduplication": "", - "name-enable-infinite-scrolling": "", - "name-enable-log-details": "", - "name-enable-syntax-highlighting": "", - "name-font-size": "", - "name-order": "", - "name-prettify-json": "", - "name-show-controls": "", - "name-time": "", - "name-unique-labels": "", - "name-wrap-lines": "", + "name-common-labels": "通用標籤", + "name-deduplication": "重複", + "name-details-mode": "", + "name-details-options": { + "label-inline": "", + "label-sidebar": "" + }, + "name-enable-infinite-scrolling": "啟用無限捲動", + "name-enable-log-details": "啟用紀錄詳細資訊", + "name-enable-syntax-highlighting": "啟用語法突顯", + "name-font-size": "字型大小", + "name-order": "順序", + "name-prettify-json": "美化 JSON", + "name-show-controls": "顯示控制項", + "name-time": "時間", + "name-unique-labels": "唯一標籤", + "name-wrap-lines": "換行", "order-options": { - "label-newest-first": "", - "label-oldest-first": "" + "label-newest-first": "最新排前", + "label-oldest-first": "最舊排前" }, "out-of-range-message": { "end-of-the-selected-time-range": "所選時間範圍的結束。" @@ -8505,6 +8933,7 @@ "un-themed-log-details": { "aria-label-data-links": "資料連結", "aria-label-fields": "欄位", + "aria-label-line": "紀錄行", "aria-label-log-level": "紀錄層級", "aria-label-no-details": "無詳細資料" }, @@ -8525,27 +8954,27 @@ } }, "logs-new": { - "category-logs": "", + "category-logs": "紀錄", "deduplication-options": { - "label-exact": "", - "label-none": "", - "label-numbers": "", - "label-signature": "" + "label-exact": "完全符合", + "label-none": "無", + "label-numbers": "數字", + "label-signature": "簽名" }, - "description-infinite-scrolling": "", - "description-show-controls": "", - "description-syntax-highlighting": "", - "name-deduplication": "", - "name-enable-log-details": "", - "name-infinite-scrolling": "", - "name-order": "", - "name-show-controls": "", - "name-syntax-highlighting": "", - "name-time": "", - "name-wrap-lines": "", + "description-infinite-scrolling": "實驗性。捲動至紀錄清單底部,以請求更多結果。", + "description-show-controls": "顯示控制項以跳至最後或第一個紀錄行,並按紀錄層級篩選", + "description-syntax-highlighting": "運用預先定義的語法著色規則,來突顯紀錄行中的相關部分", + "name-deduplication": "重複", + "name-enable-log-details": "啟用紀錄詳細資訊", + "name-infinite-scrolling": "啟用無限捲動", + "name-order": "順序", + "name-show-controls": "顯示控制項", + "name-syntax-highlighting": "啟用語法突顯", + "name-time": "時間", + "name-wrap-lines": "換行", "order-options": { - "label-newest-first": "", - "label-oldest-first": "" + "label-newest-first": "最新排前", + "label-oldest-first": "最舊排前" } }, "manage-dashbaords": { @@ -8572,6 +9001,8 @@ "updated-on": "更新日期" }, "snapshot-list-table": { + "body-delete": "", + "confirmText-delete": "", "title-delete": "刪除" }, "unthemed-dashboard-import": { @@ -8581,8 +9012,17 @@ "text": { "import-dashboard": "匯入儀表板" } + }, + "validation-srv": { + "message-name-required": "", + "message-reserved-name": "", + "message-same-name": "", + "message-same-name-general": "" } }, + "metric-select": { + "noOptionsMessage-no-options-found": "" + }, "migrate-to-cloud": { "build-snapshot": { "description": "此工具可以將某些資源從此安裝移轉至您的雲端堆疊。若要開始,您需要建立此安裝的快照。建立快照通常只需不到兩分鐘。快照與此 Grafana 安裝一起儲存。", @@ -8731,6 +9171,8 @@ "resource-details": { "dismiss-button": "確定", "error-messages": { + "alert-rules-group-quota-reached": "", + "alert-rules-quota-reached": "", "dashboard-already-managed": "儀表板已由 Grafana 在雲端執行個體中佈建和管理。我們建議您將來使用已設定的儀表板。若您仍然希望將儀表板複製至雲端執行個體,請變更儀表板 JSON 中的儀表板 ID,儲存新的快照並重新上傳。", "datasource-already-managed": "資料來源已由 Grafana 在雲端執行個體中佈建和管理。如果這是不同的資源,請設定另一個 UID 並再試一次。", "datasource-invalid-url": "有一個資料來源的網址無效。請提供有效的網址,然後再試一次。", @@ -9230,12 +9672,12 @@ "rss-button": "部落格的最新消息" }, "news": { - "category-news": "", - "description-show-image": "", - "description-url": "", + "category-news": "新聞", + "description-show-image": "控制新聞項目社群 (og:image) 圖片是否顯示在文字內容上方", + "description-url": "支援 RSS 和 Atom 資訊來源", "link-title": "前往 Grafana labs 部落格", - "name-show-image": "", - "name-url": "", + "name-show-image": "顯示圖片", + "name-url": "URL", "news-panel": { "body-error-loading-rss-feed": "請確認動態消息網址輸入正確,且已在伺服器上正確設定 CORS。請參閱<2>新聞面板文件。", "loading": "正在載入…", @@ -9244,27 +9686,27 @@ "title": "部落格的最新消息" }, "node-graph": { - "category-edges": "", - "category-node-graph": "", - "category-nodes": "", + "category-edges": "邊緣", + "category-node-graph": "節點圖表", + "category-nodes": "節點", "layout-algorithm-options": { - "description-force": "", - "description-grid": "", - "description-layered": "", - "label-force": "", - "label-grid": "", - "label-layered": "" + "description-force": "使用力導向版面配置", + "description-grid": "使用網格版面配置", + "description-layered": "使用分層版面配置", + "label-force": "力", + "label-grid": "網格", + "label-layered": "分層" }, - "name-arc-sections": "", - "name-layout-algorithm": "", - "name-main-stat-unit": "", - "name-secondary-stat-unit": "", - "name-zoom-mode": "", + "name-arc-sections": "弧形部分", + "name-layout-algorithm": "版面配置演算法", + "name-main-stat-unit": "主要統計資料單元", + "name-secondary-stat-unit": "次要統計資料單元", + "name-zoom-mode": "縮放模式", "zoom-mode-options": { - "description-cooperative": "", - "description-greedy": "", - "label-cooperative": "", - "label-greedy": "" + "description-cooperative": "可讓您正常捲動頁面", + "description-greedy": "對所有縮放手勢做出反應", + "label-cooperative": "合作", + "label-greedy": "貪婪" } }, "nodeGraph": { @@ -9340,40 +9782,40 @@ }, "registry": { "get-all-option-editors": { - "description-time-zone": "", - "name-time-zone": "" + "description-time-zone": "時區選擇", + "name-time-zone": "時區" }, "standard-field-condigs": { - "category-data-links": "" + "category-data-links": "資料連結和動作" }, "standard-field-configs": { - "category": "", - "category-thresholds": "", - "category-value-mappings": "", - "description-display-name": "", - "description-field-min-max": "", - "description-max": "", - "description-min": "", - "description-no-value": "", - "description-value-mappings": "", - "name-actions": "", - "name-ad-hoc": "", - "name-color-scheme": "", - "name-data-links": "", - "name-decimals": "", - "name-display-name": "", - "name-field-min-max": "", - "name-max": "", - "name-min": "", - "name-no-value": "", - "name-thresholds": "", - "name-unit": "", - "name-value-mappings": "", - "placeholder-decimals": "", - "placeholder-display-name": "", - "placeholder-max": "", - "placeholder-min": "", - "placeholder-unit": "" + "category": "標準選項", + "category-thresholds": "閾值", + "category-value-mappings": "值對應", + "description-display-name": "變更欄位或序列名稱", + "description-field-min-max": "計算每個欄位的最小值和最大值", + "description-max": "留空以根據所有值計算", + "description-min": "留空以根據所有值計算", + "description-no-value": "沒有數值時顯示的內容", + "description-value-mappings": "根據輸入值修改顯示文字", + "name-actions": "動作", + "name-ad-hoc": "臨時可篩選", + "name-color-scheme": "配色方案", + "name-data-links": "資料連結", + "name-decimals": "小數位數", + "name-display-name": "顯示名稱", + "name-field-min-max": "欄位最小/最大值", + "name-max": "最大", + "name-min": "最小", + "name-no-value": "無值", + "name-thresholds": "閾值", + "name-unit": "單位", + "name-value-mappings": "值對應", + "placeholder-decimals": "自動", + "placeholder-display-name": "無", + "placeholder-max": "自動", + "placeholder-min": "自動", + "placeholder-unit": "無" } }, "units": { @@ -9418,6 +9860,7 @@ } }, "org-picker": { + "noOptionsMessage-no-organizations-found": "", "select-placeholder": "選取組織" }, "page": { @@ -9481,13 +9924,13 @@ }, "panel-data-error-view": { "missing-value": { - "number": "", - "string": "", - "time": "", - "unknown": "" + "number": "資料缺少數字欄位", + "string": "資料缺少字串欄位", + "time": "資料缺少時間欄位", + "unknown": "無法視覺化資料" }, "no-value": { - "default": "" + "default": "沒有資料" }, "open-visualization-suggestions": "開啟可視化建議", "switch-to-table": "切換到表格" @@ -9527,25 +9970,25 @@ "select-placeholder": "按類型篩選" }, "piechart": { - "category-legend": "", - "category-pie-chart": "", - "description-labels": "", - "description-pie-chart-type": "", + "category-legend": "圖例", + "category-pie-chart": "圓餅圖", + "description-labels": "選擇要在圓餅圖中顯示的標籤", + "description-pie-chart-type": "應如何呈現圓餅圖", "labels-options": { - "label-name": "", - "label-percent": "", - "label-value": "" + "label-name": "名稱", + "label-percent": "百分比", + "label-value": "值" }, "legend-values-options": { - "label-percent": "", - "label-value": "" + "label-percent": "百分比", + "label-value": "值" }, - "name-labels": "", - "name-legend-values": "", - "name-pie-chart-type": "", + "name-labels": "標籤", + "name-legend-values": "圖例值", + "name-pie-chart-type": "圓餅圖類型", "pie-chart-type-options": { - "label-donut": "", - "label-pie": "" + "label-donut": "甜甜圈圖", + "label-pie": "圓餅圖" } }, "playlist": { @@ -9640,6 +10083,7 @@ "permission": "您沒有查看此頁面的權限。", "title-access-denied": "拒絕存取" }, + "error-loading-plugin": "", "no-root-app-page-component-found": "找不到根應用程式頁面元件" }, "browse": { @@ -9683,8 +10127,7 @@ "update-status-text": "外掛程式已更新" }, "versions": { - "confirmation-text-1": "您確定要降級至版本嗎", - "confirmation-text-2": "通常不應這麼做", + "confirmation-text": "", "downgrade-confirm": "降級", "downgrade-title": "降級外掛程式版本" } @@ -9713,7 +10156,7 @@ "dependencies": "依存關係", "documentation": "文件", "downloads": "下載量", - "from": "", + "from": "自", "installedVersion": "安裝版本", "lastCommitDate": "上次提交日期:", "latestReleaseDate": "最新發佈日期:", @@ -9738,6 +10181,10 @@ "empty-state": { "message": "未找到外掛程式" }, + "extensions": { + "extension-error-alert-description": "", + "extension-error-alert-title": "" + }, "extensions-log-data-source": { "message": { "ok": "確定" @@ -9771,11 +10218,15 @@ "updating": "更新中" }, "install-controls-button": { - "title-uninstall-modal": "解除安裝 {{plugin}}" + "title-uninstall-modal": "解除安裝 {{plugin}}", + "uninstall-controls": { + "body-uninstall-plugin": "", + "confirmText-confirm": "" + } }, "install-controls-warning": { "body-not-published": "此外掛程式未發佈到 <2>grafana.com/plugins,無法透過目錄管理。", - "enterprise-plugin-info": "", + "enterprise-plugin-info": "此外掛程式僅適用於 Grafana Cloud 和 Grafana Enterprise。", "learn-more": "了解更多資訊", "no-valid-grafana-enterprise-license-detected": "未偵測到有效的 Grafana 企業版授權。", "title-dev-alert": "這是外掛程式的開發版本,無法解除安裝。", @@ -9933,15 +10384,24 @@ }, "provisioned-resource-form": { "save-or-delete-resource-shared-fields": { - "comment-placeholder-describe-changes-optional": "", - "description-branch-name-in-git-hub": "", + "comment-placeholder-describe-changes-optional": "新增備註以描述您的變更(選填)", + "description-branch-name-in-git-hub": "GitHub 中的分支名稱", "description-inside-repository": "", - "label-branch": "", - "label-comment": "", - "label-path": "", - "label-workflow": "" + "label-branch": "分支", + "label-comment": "評論", + "label-path": "路徑", + "label-workflow": "工作流程" } }, + "provisioned-resource-preview-banner": { + "preview-banner": { + "not-saved": "", + "open-pull-request-in-git-hub": "", + "view-pull-request-in-git-hub": "" + }, + "title-created-branch-git-hub": "", + "title-loaded-pull-request-git-hub": "" + }, "provisioning": { "banner": { "message": "此功能目前正在積極開發中。為了獲得最佳體驗和最新改進版本,建議使用 Grafana 的<2>夜間版本。" @@ -10802,6 +11262,7 @@ } }, "service-account-picker": { + "noOptionsMessage-no-service-accounts-found": "", "select-aria-label": "服務帳戶選擇器", "select-placeholder": "開始輸入以搜尋服務帳戶" }, @@ -10847,6 +11308,10 @@ }, "service-account-page-unconnected": { "add-service-account-token": "新增服務帳戶權杖", + "body-delete-service-account": "", + "body-disable-service-account": "", + "confirmText-delete-service-account": "", + "confirmText-disable-service-account": "", "delete-service-account": "刪除服務帳戶", "disable-service-account": "停用服務帳戶", "enable-service-account": "啟用服務帳戶", @@ -10873,6 +11338,7 @@ "used-by": "使用者" }, "service-account-profile-row": { + "confirmText-save": "", "edit": "編輯" }, "service-account-role-row": { @@ -10886,10 +11352,15 @@ }, "service-accounts-list-page-unconnected": { "add-service-account": "新增服務帳戶", + "body-delete-with-tokens": "", + "body-disable-service-account": "", + "confirmText-delete": "", + "confirmText-disable-service-account": "", "placeholder-search-service-account-by-name": "按名稱搜尋服務帳戶", "sub-title": "服務帳戶及其權杖可用於針對 Grafana API 進行驗證。請在我們的<2>文件中閱讀更多資訊。", "title-delete-service-account": "刪除服務帳戶", - "title-disable-service-account": "停用服務帳戶" + "title-disable-service-account": "停用服務帳戶", + "body-delete_other": "" }, "token-expiration": { "content-this-token-has-expired": "此權杖已過期", @@ -10903,6 +11374,7 @@ }, "share-dashboard": { "menu": { + "export-image-title": "", "export-json-title": "匯出為 JSON", "share-externally-title": "外部分享", "share-internally-title": "內部分享", @@ -10938,6 +11410,19 @@ "view-button": "檢視 JSON", "view-button-yaml": "檢視 YAML" }, + "image": { + "actions": "", + "cancel-button": "", + "download-button": "", + "error-title": "", + "generate-button": "", + "generating": "", + "info-text": "", + "preview": "", + "preview-aria": "", + "preview-region": "", + "title": "" + }, "library": { "info": "建立資料庫面板。" }, @@ -10946,7 +11431,7 @@ "info-text": "建立指向此儀表板或面板的直接連結,並使用以下選項自訂。", "link-url": "連結網址", "render-alert": "未安裝圖片轉譯器外掛程式", - "render-instructions": "若要呈現面板圖像,您必須安裝 <2>Grafana 圖像渲染器外掛程式。請聯絡您的 Grafana 管理員以安裝外掛程式。", + "render-instructions": "", "rendered-image": "直接連結轉譯的圖片", "save-alert": "儀表板未儲存", "save-dashboard": "若要呈現面板圖片,您必須先儲存儀表板。", @@ -11014,6 +11499,12 @@ } }, "share-panel-image": { + "disabled-help": "", + "download-disabled-help": "", + "error-title": "", + "form": { + "label": "" + }, "preview": { "title": "面板預覽" }, @@ -11173,112 +11664,112 @@ }, "stat": { "add-orientation-option": { - "description-orientation": "", - "name-orientation": "", + "description-orientation": "版面配置方向", + "name-orientation": "方向", "orientation-options": { - "label-auto": "", - "label-horizontal": "", - "label-vertical": "" + "label-auto": "自動", + "label-horizontal": "橫向", + "label-vertical": "縱向" } }, "add-standard-data-reduce-options": { - "category-value-options": "", - "description-calculation": "", - "description-fields": "", - "description-limit": "", - "description-show": "", + "category-value-options": "值選項", + "description-calculation": "選擇一個歸約函數/計算方式", + "description-fields": "選擇應包含在面板中的欄位", + "description-limit": "可顯示的最大列數", + "description-show": "計算每欄或系列的單一值,或顯示每列", "fields-options": { - "label-all-fields": "", - "label-numeric-fields": "" + "label-all-fields": "所有欄位", + "label-numeric-fields": "數值欄位" }, - "name-calculation": "", - "name-fields": "", - "name-limit": "", - "name-show": "", + "name-calculation": "計算", + "name-fields": "欄位", + "name-limit": "限制", + "name-show": "顯示", "show-options": { - "label-all-values": "", - "label-calculate": "" + "label-all-values": "所有值", + "label-calculate": "計算" } }, - "category-stat-styles": "", + "category-stat-styles": "統計樣式", "color-mode-options": { - "label-background-gradient": "", - "label-background-solid": "", - "label-none": "", - "label-value": "" + "label-background-gradient": "漸層背景", + "label-background-solid": "實體背景", + "label-none": "無", + "label-value": "值" }, - "description-graph-mode": "", - "description-text-mode": "", + "description-graph-mode": "統計面板圖表/迷你趨勢圖模式", + "description-text-mode": "控制是否顯示名稱和數值,還是僅顯示名稱", "graph-mode": { "options": { - "label-area": "", - "label-none": "" + "label-area": "區域", + "label-none": "無" } }, "name-color-modcolor-mode-options": { - "label": "" + "label": "彩色模式" }, - "name-graph-mode": "", - "name-show-percent-change": "", - "name-text-alignment": "", - "name-text-mode": "", - "name-wide-layout": "", - "percent-change-color-mode": "", + "name-graph-mode": "圖表模式", + "name-show-percent-change": "顯示百分比變化", + "name-text-alignment": "文字對齊", + "name-text-mode": "文字模式", + "name-wide-layout": "寬版面", + "percent-change-color-mode": "百分比變化色彩模式", "percent-change-color-mode-options": { - "label-inverted": "", - "label-same-as-value": "", - "label-standard": "" + "label-inverted": "顛倒", + "label-same-as-value": "與值相同", + "label-standard": "標準" }, "text-alignment-options": { - "label-auto": "", - "label-center": "" + "label-auto": "自動", + "label-center": "置中" }, "text-mode-options": { - "label-auto": "", - "label-name": "", - "label-none": "", - "label-value": "", - "label-value-and-name": "" + "label-auto": "自動", + "label-name": "名稱", + "label-none": "無", + "label-value": "值", + "label-value-and-name": "值和名稱" }, "wide-layout-options": { - "label-off": "", - "label-on": "" + "label-off": "關閉", + "label-on": "開啟" } }, "state-timeline": { "align-values-options": { - "label-center": "", - "label-left": "", - "label-right": "" + "label-center": "置中", + "label-left": "靠左", + "label-right": "右側" }, - "category-state-timeline": "", - "name-align-values": "", - "name-connect-null-values": "", - "name-disconnect-values": "", - "name-fill-opacity": "", - "name-line-width": "", - "name-merge-equal-consecutive-values": "", - "name-page-size": "", - "name-row-height": "", - "name-show-values": "", + "category-state-timeline": "狀態時間軸", + "name-align-values": "對齊值", + "name-connect-null-values": "連接空值", + "name-disconnect-values": "斷開值", + "name-fill-opacity": "填充不透明度", + "name-line-width": "線寬", + "name-merge-equal-consecutive-values": "合併相等的連續值", + "name-page-size": "頁面大小(啟用分頁)", + "name-row-height": "列高", + "name-show-values": "顯示值", "show-values-options": { - "label-always": "", - "label-auto": "", - "label-never": "" + "label-always": "總是", + "label-auto": "自動", + "label-never": "從不" } }, "status-history": { - "category-status-history": "", - "name-column-width": "", - "name-fill-opacity": "", - "name-line-width": "", - "name-page-size": "", - "name-row-height": "", - "name-show-values": "", + "category-status-history": "狀態歷史紀錄", + "name-column-width": "欄寬", + "name-fill-opacity": "填充不透明度", + "name-line-width": "線寬", + "name-page-size": "頁面大小(啟用分頁)", + "name-row-height": "列高", + "name-show-values": "顯示值", "show-values-options": { - "label-always": "", - "label-auto": "", - "label-never": "" + "label-always": "總是", + "label-auto": "自動", + "label-never": "從不" }, "status-history-panel": { "too-many-points_other": "資料點過多,無法正確顯示。<1>更新查詢以回傳較少的資料點。<3>(已收到 {{count}} 點)" @@ -11317,13 +11808,13 @@ "label-gauge-display-mode": "儀表顯示模式", "label-value-display": "數值顯示" }, - "category-cell-options": "", - "category-table": "", - "category-table-footer": "", + "category-cell-options": "儲存格選項", + "category-table": "表格", + "category-table-footer": "表格頁尾", "cell-height-options": { - "label-large": "", - "label-medium": "", - "label-small": "" + "label-large": "大", + "label-medium": "中", + "label-small": "小" }, "color-background-cell-options-editor": { "description-apply-to-entire-row": "若選取,整列將以此儲存格的顏色顯示。", @@ -11337,82 +11828,84 @@ "wrap-text": "文字自動換行" }, "column-alignment-options": { - "label-auto": "", - "label-center": "", - "label-left": "", - "label-right": "" + "label-auto": "自動", + "label-center": "置中", + "label-left": "靠左", + "label-right": "右側" }, "container": { "content": "在單個表格中顯示太多欄位可能會影響效能,並使資料難以讀取。請考慮縮小您的查詢範圍。", "show-all-series": "顯示所有列", "show-only-series": "僅顯示 {{MAX_NUMBER_OF_COLUMNS}} 個欄位" }, - "description-calculation": "", - "description-cell-value-inspect": "", - "description-column-filter": "", - "description-count-rows": "", - "description-fields": "", - "description-min-column-width": "", + "description-calculation": "選擇一個歸約函數/計算方式", + "description-cell-value-inspect": "在模式視窗中啟用儲存格值檢查", + "description-column-filter": "啟用/停用表格中的欄位篩選條件", + "description-count-rows": "顯示所有資料列的單一計數", + "description-fields": "選擇應計算的欄位", + "description-min-column-width": "欄自動調整大小的最小寬度", + "description-wrap-header-text": "", "image-cell-options-editor": { "description-alt-text": "若圖片無法顯示或為使用螢幕閱讀器的使用者顯示的替代文字", "description-title-text": "滑鼠移至圖片上方時所顯示的文字", "label-alt-text": "替代文字", "label-title-text": "標題文字" }, - "name-calculation": "", - "name-cell-height": "", - "name-cell-type": "", - "name-cell-value-inspect": "", - "name-column-alignment": "", - "name-column-filter": "", - "name-column-width": "", - "name-count-rows": "", - "name-enable-paginations": "", - "name-fields": "", - "name-hide-in-table": "", - "name-min-column-width": "", - "name-show-table-footer": "", - "name-show-table-header": "", - "placeholder-column-width": "", - "placeholder-fields": "" + "name-calculation": "計算", + "name-cell-height": "儲存格高度", + "name-cell-type": "儲存格類型", + "name-cell-value-inspect": "儲存格值檢查", + "name-column-alignment": "欄對齊方式", + "name-column-filter": "欄篩選條件", + "name-column-width": "欄寬", + "name-count-rows": "計數列", + "name-enable-paginations": "啟用分頁", + "name-fields": "欄位", + "name-hide-in-table": "在表格中隱藏", + "name-min-column-width": "最小欄寬", + "name-show-table-footer": "顯示表格頁尾", + "name-show-table-header": "顯示表格頁首", + "name-wrap-header-text": "換行標題文字", + "placeholder-column-width": "自動", + "placeholder-fields": "所有數值欄位" }, "table-new": { - "category-cell-options": "", - "category-table": "", - "category-table-footer": "", + "category-cell-options": "儲存格選項", + "category-table": "表格", + "category-table-footer": "表格頁尾", "cell-height-options": { - "label-large": "", - "label-medium": "", - "label-small": "" + "label-large": "大", + "label-medium": "中", + "label-small": "小" }, "column-alignment-options": { - "label-auto": "", - "label-center": "", - "label-left": "", - "label-right": "" + "label-auto": "自動", + "label-center": "置中", + "label-left": "靠左", + "label-right": "右側" }, - "description-calculation": "", - "description-cell-value-inspect": "", - "description-column-filter": "", - "description-count-rows": "", - "description-fields": "", - "description-min-column-width": "", - "name-calculation": "", - "name-cell-height": "", - "name-cell-type": "", - "name-cell-value-inspect": "", - "name-column-alignment": "", - "name-column-filter": "", - "name-column-width": "", - "name-count-rows": "", - "name-enable-pagination": "", - "name-fields": "", - "name-hide-in-table": "", - "name-min-column-width": "", - "name-show-table-footer": "", - "name-show-table-header": "", - "placeholder-column-width": "", - "placeholder-fields": "" + "description-calculation": "選擇一個歸約函數/計算方式", + "description-cell-value-inspect": "在模式視窗中啟用儲存格值檢查", + "description-column-filter": "啟用/停用表格中的欄位篩選條件", + "description-count-rows": "顯示所有資料列的單一計數", + "description-fields": "選擇應計算的欄位", + "description-min-column-width": "欄自動調整大小的最小寬度", + "name-calculation": "計算", + "name-cell-height": "儲存格高度", + "name-cell-type": "儲存格類型", + "name-cell-value-inspect": "儲存格值檢查", + "name-column-alignment": "欄對齊方式", + "name-column-filter": "欄篩選條件", + "name-column-width": "欄寬", + "name-count-rows": "計數列", + "name-enable-pagination": "啟用分頁", + "name-fields": "欄位", + "name-hide-in-table": "在表格中隱藏", + "name-min-column-width": "最小欄寬", + "name-show-table-footer": "顯示表格頁尾", + "name-show-table-header": "顯示表格頁首", + "placeholder-column-width": "自動", + "placeholder-fields": "所有數值欄位" }, "tag-filter": { "clear-button": "清除標籤", @@ -11423,6 +11916,7 @@ "tag-option-label": "標記選項" }, "team-picker": { + "noOptionsMessage-no-teams-found": "", "select-aria-label": "團隊選擇器", "select-placeholder": "選擇團隊" }, @@ -11496,17 +11990,17 @@ } }, "text": { - "category-text": "", + "category-text": "文字", "mode-options": { - "label-code": "", - "label-html": "", - "label-markdown": "" + "label-code": "代碼", + "label-html": "HTML", + "label-markdown": "Markdown" }, - "name-content": "", - "name-language": "", - "name-mode": "", - "name-show-line-numbers": "", - "name-show-mini-map": "" + "name-content": "內容", + "name-language": "語言", + "name-mode": "模式", + "name-show-line-numbers": "顯示行號", + "name-show-mini-map": "顯示迷你地圖" }, "theme-preview": { "breadcrumbs": { @@ -11554,7 +12048,7 @@ "apply-button": "套用時間範圍", "default-error": "請輸入過去的日期或「{{now}}」", "fiscal-year": "會計年度:{{from}} - {{to}}", - "from-input": "", + "from-input": "自", "open-input-calendar": "開啟行事曆", "range-error": "「自」不能晚於「至」", "to-input": "至" @@ -11572,7 +12066,7 @@ "aria-role": "時間範圍選取", "default-title": "時間範圍", "example-title": "時間範圍範例", - "from-label": "", + "from-label": "自", "from-to": "{{timeOptionFrom}} 至 {{timeOptionTo}}", "specify": "指定時間範圍", "submit-button-label": "TimePicker 提交按鈕", @@ -11585,8 +12079,8 @@ }, "timeline": { "missing-field": { - "all": "", - "time": "" + "all": "無可繪圖欄位", + "time": "資料沒有時間欄位" } }, "timeseries": { @@ -11604,54 +12098,54 @@ "tooltip-delete": "刪除", "tooltip-edit": "編輯" }, - "category-axis": "", + "category-axis": "軸", "config": { "get-graph-field-config": { - "aria-label-bar-width-factor": "", - "aria-label-fill-opacity": "", - "aria-label-line-width": "", - "aria-label-point-size": "", - "category-graph-styles": "", - "category-thresholds": "", - "name-bar-alignment": "", - "name-bar-width-factor": "", - "name-connect-nulls": "", - "name-disconnect-values": "", - "name-fill-below-to": "", - "name-fill-opacity": "", - "name-gradient-mode": "", - "name-line-interpolation": "", - "name-line-style": "", - "name-line-width": "", - "name-point-size": "", - "name-show-points": "", - "name-show-thresholds": "", - "name-style": "", - "name-transform": "", + "aria-label-bar-width-factor": "長條寬度係數", + "aria-label-fill-opacity": "填充不透明度", + "aria-label-line-width": "線寬", + "aria-label-point-size": "點大小", + "category-graph-styles": "圖表樣式", + "category-thresholds": "閾值", + "name-bar-alignment": "長條對齊", + "name-bar-width-factor": "長條寬度係數", + "name-connect-nulls": "連接空值", + "name-disconnect-values": "斷開值", + "name-fill-below-to": "填滿下方至", + "name-fill-opacity": "填充不透明度", + "name-gradient-mode": "漸變模式", + "name-line-interpolation": "線條插值", + "name-line-style": "線條樣式", + "name-line-width": "線寬", + "name-point-size": "點大小", + "name-show-points": "顯示點", + "name-show-thresholds": "顯示閾值", + "name-style": "樣式", + "name-transform": "轉變", "transform-options": { - "description-constant": "", - "description-style": "", - "label-constant": "", - "label-style": "" + "description-constant": "第一個值將顯示為常數線", + "description-style": "將結果翻轉為 Y 軸上的負值", + "label-constant": "常數", + "label-style": "負 Y" } } }, "insert-nulls-editor": { "disconnect-options": { - "label-never": "", - "label-threshold": "" + "label-never": "從不", + "label-threshold": "閾值" } }, "line-style-editor": { "line-fill-options": { - "label-dash": "", - "label-dots": "", - "label-solid": "" + "label-dash": "虛線", + "label-dots": "點", + "label-solid": "實體" }, "title-the-input-expects-a-segment-list": "輸入欄位預期為區段清單", "tooltip-help": "說明" }, - "name-time-zone": "", + "name-time-zone": "時區", "nulls-threshold-input": { "placeholder-never": "從不" }, @@ -11661,9 +12155,9 @@ }, "span-nulls-editor": { "gaps-options": { - "label-always": "", - "label-never": "", - "label-threshold": "" + "label-always": "總是", + "label-never": "從不", + "label-threshold": "閾值" } }, "timezones-editor": { @@ -11672,17 +12166,17 @@ } }, "traces": { - "category-span-filters": "", - "name-critical-path-only": "", - "name-find-in-trace": "", - "name-max-duration": "", - "name-min-duration": "", - "name-service-name": "", - "name-service-name-operator": "", - "name-show-matches-only": "", - "name-span-name": "", - "name-span-name-operator": "", - "name-tags": "", + "category-span-filters": "範圍篩選器", + "name-critical-path-only": "僅顯示關鍵路徑", + "name-find-in-trace": "在追蹤中尋找", + "name-max-duration": "最大持續時間", + "name-min-duration": "最小持續時間", + "name-service-name": "服務名稱", + "name-service-name-operator": "服務名稱運算子", + "name-show-matches-only": "僅顯示符合項目", + "name-span-name": "範圍名稱", + "name-span-name-operator": "範圍名稱運算子", + "name-tags": "標籤", "traces-panel": { "no-data-found-in-response": "回應中找不到資料" } @@ -11697,20 +12191,20 @@ "axis-editor": { "log-mode-options": { "description": { - "split-the-buckets-based-on-size": "" + "split-the-buckets-based-on-size": "根據大小分割儲存貯體" }, "label": { - "split": "" + "split": "分割" } }, "mode-options": { "description": { - "split-the-buckets-based-on-count": "", - "split-the-buckets-based-on-size": "" + "split-the-buckets-based-on-count": "根據計數分割儲存貯體", + "split-the-buckets-based-on-size": "根據大小分割儲存貯體" }, "label": { - "count": "", - "size": "" + "count": "計數", + "size": "大小" } }, "placeholder-auto": "自動" @@ -11724,20 +12218,39 @@ "placeholder-fields-or-number": "欄位或數字" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "", + "reduce-row": "", + "row-index": "", + "unary-operation": "" + } + }, + "label": { + "cumulative-functions": "", + "window-functions": "" + }, "label-alias": "別名", "label-mode": "模式", "label-replace-all-fields": "取代所有欄位" }, "calculate-heatmap": { "add-heatmap-calculation-options": { - "name-x-bucket": "", - "name-y-bucket": "", - "name-y-bucket-scale": "" + "name-x-bucket": "X 儲存貯體", + "name-y-bucket": "Y 儲存貯體", + "name-y-bucket-scale": "Y 儲存貯體刻度" } }, "concatenate-transformer-editor": { "label-label": "標籤", "label-name": "名稱", + "name-modes": { + "label": { + "copy-frame-name-to-field": "", + "ignore-the-frame-name": "", + "label-frame": "" + } + }, "placeholder-frame": "畫面" }, "config-from-query-transformer-editor": { @@ -11748,6 +12261,10 @@ "convert-field-type-transformer-editor": { "aria-label-add-a-convert-field-type-transformer": "新增轉換欄位類型轉換器", "aria-label-remove-convert-field-type-transformer": "移除轉換欄位類型轉換器", + "convert-field-type": "", + "description": { + "convert-to-specified-field-type": "" + }, "label": { "browser": "瀏覽器", "utc": "UTC" @@ -11758,7 +12275,13 @@ "label-input-format": "輸入格式", "label-join-with": "合併方式", "label-set-timezone": "設定時區", + "name": { + "convert-field-type": "" + }, "placeholder-type": "類型", + "tags": { + "format-field": "" + }, "tooltip-explicit-separator-joining-array-values": "合併陣列值時使用明確的分隔符", "tooltip-input-format": "指定輸入欄位的格式,以便 Grafana 可以正確解析日期字串。", "tooltip-specify-the-output-format": "指定輸出格式。", @@ -11776,10 +12299,23 @@ "tooltip-enable": "啟用" }, "draggable-sort-order": { - "asc": "", - "desc": "", + "asc": "升序", + "desc": "降序", "off": "關閉" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "" + }, + "name": { + "concatenate-fields": "" + }, + "tags": { + "combine": "" + } + } + }, "enum-mapping-editor": { "add-enum-value": "新增列舉值", "generate-enum-values-from-data": "從資料產生列舉值" @@ -11790,11 +12326,22 @@ "remove-enum-row-tooltip-delete": "刪除" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "" + }, + "field-name-picker-settings": { + "placeholderText": { + "select-field": "" + } + }, "label-delimiter": "定界符", "label-format": "格式", "label-keep-time": "保持時間", "label-replace-all-fields": "取代所有欄位", "label-source": "來源", + "name": { + "extract-fields": "" + }, "placeholder-auto": "自動", "placeholder-select-delimiter": "選擇分隔符…", "tooltip-regexp": "範例:{{regexExample}}" @@ -11803,8 +12350,22 @@ "aria-label-threshold-color": "閾值顏色" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "" + }, + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "欄位", - "label-lookup": "查找" + "label-lookup": "查找", + "name": { + "lookup-fields-from-resource": "" + } }, "field-to-config-mapping-editor": { "additional-settings": "附加設定", @@ -11813,12 +12374,26 @@ "use-as": "使用為" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "" + }, "label-from-variable": "從變數", "label-identifier": "標識符", "label-use-variable": "使用變數", "label-variable": "變數", + "name": { + "filter-fields-by-name": "" + }, "placeholder-regular-expression-pattern": "規則運算式模式" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "" + }, + "name": { + "filter-data-by-query": "" + } + }, "filter-by-value-filter-editor": { "label-field": "欄位", "label-match": "符合", @@ -11828,14 +12403,48 @@ }, "filter-by-value-transformer-editor": { "add-condition": "新增條件", + "description": { + "remove-rows-query-results-user-defined-filters": "" + }, + "filter-match": { + "label": { + "match-all": "", + "match-any": "" + } + }, + "filter-types": { + "label": { + "exclude": "", + "include": "" + } + }, "label-conditions": "條件", - "label-filter-type": "篩選器類型" + "label-filter-type": "篩選器類型", + "name": { + "filter-data-by-values": "" + } }, "format-string-transfomer-editor": { + "field-name-picker-settings": { + "noFieldsMessage": { + "no-text-fields-found": "" + }, + "placeholderText": { + "select-text-field": "" + } + }, "label-field": "欄位", "label-format": "格式", "label-substring-range": "子字串範圍" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "" + }, + "name": { + "format-string": "" + } + }, "format-time-transfomer-editor": { "label-format": "格式", "label-set-timezone": "設定時區", @@ -11843,6 +12452,70 @@ "tooltip-format": "指定為 <2>Moment.js 格式字串的欄位輸出格式。", "tooltip-timezone-manually": "手動設定日期的時區" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "" + }, + "name": { + "format-time": "" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "" + }, + "name": { + "add-field-from-calculation": "" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "" + }, + "name": { + "config-from-query-results": "" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "" + }, + "name": { + "create-heatmap": "" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "" + }, + "name": { + "join-by-labels": "" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "" + }, + "name": { + "partition-by-values": "" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "" + }, + "name": { + "rows-to-fields": "" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "" + }, + "name": { + "spatial-operations": "" + } + }, "get-timezone-options": { "label": { "browser": "瀏覽器", @@ -11870,22 +12543,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "", + "group-by": "" + } + }, "placeholder-ignored": "已略過", "placeholder-select-stats": "選擇統計資料" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "" + }, + "name": { + "group-by": "" + }, "title-calc-alert": "如未將任何欄位分組,則計算將無效" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "" + }, "description-show-field-names": "若啟用,巢狀表格將顯示欄位名稱作為表格標題", "label-show-field-names-in-nested-tables": "在巢狀表格中顯示欄位名稱", + "name": { + "group-to-nested-tables": "" + }, "title-calc-alert": "如未將任何欄位分組,則計算將無效。" }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "" + }, "label-cell-value": "儲存格數值", "label-column": "欄位", "label-empty-value": "空白數值", "label-row": "列", + "name": { + "grouping-to-matrix": "" + }, "special-value-options": { "description": { "boolean-false-value": "布林假值", @@ -11904,6 +12601,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "" + }, + "name": { + "histogram": "" + }, "placeholder-auto": "自動", "placeholder-default": "預設值:30", "placeholder-none": "無" @@ -11912,7 +12615,16 @@ "label-as-percentile": "作為百分位數", "tooltip-transform-index-percentile": "將列索引轉換為百分位數。" }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "" + }, + "name": { + "join-by-field": "" + } + }, "join-by-labels-transformer-editor": { + "error-required": "", "info": { "input-contain-labels": "輸入內容不包含任何標籤", "no-input-or-labels-found": "找不到輸入內容(或標籤)", @@ -11934,10 +12646,30 @@ "label-labels": "標籤", "label-mode": "模式", "label-value-field-name": "值欄位名稱", + "modes": { + "label": { + "columns": "", + "rows": "" + } + }, "placeholder-optional-select-label": "(選填)選取標籤", "tooltip-replace-value-field-label": "使用標籤取代數值欄位名稱" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "" + }, + "name": { + "labels-to-fields": "" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "" + }, + "name": { + "limit": "" + }, "placeholder-value-or-variable": "值或變數" }, "long-info": { @@ -11948,7 +12680,13 @@ "string-values-separate": "字串值以單獨欄位而非標籤表示" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "套用到單個框架時,合併不起作用。" + "description": { + "merge-multiple-series": "" + }, + "merge-effect-applied-single-frame": "套用到單個框架時,合併不起作用。", + "name": { + "merge": "" + } }, "multi-info": { "all-values-are-numeric": "所有值都是數值", @@ -11958,10 +12696,16 @@ "time-in-ascending-order": "時間遞增排序" }, "organize-fields-transformer-editor": { - "field-order": "", + "description": { + "reorder-hide-or-rename-fields": "" + }, + "field-order": "欄位順序", "field-order-auto": "自動", - "field-order-manual": "", - "first-frame-warning": "組織欄位僅適用於單個框架。請考慮先套用加入轉換或篩選輸入。" + "field-order-manual": "手動", + "first-frame-warning": "組織欄位僅適用於單個框架。請考慮先套用加入轉換或篩選輸入。", + "name": { + "organize-fields": "" + } }, "partion-by-values-editor": { "tooltip-naming": "設定所選欄位名稱的顯示方式。因為框架名稱通常更適合表格資料" @@ -11986,9 +12730,41 @@ "partition-values-works-single-frame": "按值分割僅適用於單個框架。", "tooltip-keeps-partition-fields-frames": "在框架中保留分割欄位" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "" + }, + "name": { + "prepare-time-series": "" + } + }, "prepare-time-series-editor": { "label-format": "格式", - "label-info": "資訊" + "label-info": "資訊", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "" + }, + "label": { + "long-time-series": "" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "" + }, + "label": { + "multiframe-time-series": "" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "" + }, + "label": { + "wide-time-series": "" + } + } }, "range-matcher-editor": { "and": "和", @@ -12000,6 +12776,9 @@ "label-operation": "操作" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "" + }, "label-calculations": "計算", "label-include-time": "包含時間", "label-labels-to-fields": "欄位標籤", @@ -12014,12 +12793,23 @@ "series-to-rows": "將序列轉為列" } }, + "name": { + "reduce": "" + }, "placeholder-choose-stat": "選擇統計資料" }, "regex-matcher-editor": { "placeholder-value-or-variable": "值或變數", "variable-regex-documentation": "將多值變數格式化為正則表達式字串" }, + "regression": { + "description": { + "create-new-data-frame": "" + }, + "name": { + "regression-analysis": "" + } + }, "regression-transformer-editor": { "label": { "cubic": "立方", @@ -12038,9 +12828,17 @@ "polynomial": "多項式" } }, - "regression": "", + "regression": "回歸", "tooltip-number-of-xy-points-to-predict": "要預測的 X、Y 點數" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "" + }, + "name": { + "rename-fields-by-regex": "" + } + }, "rename-by-regex-transformer-editor": { "label-match": "符合", "label-replace": "取代", @@ -12049,11 +12847,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "欄位", - "label-mode": "模式" + "label-mode": "模式", + "modes": { + "description": { + "combine-data-from-two-tables": "", + "join-on-a-field": "", + "keep-all-rows": "" + }, + "label": { + "inner": "", + "outer-tabular": "", + "outer-time-series": "" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "" + }, + "name": { + "series-to-rows": "" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "" + }, "label-field": "欄位", "label-reverse": "反向", + "name": { + "sort-by": "" + }, "placeholder-select-field": "選擇欄位" }, "supplier": { @@ -12073,6 +12897,17 @@ "transform": "轉變" } }, + "time-series-table": { + "description": { + "convert-to-table-rows": "" + }, + "is-applicable-description": { + "requires-time-series-frame": "" + }, + "name": { + "time-series-to-table": "" + } + }, "time-series-table-transform-editor": { "label-stat": "統計資料", "label-time-field": "時間欄位", @@ -12086,10 +12921,34 @@ "placeholder-value": "數值", "tooltip-name-for-value-fields": "值欄位的名稱" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "" + }, + "name": { + "transpose": "" + }, + "tags": { + "pivot": "", + "transform": "", + "translate": "" + } + }, "unary-operation-editor": { "label-operation": "操作", "placeholder-field": "欄位" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "", + "combine": "", + "create-new-visualization": "", + "filter": "", + "perform-spatial-operations": "", + "reformat": "", + "reorder-and-rename": "" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "任何類型的多個數值欄位", "single-frame": "單一框架", @@ -12122,10 +12981,10 @@ } }, "trend": { - "category-x-axis": "", - "description-x-field": "", - "name-x-field": "", - "placeholder-x-field": "" + "category-x-axis": "X 軸", + "description-x-field": "遞增的數值", + "name-x-field": "X 欄位", + "placeholder-x-field": "第一個數值" }, "upgrade-box": { "discovery-text": "您已發現專業版功能!", @@ -12142,6 +13001,7 @@ "title": "組織" }, "user-picker": { + "noOptionsMessage-no-users-found": "", "select-aria-label": "使用者選擇器", "select-placeholder": "開始輸入以搜尋使用者" }, @@ -12227,6 +13087,8 @@ } }, "confirm-delete-modal": { + "body-delete-variable": "", + "confirmText-delete": "", "title-delete-variable": "刪除變數" }, "create-ad-hoc-variable-adapter": { @@ -12275,9 +13137,34 @@ "label-refresh": "重新整理" }, "query-variable-sort-select": { - "description-values-variable": "如何將此變數的值排序" + "description-values-variable": "如何將此變數的值排序", + "name-sort": "", + "sort_options": { + "label": { + "alphabetical-asc": "", + "alphabetical-caseinsensitive-asc": "", + "alphabetical-caseinsensitive-desc": "", + "alphabetical-desc": "", + "disabled": "", + "natural-asc": "", + "natural-desc": "", + "numerical-asc": "", + "numerical-desc": "" + } + } + }, + "query-variable-static-options": { + "add-option-button-label": "", + "description": "", + "label-placeholder": "", + "remove-option-button-label": "", + "value-placeholder": "" + }, + "query-variable-static-options-sort-select": { + "description-values-variable": "" }, "text-box-variable-editor": { + "name-default-value": "", "placeholder-default-value-if-any": "預設值(如有)", "text-options": "文字選項" }, @@ -12306,6 +13193,8 @@ "description-optional-display-name": "可選的顯示名稱", "description-template-variable-characters": "範本變數的名稱。(最多 50 個字元)", "general": "一般", + "name-label": "", + "name-name": "", "placeholder-descriptive-text": "描述性文字", "placeholder-label-name": "標籤名稱", "placeholder-variable-name": "變數名稱", @@ -12320,9 +13209,15 @@ "tooltip-duplicate-variable": "複製變數", "tooltip-remove-variable": "移除變數" }, + "variable-editor-un-connected": { + "name-description": "" + }, "variable-options": { "aria-label-toggle-all-values": "切換所有值" }, + "variable-type-select": { + "name-select-variable-type": "" + }, "variable-usages-button": { "tooltip-show-usages": "顯示使用情況" }, @@ -12352,20 +13247,20 @@ } }, "xychart": { - "category-xychart": "", - "name-fill-opacity": "", - "name-line-style": "", - "name-line-width": "", - "name-max-point-size": "", - "name-min-point-size": "", - "name-point-shape": "", - "name-point-size": "", - "name-point-stroke-width": "", - "name-series-mapping": "", - "name-show": "", + "category-xychart": "XY 散佈圖", + "name-fill-opacity": "填充不透明度", + "name-line-style": "線條樣式", + "name-line-width": "線寬", + "name-max-point-size": "最大點大小", + "name-min-point-size": "最小點大小", + "name-point-shape": "點形狀", + "name-point-size": "點大小", + "name-point-stroke-width": "點描邊寬度", + "name-series-mapping": "序列對應", + "name-show": "顯示", "point-shape-options": { - "label-circle": "", - "label-square": "" + "label-circle": "圓形", + "label-square": "正方形" }, "series-editor": { "add-series": "新增序列", @@ -12377,18 +13272,18 @@ "label-y-field": "Y 欄位", "placeholder-all-frames": "所有框架", "placeholder-select-frame": "選取框架", - "placeholder-x-field": "", - "placeholder-y-field": "", + "placeholder-x-field": "每個框架中的第一個數字或時間欄位", + "placeholder-y-field": "每個框架中的剩餘數字欄位", "tooltip-delete-series": "刪除序列" }, "series-mapping-options": { - "label-auto": "", - "label-manual": "" + "label-auto": "自動", + "label-manual": "手動" }, "show-options": { - "label-both": "", - "label-lines": "", - "label-points": "" + "label-both": "兩者", + "label-lines": "線", + "label-points": "點" } } } \ No newline at end of file diff --git a/public/openapi3.json b/public/openapi3.json index f44e48250d6..3a1e1c93c5b 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7360,6 +7360,8 @@ "properties": { "errorCode": { "enum": [ + "ALERT_RULES_QUOTA_REACHED", + "ALERT_RULES_GROUP_QUOTA_REACHED", "DATASOURCE_NAME_CONFLICT", "DATASOURCE_INVALID_URL", "DATASOURCE_ALREADY_MANAGED", @@ -12115,7 +12117,7 @@ "properties": { "Object": { "additionalProperties": {}, - "description": "Object is a JSON compatible map with string, float, int, bool, []interface{},\nor map[string]interface{} children.", + "description": "Object is a JSON compatible map with string, float, int, bool, []any,\nor map[string]any children.", "type": "object" } }, diff --git a/public/test/core/redux/reducerTester.ts b/public/test/core/redux/reducerTester.ts index 38c466afa2b..79e2fddbc2d 100644 --- a/public/test/core/redux/reducerTester.ts +++ b/public/test/core/redux/reducerTester.ts @@ -2,7 +2,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { cloneDeep } from 'lodash'; import { Action } from 'redux'; -import { StoreState } from 'app/types'; +import { StoreState } from 'app/types/store'; type GrafanaReducer = (state: S, action: A) => S; diff --git a/public/test/core/redux/reduxTester.ts b/public/test/core/redux/reduxTester.ts index 787ce13b59b..caf43717f21 100644 --- a/public/test/core/redux/reduxTester.ts +++ b/public/test/core/redux/reduxTester.ts @@ -3,7 +3,7 @@ import { Middleware, Store, StoreEnhancer, UnknownAction } from 'redux'; import { thunk, ThunkDispatch, ThunkMiddleware } from 'redux-thunk'; import { setStore } from '../../../app/store/store'; -import { StoreState } from '../../../app/types'; +import { StoreState } from '../../../app/types/store'; export interface ReduxTesterGiven { givenRootReducer: (rootReducer: Reducer>>) => ReduxTesterWhen; diff --git a/public/test/helpers/convertToStoreState.ts b/public/test/helpers/convertToStoreState.ts index 9f696b0e45a..92982147231 100644 --- a/public/test/helpers/convertToStoreState.ts +++ b/public/test/helpers/convertToStoreState.ts @@ -2,7 +2,7 @@ import { TypedVariableModel } from '@grafana/data'; import { getPreloadedState } from '../../app/features/variables/state/helpers'; import { VariablesState } from '../../app/features/variables/state/types'; -import { StoreState } from '../../app/types'; +import { StoreState } from '../../app/types/store'; export const convertToStoreState = (key: string, models: TypedVariableModel[]): StoreState => { const variables = models.reduce((byName, variable) => { diff --git a/public/test/helpers/getTemplateSrvDependencies.ts b/public/test/helpers/getTemplateSrvDependencies.ts index c1da0c4d247..d602d5aa03d 100644 --- a/public/test/helpers/getTemplateSrvDependencies.ts +++ b/public/test/helpers/getTemplateSrvDependencies.ts @@ -1,7 +1,7 @@ import { TemplateSrvDependencies } from 'app/features/templating/template_srv'; import { getFilteredVariables, getVariables, getVariableWithName } from '../../app/features/variables/state/selectors'; -import { StoreState } from '../../app/types'; +import { StoreState } from '../../app/types/store'; export const getTemplateSrvDependencies = (state: StoreState): TemplateSrvDependencies => ({ getFilteredVariables: (filter) => getFilteredVariables(filter, state), diff --git a/relyance.yaml b/relyance.yaml new file mode 100644 index 00000000000..41a798d27f2 --- /dev/null +++ b/relyance.yaml @@ -0,0 +1,99 @@ +# Relyance Compliance Inspector Configuration + +repository: grafana/grafana + +# Services and Code Relationship +# Purpose: Configuration used to visualize the code base and relationships between microservices and code. +# Note: Directory paths are relative to the repository's root directory. +# Note: Need a separate entry for each microservice. See example below. +services: + + + +# Example: Services and Code Relationship +# Note: Do not use below. It is an example. +# Note: Directory paths are relative to the repository's root directory +#- service: paymentservice +# source: +# - src/payments +# - src/checkout_service +#- service: catalogservice +# source: +# - src/catalog + + +# Source Directories to Exclude +# Purpose: Configuration is used to exclude directories from analysis such as test directories etc. +# Note: Directory paths are relative to the repository's root directory +exclude: + # Test and development environments + - e2e + - devenv + - hack + - scripts + - tools + - pkg/tests + + # Documentation and examples + - docs + - contribute + - grafana-mixin + + # Build artifacts and dependencies + - packaging + - data + - bin + - plugins-bundled + - vendor + - node_modules + + # Static UI assets + - public/fonts + - public/img + - public/locales + - public/vendor + - public/lib + + # Configuration templates + - conf + - emails + - cue.mod + - kinds + - kindsv2 + + # Temporary and cache directories + - tmp + - cache + - .cache + - venv + + +# Excluding files and directories using patterns (enable lines as needed) +exclude_patterns: + # Test and mock files + - "**/*_test.go" + - "**/*.test.ts" + - "**/*.spec.ts" + - "**/testdata/**" + - "**/*_mock.go" + - "**/mocks/**" + + # Generated code + - "**/*_gen.go" + - "**/wire_gen.go" + + # Documentation and examples + - "**/*.md" + - "**/README.*" + - "**/LICENSE*" + - "**/*.example.*" + + # System files and dependencies + - "**/.DS_Store" + - "**/*.log" + - "**/go.sum" + - "**/yarn.lock" + + # Generated specs + - "**/swagger.json" + - "**/openapi*.json" diff --git a/scripts/check-breaking-changes.sh b/scripts/check-breaking-changes.sh index 79fa3d01978..dc1e5741d6b 100755 --- a/scripts/check-breaking-changes.sh +++ b/scripts/check-breaking-changes.sh @@ -67,5 +67,10 @@ echo "message=$GITHUB_MESSAGE" >>"$GITHUB_OUTPUT" mkdir -p ./levitate echo "$GITHUB_LEVITATE_MARKDOWN" >./levitate/levitate.md +if [[ "$IS_FORK" == "true" ]]; then + cat ./levitate/levitate.md >> "$GITHUB_STEP_SUMMARY" + exit $EXIT_CODE +fi + # We will exit the workflow accordingly at another step exit 0 diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 50b23341467..63175726fd5 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -834,7 +834,7 @@ def playwright_e2e_tests_step(): "commands": [ "npx wait-on@7.0.1 http://$HOST:$PORT", "yarn playwright install --with-deps chromium", - "yarn e2e:playwright", + "yarn e2e:playwright --grep @plugins", ], } diff --git a/scripts/grafana-server/custom.ini b/scripts/grafana-server/custom.ini index 68968c502fa..d8e92d8782e 100644 --- a/scripts/grafana-server/custom.ini +++ b/scripts/grafana-server/custom.ini @@ -1,3 +1,8 @@ +app_mode = development + +[log] +level = error +filters = http.server:info,settings:info [security] content_security_policy = true diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 6162439a299..6a71bf6e19b 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -41,11 +41,11 @@ echo -e "Copying custom plugins from e2e tests" mkdir -p "$RUNDIR/data/plugins" -if [ -d "./e2e/test-plugins" ]; then - ln -s $(realpath ./e2e/test-plugins/*) "$RUNDIR/data/plugins" +if [ -d "./e2e-playwright/test-plugins" ]; then + ln -s $(realpath ./e2e-playwright/test-plugins/*) "$RUNDIR/data/plugins" # when running in CI -elif [ -d "../e2e/test-plugins" ]; then - cp -r "../e2e/test-plugins" "$RUNDIR/data/plugins" +elif [ -d "../e2e-playwright/test-plugins" ]; then + cp -r "../e2e-playwright/test-plugins" "$RUNDIR/data/plugins" fi if [ "$INSTALL_IMAGE_RENDERER" ]; then @@ -60,17 +60,17 @@ cp devenv/dashboards.yaml $PROV_DIR/dashboards cp devenv/alert_rules.yaml $PROV_DIR/alerting cp devenv/plugins.yaml $PROV_DIR/plugins -cp -r devenv $RUNDIR +cp -r devenv $ -echo -e "Starting Grafana Server port $PORT" +echo -e "Starting Grafana server port $PORT" -$RUNDIR/bin/"$ARCH"grafana-server \ - --homepath=$HOME_PATH \ - --pidfile=$RUNDIR/pid \ - cfg:server.http_port=$PORT \ - cfg:server.router_logging=1 \ - cfg:app_mode=development \ - cfg:enterprise.license_path=$1 - -# 2>&1 > $RUNDIR/output.log & -# cfg:log.level=debug \ +# We get a lot of "context canceled" errors from navigating away from pages, +# so filter them out because they're not useful +{ + exec "$RUNDIR/bin/${ARCH}grafana" server \ + --homepath="$HOME_PATH" \ + --pidfile="$RUNDIR/pid" \ + cfg:server.http_port="$PORT" \ + cfg:enterprise.license_path="$1" \ + 2>&1 +} | grep -v -i "context canceled" diff --git a/scripts/levitate-parse-json-report.js b/scripts/levitate-parse-json-report.js index 3bc62697de0..5cf92259910 100644 --- a/scripts/levitate-parse-json-report.js +++ b/scripts/levitate-parse-json-report.js @@ -4,6 +4,8 @@ const printAffectedPluginsSection = require('./levitate-show-affected-plugins'); const data = JSON.parse(fs.readFileSync('data.json', 'utf8')); +const isFork = Boolean(process.env.IS_FORK || false); + function stripAnsi(str) { return str.replace(/\x1b\[[0-9;]*m/g, ''); } @@ -30,7 +32,8 @@ if (data.changes.length > 0) { markdown += printSection('Changes', data.changes); } -if (data.removals.length > 0 || data.changes.length > 0) { +// The logic below would need access to secrets for accessing BigQuery, however that's not available on forks. +if ((data.removals.length > 0 || data.changes.length > 0) && !isFork) { markdown += printAffectedPluginsSection(data); } diff --git a/scripts/releasefinder.sh b/scripts/releasefinder.sh index fb5bb23a386..ddf407cf5e2 100755 --- a/scripts/releasefinder.sh +++ b/scripts/releasefinder.sh @@ -13,6 +13,9 @@ # # Example: ./scripts/releasefinder.sh a1b2c3d4e5f6 # +# Note: This script requires a full repository clone with all branches and tags. +# It will not work correctly with shallow clones (--depth) or single-branch clones. +# # If you get a "Permission denied" error, make the script executable with: # chmod +x scripts/releasefinder.sh @@ -46,93 +49,64 @@ fi echo "Fetching latest remote information..." git fetch --all --tags --prune 2>/dev/null -echo "Finding release branches containing the commit..." -echo "Finding tags associated with the commit..." -echo - -echo "Results for commit: $COMMIT_HASH" +echo "Finding releases containing commit: $COMMIT_HASH" echo "=============================================" echo -# Get commit details -echo "Commit details:" -echo " Author: $(git log -1 --format="%an <%ae>" "$COMMIT_HASH")" -echo " Date: $(git log -1 --format="%ad" --date=iso "$COMMIT_HASH")" +# Get all commit details in one call for better performance +commit_info=$(git log -1 --format="%an <%ae>%n%ad%n%B" --date=iso "$COMMIT_HASH") +author=$(echo "$commit_info" | sed -n '1p') +date=$(echo "$commit_info" | sed -n '2p') +commit_message=$(echo "$commit_info" | sed -n '3,$p') -# Extract original PR number and create link -PR_NUMBER=$(git log -1 --pretty=format:"%B" "$COMMIT_HASH" | grep -o '#[0-9]\+' | head -n1 | tr -d '#') +echo "Commit details:" +echo " Author: $author" +echo " Date: $date" + +# Extract PR number and title +PR_NUMBER=$(echo "$commit_message" | grep -o '#[0-9]\+' | head -n1 | tr -d '#') if [ -n "$PR_NUMBER" ]; then - # Extract PR title (first line of commit message) - PR_TITLE=$(git log -1 --pretty=format:"%s" "$COMMIT_HASH") + PR_TITLE=$(echo "$commit_message" | head -n1) echo " PR: #$PR_NUMBER - $PR_TITLE" echo " Link: https://github.com/grafana/grafana/pull/$PR_NUMBER" fi echo -# Arrays to store results -declare -a release_branches=() -declare -a direct_tags=() -declare -a included_tags=() +# Find release branches and tags containing the commit +release_branches=$(git branch -r --contains "$COMMIT_HASH" 2>/dev/null | grep -E 'origin/release-[0-9]+\.[0-9]+\.[0-9]+(\+security-[0-9]{2})?$' | sed 's/.*origin\///') +release_tags=$(git tag --contains "$COMMIT_HASH" 2>/dev/null | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(\+security-[0-9]{2})?$' | sort -V) -# First check all release branches (including security releases) -for branch in $(git branch -r | grep -E 'origin/release-[0-9]+\.[0-9]+\.[0-9]+(\+security-[0-9]{2})?$' | sed 's/origin\///'); do - # Check if the commit is in this branch's history - if git merge-base --is-ancestor "$COMMIT_HASH" "origin/$branch" 2>/dev/null; then - release_branches+=("$branch") - fi -done +# Get all existing tags for upcoming release filtering +all_tags=$(git tag 2>/dev/null | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(\+security-[0-9]{2})?$') -# Then check all version tags (including security releases) -for tag in $(git tag | sort -V); do - # Skip non-version tags - if ! [[ $tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+(\+security-[0-9]{2})?$ ]]; then - continue - fi - - # Check if the commit is in this tag - if git merge-base --is-ancestor "$COMMIT_HASH" "$tag" 2>/dev/null; then - # If this is the first tag containing the commit, it's the initial release tag - if [ ${#direct_tags[@]} -eq 0 ]; then - direct_tags+=("$tag") - else - included_tags+=("$tag") - fi - fi -done - -# Print previous releases if they exist -if [ ${#direct_tags[@]} -gt 0 ] || [ ${#included_tags[@]} -gt 0 ]; then +# Display previous releases +if [ -n "$release_tags" ]; then echo "This commit has been included in these PREVIOUS on-prem releases:" - # Get all tags sorted - readarray -t all_tags < <(printf "%s\n" "${direct_tags[@]}" "${included_tags[@]}" | sort -V) - # Get the first release - first_release="${all_tags[0]}" - # Print all tags with annotation for the first release - for tag in "${all_tags[@]}"; do + first_release=$(echo "$release_tags" | head -1) + while read -r tag; do if [ "$tag" = "$first_release" ]; then echo " - $tag (first release)" else echo " - $tag" fi - done + done <<< "$release_tags" echo echo "Note: This code may have been backported to previous release branches. Please check the original PR for backport information." echo fi -# Print upcoming releases -if [ ${#release_branches[@]} -eq 0 ]; then - echo " This commit is not yet included in any release branches." - echo " The corresponding release branch has likely not been created yet." -else +# Display upcoming releases +if [ -n "$release_branches" ]; then echo "This commit will be included in these UPCOMING on-prem releases:" - for branch in "${release_branches[@]}"; do - # Convert branch name to tag format (e.g., release-11.5.0 -> v11.5.0) + while read -r branch; do tag_version="v${branch#release-}" # Only show branches that don't have a corresponding tag yet - if ! git tag | grep -q "^$tag_version$"; then + if ! echo "$all_tags" | grep -q "^$tag_version$"; then echo " - $tag_version" fi - done | sort -V + done <<< "$release_branches" | sort -V +else + echo "This commit is not yet included in any release branches." + echo "The corresponding release branch has likely not been created yet." fi echo diff --git a/yarn.lock b/yarn.lock index 9439036c398..a86de0b5b0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1464,7 +1464,7 @@ __metadata: languageName: node linkType: hard -"@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.16.3, @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.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": +"@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.16.3, @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.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.27.6 resolution: "@babel/runtime@npm:7.27.6" checksum: 10/cc957a12ba3781241b83d528eb69ddeb86ca5ac43179a825e83aa81263a6b3eb88c57bed8a937cdeacfc3192e07ec24c73acdfea4507d0c0428c8e23d6322bfe @@ -2058,177 +2058,184 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/aix-ppc64@npm:0.25.0" +"@esbuild/aix-ppc64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/aix-ppc64@npm:0.25.6" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/android-arm64@npm:0.25.0" +"@esbuild/android-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/android-arm64@npm:0.25.6" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/android-arm@npm:0.25.0" +"@esbuild/android-arm@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/android-arm@npm:0.25.6" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/android-x64@npm:0.25.0" +"@esbuild/android-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/android-x64@npm:0.25.6" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/darwin-arm64@npm:0.25.0" +"@esbuild/darwin-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/darwin-arm64@npm:0.25.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/darwin-x64@npm:0.25.0" +"@esbuild/darwin-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/darwin-x64@npm:0.25.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/freebsd-arm64@npm:0.25.0" +"@esbuild/freebsd-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/freebsd-arm64@npm:0.25.6" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/freebsd-x64@npm:0.25.0" +"@esbuild/freebsd-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/freebsd-x64@npm:0.25.6" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-arm64@npm:0.25.0" +"@esbuild/linux-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-arm64@npm:0.25.6" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-arm@npm:0.25.0" +"@esbuild/linux-arm@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-arm@npm:0.25.6" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-ia32@npm:0.25.0" +"@esbuild/linux-ia32@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-ia32@npm:0.25.6" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-loong64@npm:0.25.0" +"@esbuild/linux-loong64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-loong64@npm:0.25.6" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-mips64el@npm:0.25.0" +"@esbuild/linux-mips64el@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-mips64el@npm:0.25.6" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-ppc64@npm:0.25.0" +"@esbuild/linux-ppc64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-ppc64@npm:0.25.6" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-riscv64@npm:0.25.0" +"@esbuild/linux-riscv64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-riscv64@npm:0.25.6" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-s390x@npm:0.25.0" +"@esbuild/linux-s390x@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-s390x@npm:0.25.6" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/linux-x64@npm:0.25.0" +"@esbuild/linux-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/linux-x64@npm:0.25.6" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/netbsd-arm64@npm:0.25.0" +"@esbuild/netbsd-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/netbsd-arm64@npm:0.25.6" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/netbsd-x64@npm:0.25.0" +"@esbuild/netbsd-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/netbsd-x64@npm:0.25.6" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/openbsd-arm64@npm:0.25.0" +"@esbuild/openbsd-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/openbsd-arm64@npm:0.25.6" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/openbsd-x64@npm:0.25.0" +"@esbuild/openbsd-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/openbsd-x64@npm:0.25.6" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/sunos-x64@npm:0.25.0" +"@esbuild/openharmony-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/openharmony-arm64@npm:0.25.6" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/sunos-x64@npm:0.25.6" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/win32-arm64@npm:0.25.0" +"@esbuild/win32-arm64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/win32-arm64@npm:0.25.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/win32-ia32@npm:0.25.0" +"@esbuild/win32-ia32@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/win32-ia32@npm:0.25.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.0": - version: 0.25.0 - resolution: "@esbuild/win32-x64@npm:0.25.0" +"@esbuild/win32-x64@npm:0.25.6": + version: 0.25.6 + resolution: "@esbuild/win32-x64@npm:0.25.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2335,48 +2342,48 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.0.0": - version: 1.6.2 - resolution: "@floating-ui/core@npm:1.6.2" +"@floating-ui/core@npm:^1.7.2": + version: 1.7.2 + resolution: "@floating-ui/core@npm:1.7.2" dependencies: - "@floating-ui/utils": "npm:^0.2.0" - checksum: 10/5c940ef3d397aa23f859ecb033bda408dde20820af3f82090a889c35a99826cfaa7864e8131b9906a26b2c04f31fa468538a28d0715b34de541e0776e0f82d03 + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/2885a4c824f8d148222714cf2650c29bf3b30b70f465b990734766aafb1366ba23b4a285918c4f3ee67161096e7b9e4fbe0b3ca31e0f78800338cb4e6292912d languageName: node linkType: hard -"@floating-ui/dom@npm:^1.0.0, @floating-ui/dom@npm:^1.0.1": - version: 1.6.5 - resolution: "@floating-ui/dom@npm:1.6.5" +"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.7.2": + version: 1.7.2 + resolution: "@floating-ui/dom@npm:1.7.2" dependencies: - "@floating-ui/core": "npm:^1.0.0" - "@floating-ui/utils": "npm:^0.2.0" - checksum: 10/d421e7f239e9af5a2a4c7a560c29b8ce1f29398c411c8e3bd0c33a2ce800e13a378749a1606e4f6b460830f4007c459792534821013262d24d1385476b1ba48d + "@floating-ui/core": "npm:^1.7.2" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/2d581459973b66024c47f039cabdc059b39c72abca93a4a0a8110e0a90b79d3fb84e49099afb73e13b3e7f17096070422220d996b206a5f0120c92bd7c48b98b languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.1.2, @floating-ui/react-dom@npm:^2.1.3": - version: 2.1.3 - resolution: "@floating-ui/react-dom@npm:2.1.3" +"@floating-ui/react-dom@npm:^2.1.2, @floating-ui/react-dom@npm:^2.1.4": + version: 2.1.4 + resolution: "@floating-ui/react-dom@npm:2.1.4" dependencies: - "@floating-ui/dom": "npm:^1.0.0" + "@floating-ui/dom": "npm:^1.7.2" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10/185223bb436f719943054f634ab5c885b32df745c7bc27504d77783944cfe02ce0f16a01d222d7abc511d3fc56fd36703ce6056eb2c4451b6666a7800b74a29f + checksum: 10/8fad23bd7a94b50bbd715109a272528978511b793b8954098c8e99dd0b63557a8641b315ce09301ef014eada560fd8638e4dea4c21cf7ce8d93f0038a59cd442 languageName: node linkType: hard -"@floating-ui/react@npm:0.27.12": - version: 0.27.12 - resolution: "@floating-ui/react@npm:0.27.12" +"@floating-ui/react@npm:0.27.13": + version: 0.27.13 + resolution: "@floating-ui/react@npm:0.27.13" dependencies: - "@floating-ui/react-dom": "npm:^2.1.3" - "@floating-ui/utils": "npm:^0.2.9" + "@floating-ui/react-dom": "npm:^2.1.4" + "@floating-ui/utils": "npm:^0.2.10" tabbable: "npm:^6.0.0" peerDependencies: react: ">=17.0.0" react-dom: ">=17.0.0" - checksum: 10/1bd397df1aca2a095d46085f8f8763eb7ade13997a73d5148b559709991737d575d9706612a56da8051081e52d5e441ccabcc3057c81395faf2cb871f3edc2b8 + checksum: 10/84a5247e74b7666c069680e8df69e0dcdbf846f6192760c1ec8c53361b2e316895bb43847fba92f6c82e563db45794aeb8755560e769c610ef4a1b956a52d105 languageName: node linkType: hard @@ -2394,10 +2401,10 @@ __metadata: languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.8, @floating-ui/utils@npm:^0.2.9": - version: 0.2.9 - resolution: "@floating-ui/utils@npm:0.2.9" - checksum: 10/0ca786347db3dd8d9034b86d1449fabb96642788e5900cc5f2aee433cd7b243efbcd7a165bead50b004ee3f20a90ddebb6a35296fc41d43cfd361b6f01b69ffb +"@floating-ui/utils@npm:^0.2.10, @floating-ui/utils@npm:^0.2.8": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae languageName: node linkType: hard @@ -2534,13 +2541,13 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" fast-deep-equal: "npm:^3.1.3" - i18next: "npm:^24.0.0" + i18next: "npm:^25.0.0" i18next-parser: "npm:9.3.0" immer: "npm:10.1.1" lodash: "npm:4.17.21" @@ -2548,13 +2555,13 @@ __metadata: prismjs: "npm:1.30.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2577,7 +2584,7 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2585,7 +2592,7 @@ __metadata: rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2607,7 +2614,7 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -2625,7 +2632,7 @@ __metadata: style-loader: "npm:4.0.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2648,7 +2655,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2658,12 +2665,12 @@ __metadata: micro-memoize: "npm:^4.1.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uuid: "npm:11.1.0" webpack: "npm:5.97.1" peerDependencies: @@ -2688,7 +2695,7 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/logfmt": "npm:^1.2.3" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" @@ -2699,13 +2706,13 @@ __metadata: logfmt: "npm:^1.3.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-window: "npm:1.8.11" rxjs: "npm:7.8.2" stream-browserify: "npm:3.0.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uuid: "npm:11.1.0" webpack: "npm:5.97.1" peerDependencies: @@ -2720,7 +2727,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:12.1.0-pre" "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/lezer-logql": "npm:0.2.7" + "@grafana/lezer-logql": "npm:0.2.8" "@grafana/llm": "npm:0.22.1" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/plugin-configs": "npm:12.1.0-pre" @@ -2732,7 +2739,7 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2742,7 +2749,7 @@ __metadata: micro-memoize: "npm:^4.1.2" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" @@ -2772,7 +2779,7 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" i18next-parser: "npm:9.3.0" @@ -2781,7 +2788,7 @@ __metadata: rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2804,7 +2811,7 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" lodash: "npm:4.17.21" @@ -2812,7 +2819,7 @@ __metadata: rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2832,7 +2839,7 @@ __metadata: "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" @@ -2844,7 +2851,7 @@ __metadata: rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2858,7 +2865,7 @@ __metadata: "@emotion/css": "npm:11.13.5" "@grafana/data": "npm:12.1.0-pre" "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/google-sdk": "npm:0.3.2" + "@grafana/google-sdk": "npm:0.3.4" "@grafana/plugin-configs": "npm:12.1.0-pre" "@grafana/plugin-ui": "npm:0.10.7" "@grafana/runtime": "npm:12.1.0-pre" @@ -2870,27 +2877,27 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" debounce-promise: "npm:3.1.2" fast-deep-equal: "npm:^3.1.3" - i18next: "npm:^24.0.0" + i18next: "npm:^25.0.0" immer: "npm:10.1.1" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.1" prismjs: "npm:1.30.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -2922,7 +2929,7 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/prismjs": "npm:1.26.5" "@types/react": "npm:18.3.18" @@ -2931,15 +2938,15 @@ __metadata: "@types/uuid": "npm:10.0.0" buffer: "npm:6.0.3" events: "npm:3.3.0" - glob: "npm:11.0.1" - i18next: "npm:^24.0.0" + glob: "npm:11.0.3" + i18next: "npm:^25.0.0" lodash: "npm:4.17.21" lru-cache: "npm:11.1.0" monaco-editor: "npm:0.34.1" prismjs: "npm:1.30.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-select-event: "npm:5.5.1" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" @@ -2948,7 +2955,7 @@ __metadata: string_decoder: "npm:1.3.0" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uuid: "npm:11.1.0" webpack: "npm:5.97.1" peerDependencies: @@ -2972,19 +2979,19 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.2.0" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" lodash: "npm:4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" peerDependencies: "@grafana/runtime": "*" @@ -3015,7 +3022,7 @@ __metadata: rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" type-fest: "npm:^4.40.0" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" peerDependencies: "@grafana/runtime": ">=11.6 <= 12.x" "@grafana/ui": ">=11.6 <= 12.x" @@ -3064,30 +3071,30 @@ __metadata: "@grafana/schema": "npm:12.1.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@leeoniya/ufuzzy": "npm:1.0.18" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" "@types/d3-interpolate": "npm:^3.0.0" "@types/history": "npm:4.7.11" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/papaparse": "npm:5.3.16" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" "@types/string-hash": "npm:1.1.3" - "@types/systemjs": "npm:6.15.1" + "@types/systemjs": "npm:6.15.3" "@types/tinycolor2": "npm:1.4.6" d3-interpolate: "npm:3.0.1" date-fns: "npm:4.1.0" dompurify: "npm:3.2.6" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" eventemitter3: "npm:5.0.1" fast_array_intersect: "npm:1.1.0" history: "npm:4.10.1" lodash: "npm:4.17.21" - marked: "npm:15.0.12" - marked-mangle: "npm:1.1.10" + marked: "npm:16.0.0" + marked-mangle: "npm:1.1.11" moment: "npm:2.30.1" moment-timezone: "npm:0.5.47" - ol: "npm:7.4.0" + ol: "npm:10.6.0" papaparse: "npm:5.5.3" react: "npm:18.3.1" react-dom: "npm:18.3.1" @@ -3100,7 +3107,7 @@ __metadata: string-hash: "npm:^1.1.3" tinycolor2: "npm:1.6.0" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uplot: "npm:1.6.32" xss: "npm:^1.0.14" peerDependencies: @@ -3114,17 +3121,17 @@ __metadata: resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: "@grafana/tsconfig": "npm:^2.0.0" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" "@types/node": "npm:22.15.0" "@types/semver": "npm:7.7.0" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" semver: "npm:^7.7.0" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" languageName: unknown linkType: soft @@ -3162,43 +3169,43 @@ __metadata: languageName: unknown linkType: soft -"@grafana/faro-core@npm:^1.13.2, @grafana/faro-core@npm:^1.18.2": - version: 1.18.2 - resolution: "@grafana/faro-core@npm:1.18.2" +"@grafana/faro-core@npm:^1.13.2, @grafana/faro-core@npm:^1.19.0": + version: 1.19.0 + resolution: "@grafana/faro-core@npm:1.19.0" dependencies: "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/otlp-transformer": "npm:^0.201.0" - checksum: 10/b211039b8b7381d2cb6bc8f80dc25d1c7ce484e426c6406bdcdd94c9e694b88387704b0408b1e11a0c5127dd9b1fc29544b24454b0e68f8458efb1a0166de657 + "@opentelemetry/otlp-transformer": "npm:^0.202.0" + checksum: 10/9bb3549074e86dea152469c558a3f126e231c068059a605f5afdf7c1bca1efd6f634a0c1bbbf068de1c1bd841d9c46f92b20490a1dd5a623451a38e73fab16e1 languageName: node linkType: hard -"@grafana/faro-web-sdk@npm:^1.13.2, @grafana/faro-web-sdk@npm:^1.18.2": - version: 1.18.2 - resolution: "@grafana/faro-web-sdk@npm:1.18.2" +"@grafana/faro-web-sdk@npm:^1.13.2, @grafana/faro-web-sdk@npm:^1.19.0": + version: 1.19.0 + resolution: "@grafana/faro-web-sdk@npm:1.19.0" dependencies: - "@grafana/faro-core": "npm:^1.18.2" + "@grafana/faro-core": "npm:^1.19.0" ua-parser-js: "npm:^1.0.32" web-vitals: "npm:^4.0.1" - checksum: 10/f2b5d0f794c5d493c51947380081771436ed49dca02b7e1ea292f966da2e861514dc2d9ed74d1da821e85c398fb66b45c8eda2f6f628c7d6e4bfc8f498074c93 + checksum: 10/a7a3441167e55e4e59d70867ffcb860f177c386a33c22c45b6bbc1aeba9280891becb435699dd06d3d53cf8823365626787a085910b93d27073f3c0264fff7bc languageName: node linkType: hard "@grafana/faro-web-tracing@npm:^1.13.2": - version: 1.18.2 - resolution: "@grafana/faro-web-tracing@npm:1.18.2" + version: 1.19.0 + resolution: "@grafana/faro-web-tracing@npm:1.19.0" dependencies: - "@grafana/faro-web-sdk": "npm:^1.18.2" + "@grafana/faro-web-sdk": "npm:^1.19.0" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.201.0" - "@opentelemetry/instrumentation": "npm:^0.201.0" - "@opentelemetry/instrumentation-fetch": "npm:^0.201.0" - "@opentelemetry/instrumentation-xml-http-request": "npm:^0.201.0" - "@opentelemetry/otlp-transformer": "npm:^0.201.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.202.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/instrumentation-fetch": "npm:^0.202.0" + "@opentelemetry/instrumentation-xml-http-request": "npm:^0.202.0" + "@opentelemetry/otlp-transformer": "npm:^0.202.0" "@opentelemetry/resources": "npm:^2.0.0" "@opentelemetry/sdk-trace-web": "npm:^2.0.0" "@opentelemetry/semantic-conventions": "npm:^1.32.0" - checksum: 10/5ca81618b52216da3acac456d149f62d1514c9e11b9d7a091ae9fb56e99e6adf2fb5a8630db13473524fd9c818a392366a7142ba84334e8981e956a110186bcd + checksum: 10/da7e0cd6b3fb5269d26530bdb212b005698ea489748c090fe6867bdb501e124c69ea8157a88f6e6115472500c4358f86001c104ccb24b6c9b8ad24c3b525917f languageName: node linkType: hard @@ -3214,27 +3221,27 @@ __metadata: "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:12.1.0-pre" "@leeoniya/ufuzzy": "npm:1.0.18" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" - "@types/react-virtualized-auto-sizer": "npm:1.0.4" + "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" jest: "npm:^29.6.4" jest-canvas-mock: "npm:2.5.2" lodash: "npm:4.17.21" react: "npm:18.3.1" react-use: "npm:17.6.0" - react-virtualized-auto-sizer: "npm:1.0.25" + react-virtualized-auto-sizer: "npm:1.0.26" rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" @@ -3242,22 +3249,22 @@ __metadata: ts-jest: "npm:29.2.5" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 languageName: unknown linkType: soft -"@grafana/google-sdk@npm:0.3.2": - version: 0.3.2 - resolution: "@grafana/google-sdk@npm:0.3.2" +"@grafana/google-sdk@npm:0.3.4": + version: 0.3.4 + resolution: "@grafana/google-sdk@npm:0.3.4" peerDependencies: "@grafana/data": ">=10.4.0" "@grafana/ui": ">=10.4.0" react: ^18.2.0 react-dom: ^18.2.0 - checksum: 10/da0b1c7eda95a03da5ea919aaf97e3ae942c219ee0854483f84824837192fad06c302c3f7e87a7819882ba272726039a0633891bd3ded10449227aafac16dfc7 + checksum: 10/6156a07ab9a8e31ac20a689448f7d6bc86bd9d3fd0b22de8182cf5e6f96c05054f8c53a38148de58dca807adb5b0563914b03a22308ec7f770bf1cc71e8f21bd languageName: node linkType: hard @@ -3270,25 +3277,25 @@ __metadata: "@types/react": "npm:18.3.18" "@typescript-eslint/utils": "npm:^8.33.1" fast-deep-equal: "npm:^3.1.3" - i18next: "npm:^24.0.0" + i18next: "npm:^25.0.0" i18next-browser-languagedetector: "npm:^8.0.0" i18next-pseudo: "npm:^2.2.1" micro-memoize: "npm:^4.1.2" react-i18next: "npm:^15.0.0" rollup: "npm:^4.22.4" rollup-plugin-copy: "npm:3.5.0" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" peerDependencies: react: ">=18" languageName: unknown linkType: soft -"@grafana/lezer-logql@npm:0.2.7": - version: 0.2.7 - resolution: "@grafana/lezer-logql@npm:0.2.7" +"@grafana/lezer-logql@npm:0.2.8": + version: 0.2.8 + resolution: "@grafana/lezer-logql@npm:0.2.8" peerDependencies: "@lezer/lr": ^1.0.0 - checksum: 10/606a9dc77b3b3751e1f325d6b1a8994b1bafef7fe0f6f3980ee7d184244b373f828960f46a748746e765615352ed8928d10f22ff06cede588fcc9d32e70a68d8 + checksum: 10/56b31f9479037201b07d27602c023a2c2373758f232b02a4e5c501dbfabe6f708ddd2a4f8d059b5aad576297137bc2836ed3f520eceee6ffcd3001ad53c73f5c languageName: node linkType: hard @@ -3346,16 +3353,16 @@ __metadata: "@types/jest": "npm:^29.5.4" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" - "@types/systemjs": "npm:6.15.1" + "@types/systemjs": "npm:6.15.3" jest: "npm:^29.6.4" react: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-use: "npm:17.6.0" rxjs: "npm:7.8.2" ts-jest: "npm:29.2.5" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 @@ -3374,12 +3381,12 @@ __metadata: eslint: "npm:9.19.0" eslint-webpack-plugin: "npm:4.2.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" - glob: "npm:11.0.1" + glob: "npm:11.0.3" imports-loader: "npm:^5.0.0" replace-in-file-webpack-plugin: "npm:1.0.6" swc-loader: "npm:0.2.6" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" webpack: "npm:5.97.1" webpack-bundle-analyzer: "npm:^4.10.2" webpack-virtual-modules: "npm:^0.6.2" @@ -3433,7 +3440,7 @@ __metadata: resolution: "@grafana/prometheus@workspace:packages/grafana-prometheus" dependencies: "@emotion/css": "npm:11.13.5" - "@floating-ui/react": "npm:0.27.12" + "@floating-ui/react": "npm:0.27.13" "@grafana/data": "npm:12.1.0-pre" "@grafana/e2e-selectors": "npm:12.1.0-pre" "@grafana/i18n": "npm:12.1.0-pre" @@ -3447,18 +3454,18 @@ __metadata: "@lezer/common": "npm:1.2.3" "@lezer/highlight": "npm:1.2.1" "@lezer/lr": "npm:1.4.2" - "@prometheus-io/lezer-promql": "npm:0.304.1" + "@prometheus-io/lezer-promql": "npm:0.304.2" "@reduxjs/toolkit": "npm:2.5.1" "@rollup/plugin-dynamic-import-vars": "npm:2.1.5" "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-json": "npm:6.1.0" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.5" @@ -3469,7 +3476,7 @@ __metadata: "@types/semver": "npm:7.7.0" "@types/uuid": "npm:10.0.0" debounce-promise: "npm:3.1.2" - esbuild: "npm:0.25.0" + esbuild: "npm:0.25.6" i18next-parser: "npm:9.3.0" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" @@ -3492,7 +3499,7 @@ __metadata: rxjs: "npm:7.8.2" semver: "npm:7.7.2" testing-library-selector: "npm:0.3.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uuid: "npm:11.1.0" peerDependencies: react: ^18.0.0 @@ -3510,18 +3517,18 @@ __metadata: "@grafana/schema": "npm:12.1.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@grafana/ui": "npm:12.1.0-pre" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" "@rollup/plugin-terser": "npm:0.4.4" "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.14" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" - "@types/systemjs": "npm:6.15.1" - esbuild: "npm:0.25.0" + "@types/systemjs": "npm:6.15.3" + esbuild: "npm:0.25.6" history: "npm:4.10.1" lodash: "npm:4.17.21" react: "npm:18.3.1" @@ -3535,18 +3542,18 @@ __metadata: rollup-plugin-sourcemaps: "npm:0.6.3" rxjs: "npm:7.8.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 languageName: unknown linkType: soft -"@grafana/scenes-react@npm:^6.26.1": - version: 6.26.1 - resolution: "@grafana/scenes-react@npm:6.26.1" +"@grafana/scenes-react@npm:^6.27.2": + version: 6.27.2 + resolution: "@grafana/scenes-react@npm:6.27.2" dependencies: - "@grafana/scenes": "npm:6.26.1" + "@grafana/scenes": "npm:6.27.2" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3558,13 +3565,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/65ab66edcc19cdd9e05071317885688106c09c72ad6ada1429a3d3bdc0605814493e3a428af9178f90f13f620e10169280c33bbc63fa6c0dd6c40b81378cc547 + checksum: 10/415186350304d999a13fa1e66f8bb7675cb28d5174a9449b8cb3d6a25efaecfc74a0e2e69cde9513f6fd9cac9bd2891c12e93a974ec15cfa0d6d04e5b95b5306 languageName: node linkType: hard -"@grafana/scenes@npm:6.26.1, @grafana/scenes@npm:^6.26.1": - version: 6.26.1 - resolution: "@grafana/scenes@npm:6.26.1" +"@grafana/scenes@npm:6.27.2, @grafana/scenes@npm:^6.27.2": + version: 6.27.2 + resolution: "@grafana/scenes@npm:6.27.2" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3584,7 +3591,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/aed5a13a17fe8ed172637cd75edae9d20815321d2145ba0b4a179950969b6a1bcbe732ac42312b7ef34e7454d18725fd23ca58934fad43ce77b1edc2efd62ef7 + checksum: 10/8ba3d6c9b29de32a943a4a53511822f27eb51b35270368f0b9d80208eab50ae8767c6b372a7c7779a213618eab24bbb3e441dde42b038fc0a2078989879e0d14 languageName: node linkType: hard @@ -3593,15 +3600,15 @@ __metadata: resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: "@grafana/tsconfig": "npm:^2.0.0" - "@rollup/plugin-node-resolve": "npm:16.0.0" - esbuild: "npm:0.25.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" + esbuild: "npm:0.25.6" glob: "npm:^11.0.0" rimraf: "npm:6.0.1" rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" languageName: unknown linkType: soft @@ -3623,12 +3630,12 @@ __metadata: "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" "@types/jest": "npm:^29.5.4" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" - "@types/react-virtualized-auto-sizer": "npm:1.0.4" - "@types/systemjs": "npm:6.15.1" + "@types/react-virtualized-auto-sizer": "npm:1.0.8" + "@types/systemjs": "npm:6.15.3" "@types/uuid": "npm:10.0.0" i18next-parser: "npm:9.3.0" immutable: "npm:5.1.3" @@ -3636,15 +3643,15 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-use: "npm:17.6.0" - react-virtualized-auto-sizer: "npm:1.0.25" + react-virtualized-auto-sizer: "npm:1.0.26" rxjs: "npm:7.8.2" sql-formatter-plus: "npm:^1.3.6" ts-jest: "npm:29.2.5" ts-node: "npm:10.9.2" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uuid: "npm:11.1.0" peerDependencies: "@grafana/runtime": 10.4.0-pre @@ -3657,8 +3664,8 @@ __metadata: dependencies: "@grafana/tsconfig": "npm:^2.0.0" "@types/node": "npm:22.15.0" - msw: "npm:2.10.2" - typescript: "npm:5.7.3" + msw: "npm:2.10.4" + typescript: "npm:5.8.3" languageName: unknown linkType: soft @@ -3678,7 +3685,7 @@ __metadata: "@emotion/react": "npm:11.14.0" "@emotion/serialize": "npm:1.3.3" "@faker-js/faker": "npm:^9.0.0" - "@floating-ui/react": "npm:0.27.12" + "@floating-ui/react": "npm:0.27.13" "@grafana/data": "npm:12.1.0-pre" "@grafana/e2e-selectors": "npm:12.1.0-pre" "@grafana/faro-web-sdk": "npm:^1.13.2" @@ -3692,7 +3699,7 @@ __metadata: "@react-aria/focus": "npm:3.20.5" "@react-aria/overlays": "npm:3.27.3" "@react-aria/utils": "npm:3.29.1" - "@rollup/plugin-node-resolve": "npm:16.0.0" + "@rollup/plugin-node-resolve": "npm:16.0.1" "@storybook/addon-a11y": "npm:^8.6.2" "@storybook/addon-actions": "npm:^8.6.2" "@storybook/addon-docs": "npm:^8.6.2" @@ -3714,14 +3721,14 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.2.0" "@testing-library/user-event": "npm:14.6.1" - "@types/chance": "npm:1.1.6" + "@types/chance": "npm:^1.1.7" "@types/common-tags": "npm:^1.8.0" "@types/d3": "npm:7.4.3" "@types/hoist-non-react-statics": "npm:3.3.6" "@types/is-hotkey": "npm:0.1.10" "@types/jest": "npm:29.5.14" "@types/jquery": "npm:3.5.32" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:22.15.0" "@types/prismjs": "npm:1.26.5" @@ -3738,8 +3745,9 @@ __metadata: "@types/tinycolor2": "npm:1.4.6" "@types/uuid": "npm:10.0.0" calculate-size: "npm:1.1.1" - chance: "npm:1.1.12" + chance: "npm:^1.1.13" classnames: "npm:2.5.1" + clsx: "npm:^2.1.1" common-tags: "npm:1.8.2" core-js: "npm:3.40.0" css-loader: "npm:7.1.2" @@ -3747,11 +3755,11 @@ __metadata: d3: "npm:7.9.0" date-fns: "npm:4.1.0" downshift: "npm:^9.0.6" - esbuild: "npm:0.25.0" - expose-loader: "npm:5.0.0" + esbuild: "npm:0.25.6" + expose-loader: "npm:5.0.1" fs-extra: "npm:^11.2.0" hoist-non-react-statics: "npm:3.3.2" - i18next: "npm:^24.0.0" + i18next: "npm:^25.0.0" i18next-browser-languagedetector: "npm:^8.0.0" immutable: "npm:5.1.3" is-hotkey: "npm:0.2.0" @@ -3763,7 +3771,7 @@ __metadata: monaco-editor: "npm:0.34.1" msw: "npm:^2.10.2" msw-storybook-addon: "npm:^2.0.5" - ol: "npm:7.4.0" + ol: "npm:10.6.0" prismjs: "npm:1.30.0" process: "npm:^0.11.10" rc-cascader: "npm:3.34.0" @@ -3772,7 +3780,7 @@ __metadata: rc-slider: "npm:11.1.8" rc-tooltip: "npm:6.4.0" react: "npm:18.3.1" - react-calendar: "npm:^5.1.0" + react-calendar: "npm:^6.0.0" react-colorful: "npm:5.6.1" react-custom-scrollbars-2: "npm:4.5.0" react-data-grid: "grafana/react-data-grid#de920f0105cb2b7d774444e7443a675f3b568ad6" @@ -3785,7 +3793,7 @@ __metadata: react-loading-skeleton: "npm:3.5.0" react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-select-event: "npm:^5.1.0" react-table: "npm:7.8.0" react-transition-group: "npm:4.4.5" @@ -3806,7 +3814,7 @@ __metadata: style-loader: "npm:4.0.0" tinycolor2: "npm:1.6.0" tslib: "npm:2.8.1" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uplot: "npm:1.6.32" uuid: "npm:11.1.0" uwrap: "npm:0.1.1" @@ -3987,6 +3995,22 @@ __metadata: languageName: node linkType: hard +"@isaacs/balanced-match@npm:^4.0.1": + version: 4.0.1 + resolution: "@isaacs/balanced-match@npm:4.0.1" + checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 + languageName: node + linkType: hard + +"@isaacs/brace-expansion@npm:^5.0.0": + version: 5.0.0 + resolution: "@isaacs/brace-expansion@npm:5.0.0" + dependencies: + "@isaacs/balanced-match": "npm:^4.0.1" + checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -4736,9 +4760,9 @@ __metadata: languageName: node linkType: hard -"@lerna/create@npm:8.2.1": - version: 8.2.1 - resolution: "@lerna/create@npm:8.2.1" +"@lerna/create@npm:8.2.3": + version: 8.2.3 + resolution: "@lerna/create@npm:8.2.3" dependencies: "@npmcli/arborist": "npm:7.5.4" "@npmcli/package-json": "npm:5.2.0" @@ -4763,7 +4787,6 @@ __metadata: get-stream: "npm:6.0.0" git-url-parse: "npm:14.0.0" glob-parent: "npm:6.0.2" - globby: "npm:11.1.0" graceful-fs: "npm:4.2.11" has-unicode: "npm:2.0.1" ini: "npm:^1.3.8" @@ -4798,9 +4821,10 @@ __metadata: slash: "npm:^3.0.0" ssri: "npm:^10.0.6" string-width: "npm:^4.2.3" - strong-log-transformer: "npm:2.1.0" tar: "npm:6.2.1" temp-dir: "npm:1.0.0" + through: "npm:2.3.8" + tinyglobby: "npm:0.2.12" upath: "npm:2.0.1" uuid: "npm:^10.0.0" validate-npm-package-license: "npm:^3.0.4" @@ -4810,7 +4834,7 @@ __metadata: write-pkg: "npm:4.0.0" yargs: "npm:17.7.2" yargs-parser: "npm:21.1.1" - checksum: 10/802db88edad8967afcbf499f68491139965209137ec92f402ac838452079f143701d6f9abd9832b3506a7e1c56e01ea9c09ffd6f8782b1d9202413756fcfd708 + checksum: 10/1264bf324de2c83377dbc0b49c6731b9e27401c552c34601846b012c46aee496c9a3bcdeba87e59cfaf8b54d3c82f7f3ece62ff4c77ca7b3a85acfc021b7cbb2 languageName: node linkType: hard @@ -4986,48 +5010,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/jsonlint-lines-primitives@npm:~2.0.2": - version: 2.0.2 - resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2" - checksum: 10/6d8e64d34d912ebf29fead0d1917c8d8ad86e96f69b6100a9764af8cba391609474cdce7f7e4a2d579ccea58a142d1454257b795403179e9133a09af13101068 - languageName: node - linkType: hard - -"@mapbox/mapbox-gl-style-spec@npm:^13.23.1": - version: 13.25.0 - resolution: "@mapbox/mapbox-gl-style-spec@npm:13.25.0" - dependencies: - "@mapbox/jsonlint-lines-primitives": "npm:~2.0.2" - "@mapbox/point-geometry": "npm:^0.1.0" - "@mapbox/unitbezier": "npm:^0.0.0" - csscolorparser: "npm:~1.0.2" - json-stringify-pretty-compact: "npm:^2.0.0" - minimist: "npm:^1.2.5" - rw: "npm:^1.3.3" - sort-object: "npm:^0.3.2" - bin: - gl-style-composite: bin/gl-style-composite.js - gl-style-format: bin/gl-style-format.js - gl-style-migrate: bin/gl-style-migrate.js - gl-style-validate: bin/gl-style-validate.js - checksum: 10/cabd9629bde72fe961f9a9d957534f75eb74bddb10068bf7b43fa73868eeb857a31196a12dc2a4532ab732bc7d38392e9bd4582fa9021ef9a5c3465e964201d2 - languageName: node - linkType: hard - -"@mapbox/point-geometry@npm:^0.1.0": - version: 0.1.0 - resolution: "@mapbox/point-geometry@npm:0.1.0" - checksum: 10/f6f78ac8a7f798efb19db6eb1a9e05da7ba942102f5347c1a673d94202d0c606ec3f522efa3e76d583cdca46fb96dde52c3d37234f162d21df42f9e8c4f182bd - languageName: node - linkType: hard - -"@mapbox/unitbezier@npm:^0.0.0": - version: 0.0.0 - resolution: "@mapbox/unitbezier@npm:0.0.0" - checksum: 10/211fc5b0a40fafa0127baf87938a6a00535b22b51bec95df2f6141cf1dd50339bca2a9729c7a9803cdee5c2b4e0e3323a882655c74f1a86e557096684196e1ff - languageName: node - linkType: hard - "@mdx-js/react@npm:^3.0.0": version: 3.0.1 resolution: "@mdx-js/react@npm:3.0.1" @@ -5634,12 +5616,12 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.201.1": - version: 0.201.1 - resolution: "@opentelemetry/api-logs@npm:0.201.1" +"@opentelemetry/api-logs@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/api-logs@npm:0.202.0" dependencies: "@opentelemetry/api": "npm:^1.3.0" - checksum: 10/baa14906caf848b7ff32fdd2b8cbad5c96b6e5b4bb4e52cb4118b323b77b2e99630b4d58d92f110343d475a21fd5bdcaaa37c29a4a386136ed4ee01528a2b2ed + checksum: 10/22171137ad1d876a79a6f046b4adcc44a13941a07f2172948e4c8dbb6cacfe276b1fa5087f15e4acdd567f748010d531321c93ebf93a6c09586351c9048bf6c7 languageName: node linkType: hard @@ -5697,90 +5679,88 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.201.1" +"@opentelemetry/exporter-trace-otlp-http@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-exporter-base": "npm:0.201.1" - "@opentelemetry/otlp-transformer": "npm:0.201.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" "@opentelemetry/resources": "npm:2.0.1" "@opentelemetry/sdk-trace-base": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/b1cec9287384900a3dc5326e3d2c089da9cb13b77f42660c201d0da2ae47f34dda81145ee14b1c8fcce12a59dd2511704d1179a0c991fdcf1c645bffdee6072a + checksum: 10/f9e60e51b5dcca3d4d32f2091f04faa2bcaf677b9f8a88990c8cdc7c6345e4244e46c4e8df1c336fc6f741aea092407032ade1236089eb729196e6fcdc055ad0 languageName: node linkType: hard -"@opentelemetry/instrumentation-fetch@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/instrumentation-fetch@npm:0.201.1" +"@opentelemetry/instrumentation-fetch@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation-fetch@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/instrumentation": "npm:0.201.1" + "@opentelemetry/instrumentation": "npm:0.202.0" "@opentelemetry/sdk-trace-web": "npm:2.0.1" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/9d096774fb93e1c690d40628669a383cb2c972a5aa699bd2fbeb4da7c30dd30e6959dd1b464e6c4d7b3f3d92c71364ca75b3ee5da8e291dc8939f9b9fcd1f1d7 + checksum: 10/c3e87a75878b7b16f5c9ac66c73b6affb8d9d6e5ba95aa19c05a0f73c03d831e7d60e586d9a5bdf76e962acaadbe989b351fefe1908ab439afcd0158bded10e6 languageName: node linkType: hard -"@opentelemetry/instrumentation-xml-http-request@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/instrumentation-xml-http-request@npm:0.201.1" +"@opentelemetry/instrumentation-xml-http-request@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation-xml-http-request@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/instrumentation": "npm:0.201.1" + "@opentelemetry/instrumentation": "npm:0.202.0" "@opentelemetry/sdk-trace-web": "npm:2.0.1" "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/5fec2a79a4b041a22e15a1aa06436b3881d06f25e3f5ee75309bab335ddadde4523045a6352e115b0331c48488cb878d0ebfc33d76cf34df5f73bcf6c07bcbca + checksum: 10/6a2c76374f5af2d37318f5a689447a99ecc65c7c5546f3421171c870d5ddf1536e30f6fd63d8046cbc3e4cbf737529bae304959d971beefaf7f730fefb9e663c languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:0.201.1, @opentelemetry/instrumentation@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/instrumentation@npm:0.201.1" +"@opentelemetry/instrumentation@npm:0.202.0, @opentelemetry/instrumentation@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation@npm:0.202.0" dependencies: - "@opentelemetry/api-logs": "npm:0.201.1" - "@types/shimmer": "npm:^1.2.0" + "@opentelemetry/api-logs": "npm:0.202.0" import-in-the-middle: "npm:^1.8.1" require-in-the-middle: "npm:^7.1.1" - shimmer: "npm:^1.2.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/cef0b05ce1b153f9d7ce770782c67f2422d98a6c1bd4bc5d69db6c2c3d91523c7030bb0ff6ff16ae246586334f4774c8d1b71bef5e395b7e7cba4645a73a14c4 + checksum: 10/da1db1ebc4ca847cc68d894b2e3a6c6552851d93af8ea793d42474e920f710664575c8991dc269e1a83fcf8f5abdda2cc724fa24e9cc4ae19fa6f70eb68ffc0e languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@npm:0.201.1": - version: 0.201.1 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.201.1" +"@opentelemetry/otlp-exporter-base@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.202.0" dependencies: "@opentelemetry/core": "npm:2.0.1" - "@opentelemetry/otlp-transformer": "npm:0.201.1" + "@opentelemetry/otlp-transformer": "npm:0.202.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/d9c64ebf531e5a7e3d42537d2058e331165e4764b4a54d453668c1f8bbfa14008255771110bb106aa44c094ad76933a46f37f67d4b362908d511e57e72b7cd09 + checksum: 10/229778895ba1971451a8b1ec8a4787b0cdba337c87f3ca6aaf6d649f5e2b26439ba1befa36d31824fffa3cef62382792680ec70194f57aac6ebe98e95acecc69 languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.201.1, @opentelemetry/otlp-transformer@npm:^0.201.0": - version: 0.201.1 - resolution: "@opentelemetry/otlp-transformer@npm:0.201.1" +"@opentelemetry/otlp-transformer@npm:0.202.0, @opentelemetry/otlp-transformer@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.202.0" dependencies: - "@opentelemetry/api-logs": "npm:0.201.1" + "@opentelemetry/api-logs": "npm:0.202.0" "@opentelemetry/core": "npm:2.0.1" "@opentelemetry/resources": "npm:2.0.1" - "@opentelemetry/sdk-logs": "npm:0.201.1" + "@opentelemetry/sdk-logs": "npm:0.202.0" "@opentelemetry/sdk-metrics": "npm:2.0.1" "@opentelemetry/sdk-trace-base": "npm:2.0.1" protobufjs: "npm:^7.3.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/bed6f7d12aba212cfc9dd0c482de6d983f31a994faa4cb13f651f1cbe98ae8935ed25a4a25887cdcdc9a53af1ee8cd3406e869d900499c0cbadf87f3218dcdb4 + checksum: 10/67e189af60bf8308a5b93deb85bef9709a5604b8c7915d0d42a7a2bff932b7257dfdfc071812c77913e136955a985ffe838c72b5f10059021087ea0bc52d84cd languageName: node linkType: hard @@ -5808,16 +5788,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.201.1": - version: 0.201.1 - resolution: "@opentelemetry/sdk-logs@npm:0.201.1" +"@opentelemetry/sdk-logs@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/sdk-logs@npm:0.202.0" dependencies: - "@opentelemetry/api-logs": "npm:0.201.1" + "@opentelemetry/api-logs": "npm:0.202.0" "@opentelemetry/core": "npm:2.0.1" "@opentelemetry/resources": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10/c2d8aad418268c5ab4ad18f8eea5bb11fff1659b9bbbcd30546a622c2a6e04e3361de7809e702bff7c151cf7c21408ab8fd798b43ffbc8f549bfb91d0c40d4bb + checksum: 10/e1b76647282a41ad7004c86c0058b0e9be70fdcf34aa2761929401e274af79bc5e3c6610a63e14fde4d51d96d8c96adaf3247c10b9a64cb70086d2ede2e421ca languageName: node linkType: hard @@ -6158,13 +6138,13 @@ __metadata: languageName: node linkType: hard -"@prometheus-io/lezer-promql@npm:0.304.1": - version: 0.304.1 - resolution: "@prometheus-io/lezer-promql@npm:0.304.1" +"@prometheus-io/lezer-promql@npm:0.304.2": + version: 0.304.2 + resolution: "@prometheus-io/lezer-promql@npm:0.304.2" peerDependencies: "@lezer/highlight": ^1.1.2 "@lezer/lr": ^1.2.3 - checksum: 10/d308388bb817423c2b966e8d8e27deb02e98915af91043baf7c7b9634fb5ff3655378b237cc0766e5f0446fed74027f2c385aa3cff7921620e7cbdd1844bc95c + checksum: 10/dd2ce48e0f7a90ab3afb707e56252cf9debf483149bf1d63f8f80e408f76b8f8df3a99fc147876f4d360879be800e323ed53bde52c4599cd7263dac54a3d7ac6 languageName: node linkType: hard @@ -6718,9 +6698,9 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:16.0.0": - version: 16.0.0 - resolution: "@rollup/plugin-node-resolve@npm:16.0.0" +"@rollup/plugin-node-resolve@npm:16.0.1": + version: 16.0.1 + resolution: "@rollup/plugin-node-resolve@npm:16.0.1" dependencies: "@rollup/pluginutils": "npm:^5.0.1" "@types/resolve": "npm:1.20.2" @@ -6732,7 +6712,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10/018a97667d68bd78d6b1de5597680dcc5785f9339a936984a5715ad2cd7c6f2c85fb9448552b94e6903db35e2d3b218b54e5e9ca048257f2d3bdea2e05d886c7 + checksum: 10/88fee8c003a5730cca2c06edd200ec6a46c7ab28bed3a99aea6d3070f34f980f575fcbea906946579e41b0be6fd7a2fbc24cdf0ca24f172a555f130726915d8b languageName: node linkType: hard @@ -8562,9 +8542,9 @@ __metadata: languageName: node linkType: hard -"@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app": +"@test-plugins/extensions-test-app@workspace:e2e-playwright/test-plugins/grafana-extensionstest-app": version: 0.0.0-use.local - resolution: "@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app" + resolution: "@test-plugins/extensions-test-app@workspace:e2e-playwright/test-plugins/grafana-extensionstest-app" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" @@ -8594,9 +8574,9 @@ __metadata: languageName: unknown linkType: soft -"@test-plugins/grafana-e2etest-datasource@workspace:e2e/test-plugins/grafana-test-datasource": +"@test-plugins/grafana-e2etest-datasource@workspace:e2e-playwright/test-plugins/grafana-test-datasource": version: 0.0.0-use.local - resolution: "@test-plugins/grafana-e2etest-datasource@workspace:e2e/test-plugins/grafana-test-datasource" + resolution: "@test-plugins/grafana-e2etest-datasource@workspace:e2e-playwright/test-plugins/grafana-test-datasource" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" @@ -8902,10 +8882,10 @@ __metadata: languageName: node linkType: hard -"@types/chance@npm:1.1.6, @types/chance@npm:^1.1.3": - version: 1.1.6 - resolution: "@types/chance@npm:1.1.6" - checksum: 10/f4366f1b3144d143af3e6f0fad2ed1db7b9bdfa7d82d40944e9619d57fe7e6b60e8c1452f47a8ededa6b2188932879518628ecd9aac81c40384ded39c26338ba +"@types/chance@npm:^1.1.7": + version: 1.1.7 + resolution: "@types/chance@npm:1.1.7" + checksum: 10/5b3bf4ef0b7a2f6554f7767d7f081b4f613b45e74e74450d4da9c10cd12162420fc6ff5bb0abea143d6f7c43b62776b7a0e75d242cbe25c729fe020a1735e16f languageName: node linkType: hard @@ -9276,13 +9256,6 @@ __metadata: languageName: node linkType: hard -"@types/diff@npm:^7.0.0": - version: 7.0.1 - resolution: "@types/diff@npm:7.0.1" - checksum: 10/ef8c5fe0ea56737e8967c3db5e78518134f704e8e2971cb4ba6f2ed46ed5e13b4bbe41a6b2b78122b8f47c618ac25550f85a681633636a3a020369042523295d - languageName: node - linkType: hard - "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" @@ -9639,17 +9612,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:*, @types/lodash@npm:^4, @types/lodash@npm:^4.14.172": - version: 4.17.16 - resolution: "@types/lodash@npm:4.17.16" - checksum: 10/9a8bb7471a7521bd65d528e1bd14f79819a3eeb6f8a35a8a44649a7d773775c0813e93fd93bd32ccf350bb076c0bf02c6d47877c4625f526f6dd4d283c746aec - languageName: node - linkType: hard - -"@types/lodash@npm:4.17.15": - version: 4.17.15 - resolution: "@types/lodash@npm:4.17.15" - checksum: 10/27b348b5971b9c670215331b52448a13d7d65bf1fbd320a7049c9c153c1186ff5d116ba75f05f07d32d7ece8a992b26a30c7bdc9be22a3d1e4e3e6068aa04603 +"@types/lodash@npm:*, @types/lodash@npm:4.17.20, @types/lodash@npm:^4, @types/lodash@npm:^4.14.172": + version: 4.17.20 + resolution: "@types/lodash@npm:4.17.20" + checksum: 10/8cd8ad3bd78d2e06a93ae8d6c9907981d5673655fec7cb274a4d9a59549aab5bb5b3017361280773b8990ddfccf363e14d1b37c97af8a9fe363de677f9a61524 languageName: node linkType: hard @@ -9736,11 +9702,11 @@ __metadata: linkType: hard "@types/node-forge@npm:^1, @types/node-forge@npm:^1.3.0": - version: 1.3.11 - resolution: "@types/node-forge@npm:1.3.11" + version: 1.3.12 + resolution: "@types/node-forge@npm:1.3.12" dependencies: "@types/node": "npm:*" - checksum: 10/670c9b377c48189186ec415e3c8ed371f141ecc1a79ab71b213b20816adeffecba44dae4f8406cc0d09e6349a4db14eb8c5893f643d8e00fa19fc035cf49dee0 + checksum: 10/6840622b4253e04f1848fda7355603d75d0553d9e103e8eeb3c2cd832fe1f6af8a27ebfd70ff8a6b2a6c3737054ca280a9f9b7fb9cfbfb6be69c45ca4861c4aa languageName: node linkType: hard @@ -9859,6 +9825,13 @@ __metadata: languageName: node linkType: hard +"@types/rbush@npm:4.0.0": + version: 4.0.0 + resolution: "@types/rbush@npm:4.0.0" + checksum: 10/ee37c7fa322a83af4e754180022253b7e2bbbb853c9a6d8ffce1ea1e59bbb0eef3a347e2f37934bb1afa4d40ea2ea44409d7cee751cb8ac8dbded13dc9160a64 + languageName: node + linkType: hard + "@types/react-color@npm:3.0.13": version: 3.0.13 resolution: "@types/react-color@npm:3.0.13" @@ -9945,12 +9918,12 @@ __metadata: languageName: node linkType: hard -"@types/react-virtualized-auto-sizer@npm:1.0.4": - version: 1.0.4 - resolution: "@types/react-virtualized-auto-sizer@npm:1.0.4" +"@types/react-virtualized-auto-sizer@npm:1.0.8": + version: 1.0.8 + resolution: "@types/react-virtualized-auto-sizer@npm:1.0.8" dependencies: - "@types/react": "npm:*" - checksum: 10/e0d41ac6cf0f48dfef45c0cd70146578f42ad83dad90911aa0dfe72b0e1ea62ca9ff56d4f43f322ec6dae2e7cb4d025aefe0e85241f7782c249aeface1050512 + react-virtualized-auto-sizer: "npm:*" + checksum: 10/faddd1ee92e87e0b621a5be4eb510b010437375b6c85cf72d764f52506c59fad93d79c159aad251677e4e2fb548ad4a7e76c59f5a701a517bcf5552a9929c4af languageName: node linkType: hard @@ -10066,13 +10039,6 @@ __metadata: languageName: node linkType: hard -"@types/shimmer@npm:^1.2.0": - version: 1.2.0 - resolution: "@types/shimmer@npm:1.2.0" - checksum: 10/f081a31d826ce7bfe8cc7ba8129d2b1dffae44fd580eba4fcf741237646c4c2494ae6de2cada4b7713d138f35f4bc512dbf01311d813dee82020f97d7d8c491c - languageName: node - linkType: hard - "@types/sinonjs__fake-timers@npm:8.1.1": version: 8.1.1 resolution: "@types/sinonjs__fake-timers@npm:8.1.1" @@ -10173,10 +10139,10 @@ __metadata: languageName: node linkType: hard -"@types/systemjs@npm:6.15.1": - version: 6.15.1 - resolution: "@types/systemjs@npm:6.15.1" - checksum: 10/4c29f4951299b67a287b8b21219416c4d8eee277582ae045f6a97eaaa9d27ad59789f6b608c5519adf0e42eee23f139c4027cc157f37807a16b4601b353f5442 +"@types/systemjs@npm:6.15.3": + version: 6.15.3 + resolution: "@types/systemjs@npm:6.15.3" + checksum: 10/c2e6d7b3d83282657074e0b2d5bfcd6e4923f02d659e7bd3bca215466aee1960a444243d0f97dd26e8843a45e2b34ee33ecac39efb4e419ce2dc8f8f7fb03472 languageName: node linkType: hard @@ -10277,9 +10243,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.18.4": - version: 1.18.5 - resolution: "@types/webpack-env@npm:1.18.5" - checksum: 10/3c8dd0b23d45e2d33abdfbae7f1d8f75ce23d54588b08943e833f4dba81eb683ac68672a75eccbdba8e008bc1647638803c1bcadc8cdfd1dd7142fa2c3f612de + version: 1.18.8 + resolution: "@types/webpack-env@npm:1.18.8" + checksum: 10/f3932f3d6c2530f644cfc898eda1ab8182d6ae57f555c2f0179d813549b639078671b71e4041831fc306c5ebe61f5cdac794fe4ceae281fce8bf67e23661a488 languageName: node linkType: hard @@ -10317,53 +10283,53 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.22.0" +"@typescript-eslint/eslint-plugin@npm:8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.35.1" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.22.0" - "@typescript-eslint/type-utils": "npm:8.22.0" - "@typescript-eslint/utils": "npm:8.22.0" - "@typescript-eslint/visitor-keys": "npm:8.22.0" + "@typescript-eslint/scope-manager": "npm:8.35.1" + "@typescript-eslint/type-utils": "npm:8.35.1" + "@typescript-eslint/utils": "npm:8.35.1" + "@typescript-eslint/visitor-keys": "npm:8.35.1" graphemer: "npm:^1.4.0" - ignore: "npm:^5.3.1" + ignore: "npm:^7.0.0" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.0.0" + ts-api-utils: "npm:^2.1.0" peerDependencies: - "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + "@typescript-eslint/parser": ^8.35.1 eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/7211ad95f20a27182e2b55ef50102dfee4a7084c267c4e24cca24f0a28daa0360074a38bb71e407dad6d99db1165096b324b708cf35904b1d4f62fc9d5fd0f98 + typescript: ">=4.8.4 <5.9.0" + checksum: 10/22c4ff7503e4919449b996453ff29ba46e5c0024fac883ac41a313482454f13d55937789f499395dc2a7dba25b1ad47ac5295d60b118f2fa54ca768228514662 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/parser@npm:8.22.0" +"@typescript-eslint/parser@npm:8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/parser@npm:8.35.1" dependencies: - "@typescript-eslint/scope-manager": "npm:8.22.0" - "@typescript-eslint/types": "npm:8.22.0" - "@typescript-eslint/typescript-estree": "npm:8.22.0" - "@typescript-eslint/visitor-keys": "npm:8.22.0" + "@typescript-eslint/scope-manager": "npm:8.35.1" + "@typescript-eslint/types": "npm:8.35.1" + "@typescript-eslint/typescript-estree": "npm:8.35.1" + "@typescript-eslint/visitor-keys": "npm:8.35.1" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/6b7fee52345e8a32d8cfea1ac4aeb563cb0c44ba46290686afde1cd541b787fcf61bec0e6960559f544e9ba3b72670a68f8eda860384aebb5744101f0f1a68c9 + typescript: ">=4.8.4 <5.9.0" + checksum: 10/d5e0ecdb945c90fc1fea3f7dd375e424f1a6d49a97627ad24330499d573d45f85348e05a97e3a4643aec5ad9d210073487687872bd573abd79923a12fc46e716 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.33.1": - version: 8.33.1 - resolution: "@typescript-eslint/project-service@npm:8.33.1" +"@typescript-eslint/project-service@npm:8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/project-service@npm:8.35.1" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.33.1" - "@typescript-eslint/types": "npm:^8.33.1" + "@typescript-eslint/tsconfig-utils": "npm:^8.35.1" + "@typescript-eslint/types": "npm:^8.35.1" debug: "npm:^4.3.4" peerDependencies: typescript: ">=4.8.4 <5.9.0" - checksum: 10/c166e21f305eb798ce135477a1ce78e7fac040b597e0acb2e3ecca84380af2aeb4242944dc201bc611aa14f04dd4af0d123d9cf252f7fe39b0a7e158326f705d + checksum: 10/f8ceb1c6ab7cdf2c7bc334e74d0d1cd86b5e563319c5477987a05f47af433543b281912ae0cdd875561dc2cc4d3ba4ed3bdd8d5bb6dba68bcde4f68a7d0967e7 languageName: node linkType: hard @@ -10377,47 +10343,37 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/scope-manager@npm:8.22.0" +"@typescript-eslint/scope-manager@npm:8.35.1, @typescript-eslint/scope-manager@npm:^8.15.0": + version: 8.35.1 + resolution: "@typescript-eslint/scope-manager@npm:8.35.1" dependencies: - "@typescript-eslint/types": "npm:8.22.0" - "@typescript-eslint/visitor-keys": "npm:8.22.0" - checksum: 10/7fb4bae6d9f8b86a43405b24828cd36ba0751cce4346d86821a4827cded93227f92668044e5e6d802a32096b50cfcaf2ce9ab65322310fa68f5e3819bef70168 + "@typescript-eslint/types": "npm:8.35.1" + "@typescript-eslint/visitor-keys": "npm:8.35.1" + checksum: 10/9124302c969126a50c70f9ccbefec0e5a771563b5518318d56fc6242c5cff61da74e7885832370ccd406a048edc300476b1723ad1845d41bd205879d95fbc6b6 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.33.1, @typescript-eslint/scope-manager@npm:^8.15.0": - version: 8.33.1 - resolution: "@typescript-eslint/scope-manager@npm:8.33.1" - dependencies: - "@typescript-eslint/types": "npm:8.33.1" - "@typescript-eslint/visitor-keys": "npm:8.33.1" - checksum: 10/a513fa79eef269a4f3d031cd1fddbb15c793e3efd6b6b64a6ae85ea0fea03570feffdc071e2bbe9241680d23d8a4ca6d63a998e34c2a24dbe185cd509ee2f9cb - languageName: node - linkType: hard - -"@typescript-eslint/tsconfig-utils@npm:8.33.1, @typescript-eslint/tsconfig-utils@npm:^8.33.1": - version: 8.33.1 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.33.1" +"@typescript-eslint/tsconfig-utils@npm:8.35.1, @typescript-eslint/tsconfig-utils@npm:^8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.35.1" peerDependencies: typescript: ">=4.8.4 <5.9.0" - checksum: 10/9e0e348af4d54b7e42a015bf36e08965c8fa389ac7b179c2ccf2bec5fce4b31e951397394dcbaea6fb0ddf782f4f2f2ee71df108173742f756705f82d653cc9d + checksum: 10/6b6176ec7dbfbe53539bce3e7554f062ff4d220aa5cb5793d52067fe6c196d749e77557dca66f5bf1ee23972e948d5c59461fa3e11da9e34a0a27d9fb7585f5a languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/type-utils@npm:8.22.0" +"@typescript-eslint/type-utils@npm:8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/type-utils@npm:8.35.1" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.22.0" - "@typescript-eslint/utils": "npm:8.22.0" + "@typescript-eslint/typescript-estree": "npm:8.35.1" + "@typescript-eslint/utils": "npm:8.35.1" debug: "npm:^4.3.4" - ts-api-utils: "npm:^2.0.0" + ts-api-utils: "npm:^2.1.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/1da2447ce12f09370082daeef88f8922842e39d2a7b0abe3def21442f85bf4250524c60cbb97276d5cd876783b976dcb0ed85aeb8c0b100d83b7f3a59cdfccbf + typescript: ">=4.8.4 <5.9.0" + checksum: 10/728df75bac6960192c18436a8340ed7a0f78b472486279f673e4018d493569f2278b7fcac78c5e0f7ccdb873ead227de6d94bc7aebf5cf046c4d8e53c5569bfd languageName: node linkType: hard @@ -10428,17 +10384,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/types@npm:8.22.0" - checksum: 10/b43ea5b05ed0b43dcee8d2fa98b2c3f79c604780cbd56e6ba7f89e3066798b7169848694f59523fd2003e8fa699ddc97f28b0860a4eb04eea26c96d5ac9346bd - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:8.33.1, @typescript-eslint/types@npm:^8.33.1, @typescript-eslint/types@npm:^8.9.0": - version: 8.33.1 - resolution: "@typescript-eslint/types@npm:8.33.1" - checksum: 10/27bee01122366438ada7919f91f479fec75e23e2f8033e7e74a12dcfeaa2c28a72e83c545bb676be4369c983b3da5eec101cffb431c52abcc2b6664b48255147 +"@typescript-eslint/types@npm:8.35.1, @typescript-eslint/types@npm:^8.35.1, @typescript-eslint/types@npm:^8.9.0": + version: 8.35.1 + resolution: "@typescript-eslint/types@npm:8.35.1" + checksum: 10/2d5b8f40b2ef0b7d439ee119d2ed12372097c4372aea7ff6d46f92fa743fc60619f8619192fbc0df3833d941be5d9bcb5129b8f6d029716ca86ba42514fbeff9 languageName: node linkType: hard @@ -10460,32 +10409,14 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.22.0" +"@typescript-eslint/typescript-estree@npm:8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.35.1" dependencies: - "@typescript-eslint/types": "npm:8.22.0" - "@typescript-eslint/visitor-keys": "npm:8.22.0" - debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" - ts-api-utils: "npm:^2.0.0" - peerDependencies: - typescript: ">=4.8.4 <5.8.0" - checksum: 10/e3c0b191e2a0f55101c3e3333904f3a255d635e4ea0d026981cc25e83b62660a3a8a7993ac4a3d0c8756afb7dc272099eec48fd93e100a2b8467a5b80ef0026c - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:8.33.1": - version: 8.33.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.33.1" - dependencies: - "@typescript-eslint/project-service": "npm:8.33.1" - "@typescript-eslint/tsconfig-utils": "npm:8.33.1" - "@typescript-eslint/types": "npm:8.33.1" - "@typescript-eslint/visitor-keys": "npm:8.33.1" + "@typescript-eslint/project-service": "npm:8.35.1" + "@typescript-eslint/tsconfig-utils": "npm:8.35.1" + "@typescript-eslint/types": "npm:8.35.1" + "@typescript-eslint/visitor-keys": "npm:8.35.1" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -10494,22 +10425,22 @@ __metadata: ts-api-utils: "npm:^2.1.0" peerDependencies: typescript: ">=4.8.4 <5.9.0" - checksum: 10/2876b66acd72f8a1093f8671e05015554fea736d492dbc83084f74faff71d40d66a96452396272252f5e36c1dab2082679d10c7b8aca9f3e2f1b6084afeef807 + checksum: 10/b38a891a37e1c8d76bdb3e8039482b723df590bf9d192a5480ec6777a316914542f610a1d9070bc53e0642c511ddc4ee1c3c03ac0e04a5510feefa95307f51b7 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/utils@npm:8.22.0" +"@typescript-eslint/utils@npm:8.35.1, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.15.0, @typescript-eslint/utils@npm:^8.33.1, @typescript-eslint/utils@npm:^8.9.0": + version: 8.35.1 + resolution: "@typescript-eslint/utils@npm:8.35.1" dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.22.0" - "@typescript-eslint/types": "npm:8.22.0" - "@typescript-eslint/typescript-estree": "npm:8.22.0" + "@eslint-community/eslint-utils": "npm:^4.7.0" + "@typescript-eslint/scope-manager": "npm:8.35.1" + "@typescript-eslint/types": "npm:8.35.1" + "@typescript-eslint/typescript-estree": "npm:8.35.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/92a5ae5d79a5988e88fdda8d5e88f73e7b9ce24b339098d72698dba766ded274c24d0e2857bcb799c0aa7a59257e54a273eabdaaab39a5cd20283669201eeb53 + typescript: ">=4.8.4 <5.9.0" + checksum: 10/68388898dc095d7813a18049e782d90ed6500496bb68e3ea5efd7e1de24f37732b133bf88faca835b6219383f406693fdf846e16d3c48e9418388121c89dcf48 languageName: node linkType: hard @@ -10531,21 +10462,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.15.0, @typescript-eslint/utils@npm:^8.33.1, @typescript-eslint/utils@npm:^8.9.0": - version: 8.33.1 - resolution: "@typescript-eslint/utils@npm:8.33.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.7.0" - "@typescript-eslint/scope-manager": "npm:8.33.1" - "@typescript-eslint/types": "npm:8.33.1" - "@typescript-eslint/typescript-estree": "npm:8.33.1" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10/ba76f25b178e2b3280d7aad8becf6a1edd3c57f76f36e6a22d951207282ecc8bfce831ed2f2dd95a70637dcfea2932b3d0a39ac19dd8f9881502738464c6179a - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -10556,23 +10472,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.22.0": - version: 8.22.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.22.0" +"@typescript-eslint/visitor-keys@npm:8.35.1": + version: 8.35.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.35.1" dependencies: - "@typescript-eslint/types": "npm:8.22.0" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/1a172620d46e23362c5d1e1e7c8186856dff6b6f1c2697d67f9aac1b3dfd0de96c2c73487e4deed80fad3bfa5cf74cfed3519221657c6ede602b04ac091525a4 - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:8.33.1": - version: 8.33.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.33.1" - dependencies: - "@typescript-eslint/types": "npm:8.33.1" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/8c1ba7373d8d47d2f2183355db278d4042ecdf268a898c5f4736d84aaa716c6b6faf354247eff8dc1337a7f64168f02e34f3781994d11e9fa8c2b0e35c652c49 + "@typescript-eslint/types": "npm:8.35.1" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10/0add7a9c00e7b336797bb7378bd02b3ef31368a8e928afb2dbeec0cc4ab9f6413519e477f5c504d62b38d1dae3791f7ffda36d41b403411608628bff8dd123bd languageName: node linkType: hard @@ -10980,6 +10886,13 @@ __metadata: languageName: node linkType: hard +"@wojtekmaj/date-utils@npm:^2.0.2": + version: 2.0.2 + resolution: "@wojtekmaj/date-utils@npm:2.0.2" + checksum: 10/46256ee404952b3f99e6388ce832f2caf8d20c0b5086227de93ae6d3045cfe2aa56f3b07984350139cde00aa35794516ed1223b7ae997047c42b9403e25ca19e + languageName: node + linkType: hard + "@xobotyi/scrollbar-width@npm:^1.9.5": version: 1.9.5 resolution: "@xobotyi/scrollbar-width@npm:1.9.5" @@ -11793,21 +11706,21 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:10.4.20": - version: 10.4.20 - resolution: "autoprefixer@npm:10.4.20" +"autoprefixer@npm:10.4.21": + version: 10.4.21 + resolution: "autoprefixer@npm:10.4.21" dependencies: - browserslist: "npm:^4.23.3" - caniuse-lite: "npm:^1.0.30001646" + browserslist: "npm:^4.24.4" + caniuse-lite: "npm:^1.0.30001702" fraction.js: "npm:^4.3.7" normalize-range: "npm:^0.1.2" - picocolors: "npm:^1.0.1" + picocolors: "npm:^1.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.1.0 bin: autoprefixer: bin/autoprefixer - checksum: 10/d3c4b562fc4af2393623a0207cc336f5b9f94c4264ae1c316376904c279702ce2b12dc3f27205f491195d1e29bb52ffc269970ceb0f271f035fadee128a273f7 + checksum: 10/5d7aeee78ef362a6838e12312908516a8ac5364414175273e5cff83bbff67612755b93d567f3aa01ce318342df48aeab4b291847b5800c780e58c458f61a98a6 languageName: node linkType: hard @@ -12354,17 +12267,17 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3": - version: 4.25.0 - resolution: "browserslist@npm:4.25.0" +"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3, browserslist@npm:^4.24.4, browserslist@npm:^4.24.5": + version: 4.25.1 + resolution: "browserslist@npm:4.25.1" dependencies: - caniuse-lite: "npm:^1.0.30001718" - electron-to-chromium: "npm:^1.5.160" + caniuse-lite: "npm:^1.0.30001726" + electron-to-chromium: "npm:^1.5.173" node-releases: "npm:^2.0.19" update-browserslist-db: "npm:^1.1.3" bin: browserslist: cli.js - checksum: 10/4a5442b1a0d09c4c64454f184b8fed17d8c3e202034bf39de28f74497d7bd28dddee121b2bab4e34825fe0ed4c166d84e32a39f576c76fce73c1f8f05e4b6ee6 + checksum: 10/bfb5511b425886279bbe2ea44d10e340c8aea85866c9d45083c13491d049b6362e254018c0afbf56d41ceeb64f994957ea8ae98dbba74ef1e54ef901c8732987 languageName: node linkType: hard @@ -12534,13 +12447,13 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1": - version: 1.0.1 - resolution: "call-bind-apply-helpers@npm:1.0.1" +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - checksum: 10/6e30c621170e45f1fd6735e84d02ee8e02a3ab95cb109499d5308cbe5d1e84d0cd0e10b48cc43c76aa61450ae1b03a7f89c37c10fc0de8d4998b42aab0f268cc + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 languageName: node linkType: hard @@ -12556,13 +12469,13 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": - version: 1.0.3 - resolution: "call-bound@npm:1.0.3" +"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - get-intrinsic: "npm:^1.2.6" - checksum: 10/c39a8245f68cdb7c1f5eea7b3b1e3a7a90084ea6efebb78ebc454d698ade2c2bb42ec033abc35f1e596d62496b6100e9f4cdfad1956476c510130e2cda03266d + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10/ef2b96e126ec0e58a7ff694db43f4d0d44f80e641370c21549ed911fecbdbc2df3ebc9bddad918d6bbdefeafb60bb3337902006d5176d72bcd2da74820991af7 languageName: node linkType: hard @@ -12634,10 +12547,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001718": - version: 1.0.30001723 - resolution: "caniuse-lite@npm:1.0.30001723" - checksum: 10/edab89e84a2b257cf640f0bac1f25f92c699ade86143b2affc73403468f894023416a9f4a99e5345c933956990b005a2facfb87ac4517c8ccb588819bb62453b +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001726": + version: 1.0.30001727 + resolution: "caniuse-lite@npm:1.0.30001727" + checksum: 10/6155a4141332c337d6317325bea58a09036a65f45bd9bd834ec38978b40c27d214baa04d25b21a5661664f3fbd00cb830e2bdb7eee8df09970bdd98a71f4dabf languageName: node linkType: hard @@ -12744,10 +12657,10 @@ __metadata: languageName: node linkType: hard -"chance@npm:1.1.12, chance@npm:^1.0.10": - version: 1.1.12 - resolution: "chance@npm:1.1.12" - checksum: 10/8700d5a66e27b47f4bdcf68f48489e1a490f39cd8bc8e39cac67c089b3f8d04b4bbc6710db0e85e802ad698cf3440d92784861208d2c248820355404b85b3f30 +"chance@npm:^1.1.13": + version: 1.1.13 + resolution: "chance@npm:1.1.13" + checksum: 10/968e31ce9b8ce554c8a84fb66d85f6d06a33517bf21355ab760e347a0aaaece0652a23c791406826d4efe619b1aa4e7b66fb42acbd07859ba36ec2498a167b8c languageName: node linkType: hard @@ -12916,15 +12829,15 @@ __metadata: languageName: node linkType: hard -"chrome-remote-interface@npm:0.33.2": - version: 0.33.2 - resolution: "chrome-remote-interface@npm:0.33.2" +"chrome-remote-interface@npm:0.33.3": + version: 0.33.3 + resolution: "chrome-remote-interface@npm:0.33.3" dependencies: commander: "npm:2.11.x" ws: "npm:^7.2.0" bin: chrome-remote-interface: bin/client.js - checksum: 10/fa82c76c5af629f5fbccb22c383604650f8679571385b8610783fde6f006b899c48e1522be551fd9670de59c1a2d93beb152a55651f24ba9c91ca0f8df3f581c + checksum: 10/65d07afc8f97fad6326bd94f0c4ef004d76b16841015c72d09aefbfa4df62b47407067a01c886535a706c36b23070b40fc3edb96444665438efa1d4f65cc3db8 languageName: node linkType: hard @@ -13136,17 +13049,10 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.1.1": - version: 1.2.1 - resolution: "clsx@npm:1.2.1" - checksum: 10/5ded6f61f15f1fa0350e691ccec43a28b12fb8e64c8e94715f2a937bc3722d4c3ed41d6e945c971fc4dcc2a7213a43323beaf2e1c28654af63ba70c9968a8643 - languageName: node - linkType: hard - -"clsx@npm:^2.0.0": - version: 2.0.0 - resolution: "clsx@npm:2.0.0" - checksum: 10/943766d1b02fee3538c871e56638d87f973fbc2d6291ce221215ea436fdecb9be97ad323f411839c2d52c45640c449b1a53fbfe7e8b3d529b4e263308b630c9a +"clsx@npm:^2.0.0, clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919 languageName: node linkType: hard @@ -14007,14 +13913,14 @@ __metadata: languageName: node linkType: hard -"css-minimizer-webpack-plugin@npm:7.0.0": - version: 7.0.0 - resolution: "css-minimizer-webpack-plugin@npm:7.0.0" +"css-minimizer-webpack-plugin@npm:7.0.2": + version: 7.0.2 + resolution: "css-minimizer-webpack-plugin@npm:7.0.2" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.25" - cssnano: "npm:^7.0.1" + cssnano: "npm:^7.0.4" jest-worker: "npm:^29.7.0" - postcss: "npm:^8.4.38" + postcss: "npm:^8.4.40" schema-utils: "npm:^4.2.0" serialize-javascript: "npm:^6.0.2" peerDependencies: @@ -14032,7 +13938,7 @@ __metadata: optional: true lightningcss: optional: true - checksum: 10/47d8f8a38c97496759f1676b5344231d48bfb205cc272e163a113d4cd40daddd17f8eb719d1f1071cf3ec4d62fd83537801cba598d7c653657aa97f1461e4a8b + checksum: 10/80ada5f059900a3b474e33ff0f10c0e5f5a37b7525fa7ef30812b6049bcbc28fbda1d703d8f76724a97449114306de5cd7a7af15003a16784e6c9cc374dfedc8 languageName: node linkType: hard @@ -14142,13 +14048,6 @@ __metadata: languageName: node linkType: hard -"csscolorparser@npm:~1.0.2": - version: 1.0.3 - resolution: "csscolorparser@npm:1.0.3" - checksum: 10/b46b9032eaace69e5bb151bb64473547d8e48813b45395c1923cde1c87674bbd030c99536f44373e092de6afdca1a8134933a1503e779aaeb703b7c7897f5eca - languageName: node - linkType: hard - "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -14172,64 +14071,64 @@ __metadata: languageName: node linkType: hard -"cssnano-preset-default@npm:^7.0.6": - version: 7.0.6 - resolution: "cssnano-preset-default@npm:7.0.6" +"cssnano-preset-default@npm:^7.0.7": + version: 7.0.7 + resolution: "cssnano-preset-default@npm:7.0.7" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" css-declaration-sorter: "npm:^7.2.0" - cssnano-utils: "npm:^5.0.0" - postcss-calc: "npm:^10.0.2" - postcss-colormin: "npm:^7.0.2" - postcss-convert-values: "npm:^7.0.4" - postcss-discard-comments: "npm:^7.0.3" - postcss-discard-duplicates: "npm:^7.0.1" - postcss-discard-empty: "npm:^7.0.0" - postcss-discard-overridden: "npm:^7.0.0" - postcss-merge-longhand: "npm:^7.0.4" - postcss-merge-rules: "npm:^7.0.4" - postcss-minify-font-values: "npm:^7.0.0" - postcss-minify-gradients: "npm:^7.0.0" - postcss-minify-params: "npm:^7.0.2" - postcss-minify-selectors: "npm:^7.0.4" - postcss-normalize-charset: "npm:^7.0.0" - postcss-normalize-display-values: "npm:^7.0.0" - postcss-normalize-positions: "npm:^7.0.0" - postcss-normalize-repeat-style: "npm:^7.0.0" - postcss-normalize-string: "npm:^7.0.0" - postcss-normalize-timing-functions: "npm:^7.0.0" - postcss-normalize-unicode: "npm:^7.0.2" - postcss-normalize-url: "npm:^7.0.0" - postcss-normalize-whitespace: "npm:^7.0.0" - postcss-ordered-values: "npm:^7.0.1" - postcss-reduce-initial: "npm:^7.0.2" - postcss-reduce-transforms: "npm:^7.0.0" - postcss-svgo: "npm:^7.0.1" - postcss-unique-selectors: "npm:^7.0.3" + cssnano-utils: "npm:^5.0.1" + postcss-calc: "npm:^10.1.1" + postcss-colormin: "npm:^7.0.3" + postcss-convert-values: "npm:^7.0.5" + postcss-discard-comments: "npm:^7.0.4" + postcss-discard-duplicates: "npm:^7.0.2" + postcss-discard-empty: "npm:^7.0.1" + postcss-discard-overridden: "npm:^7.0.1" + postcss-merge-longhand: "npm:^7.0.5" + postcss-merge-rules: "npm:^7.0.5" + postcss-minify-font-values: "npm:^7.0.1" + postcss-minify-gradients: "npm:^7.0.1" + postcss-minify-params: "npm:^7.0.3" + postcss-minify-selectors: "npm:^7.0.5" + postcss-normalize-charset: "npm:^7.0.1" + postcss-normalize-display-values: "npm:^7.0.1" + postcss-normalize-positions: "npm:^7.0.1" + postcss-normalize-repeat-style: "npm:^7.0.1" + postcss-normalize-string: "npm:^7.0.1" + postcss-normalize-timing-functions: "npm:^7.0.1" + postcss-normalize-unicode: "npm:^7.0.3" + postcss-normalize-url: "npm:^7.0.1" + postcss-normalize-whitespace: "npm:^7.0.1" + postcss-ordered-values: "npm:^7.0.2" + postcss-reduce-initial: "npm:^7.0.3" + postcss-reduce-transforms: "npm:^7.0.1" + postcss-svgo: "npm:^7.0.2" + postcss-unique-selectors: "npm:^7.0.4" peerDependencies: - postcss: ^8.4.31 - checksum: 10/686e7652d01ad4337dbad17b22fdb9cf132cf4664fddd05194da13a1f44f1177697745bbc6da73083941356280e89fe2cceacb6f422cc4522d70ff51db83cd63 + postcss: ^8.4.32 + checksum: 10/1ca9b739531acc2dff66347cc1b6195da0549058b5b00b9d36c3f241535ad67476218f61201cfb15e8b460357ec42414aa53bf78a7f01ee26ac26b7852e6c244 languageName: node linkType: hard -"cssnano-utils@npm:^5.0.0": - version: 5.0.0 - resolution: "cssnano-utils@npm:5.0.0" +"cssnano-utils@npm:^5.0.1": + version: 5.0.1 + resolution: "cssnano-utils@npm:5.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/89ed5b8ca554697b4ae285e0d3e134fccc9a0471adda57c8fba17a2bace2f062b9fcf7aeaf66fbd7fabddca8a15a6b1e5ccb70a2783421ae1ac164f779d9f24e + postcss: ^8.4.32 + checksum: 10/cdf37315d3cf9726e10ce842b18e148e4df1d1d18d292540e724d5a96994901abc631c8894328c39ab70c864449a8a83f8fc117114fdcbade204e5e65898af90 languageName: node linkType: hard -"cssnano@npm:^7.0.1": - version: 7.0.6 - resolution: "cssnano@npm:7.0.6" +"cssnano@npm:^7.0.4": + version: 7.0.7 + resolution: "cssnano@npm:7.0.7" dependencies: - cssnano-preset-default: "npm:^7.0.6" - lilconfig: "npm:^3.1.2" + cssnano-preset-default: "npm:^7.0.7" + lilconfig: "npm:^3.1.3" peerDependencies: - postcss: ^8.4.31 - checksum: 10/12b1e1f2b52ff2ba0ecb470e51f8fb3298d976bf91a51c7d2854793ea1e2af5d3c40385a85ad82d2117c84b9528d08f4bfecbb14949c6014d953dae34260952b + postcss: ^8.4.32 + checksum: 10/c5b3123757834537f818e0f3eb6b20da51a194fefed599632f7ddd600c9e25d38abe38a22582a579660a49368a146c294e2096b2837cbeeda51ddfc85b108601 languageName: node linkType: hard @@ -15141,10 +15040,10 @@ __metadata: languageName: node linkType: hard -"diff@npm:^7.0.0": - version: 7.0.0 - resolution: "diff@npm:7.0.0" - checksum: 10/e9b8e48d054c9c0c093c65ce8e2637af94b35f2427001607b14e5e0589e534ea3413a7f91ebe6d7c5a1494ace49cb7c7c3972f442ddd96a4767ff091999a082e +"diff@npm:^8.0.0": + version: 8.0.2 + resolution: "diff@npm:8.0.2" + checksum: 10/82a2120d3418f97822e17a6044ccd4b99a91e26e145e8698353673d7146bd2d092bbebb79c112aae7badc7b9c526f9098cbe342f96174feb6beabdd2587b3c42 languageName: node linkType: hard @@ -15409,17 +15308,17 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": +"duplexer@npm:^0.1.2": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: 10/62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 languageName: node linkType: hard -"earcut@npm:^2.2.3": - version: 2.2.4 - resolution: "earcut@npm:2.2.4" - checksum: 10/ca8b24714cc2fa67f98fbca6ddcf64bb42ee8d75d0b4f1a81486b3282b0f7f1bf9ec49ad4d02149985886a0c8a03a173463f2acb1f51fa0bb7ba2e1d4aa1254d +"earcut@npm:^3.0.0": + version: 3.0.1 + resolution: "earcut@npm:3.0.1" + checksum: 10/92786b5a35276a099eb2545bd7183e31a8ddd34f0d24d26eb26a280bcf7d35dcf5a01cd8712343bfbe504849229d01ff4771aa4ed0d8d900083afaa01d732b5c languageName: node linkType: hard @@ -15458,17 +15357,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.160": - version: 1.5.167 - resolution: "electron-to-chromium@npm:1.5.167" - checksum: 10/078093a38e7295e575f381943f62914f49b53dd73506af2ce3e59332835c42b487ad02ff1207dfdcb33a5886d74a98e352c04431c0537366d9999a79c7d15c94 - languageName: node - linkType: hard - -"emitter-component@npm:^1.1.1": - version: 1.1.1 - resolution: "emitter-component@npm:1.1.1" - checksum: 10/b43814692fb874c1a75c3c417670123ab33961225b73fa680501dbd5d4ab779ef37082570fb64516bf12cedd906842eea0bccbf1d3bc530162e86bafa17d2737 +"electron-to-chromium@npm:^1.5.173": + version: 1.5.180 + resolution: "electron-to-chromium@npm:1.5.180" + checksum: 10/8d7f68650427f6bcb107ee1dcbe18f68b5c582601653095bec653fa898bd8427be4b1836581ce74405dca7cb36ebfc265a85c186e750b4de6e6f3ac4cfeef71b languageName: node linkType: hard @@ -15842,12 +15734,12 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" dependencies: es-errors: "npm:^1.3.0" - checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 languageName: node linkType: hard @@ -15947,35 +15839,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.25.0, esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:^0.25.0": - version: 0.25.0 - resolution: "esbuild@npm:0.25.0" +"esbuild@npm:0.25.6, esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:^0.25.0": + version: 0.25.6 + resolution: "esbuild@npm:0.25.6" dependencies: - "@esbuild/aix-ppc64": "npm:0.25.0" - "@esbuild/android-arm": "npm:0.25.0" - "@esbuild/android-arm64": "npm:0.25.0" - "@esbuild/android-x64": "npm:0.25.0" - "@esbuild/darwin-arm64": "npm:0.25.0" - "@esbuild/darwin-x64": "npm:0.25.0" - "@esbuild/freebsd-arm64": "npm:0.25.0" - "@esbuild/freebsd-x64": "npm:0.25.0" - "@esbuild/linux-arm": "npm:0.25.0" - "@esbuild/linux-arm64": "npm:0.25.0" - "@esbuild/linux-ia32": "npm:0.25.0" - "@esbuild/linux-loong64": "npm:0.25.0" - "@esbuild/linux-mips64el": "npm:0.25.0" - "@esbuild/linux-ppc64": "npm:0.25.0" - "@esbuild/linux-riscv64": "npm:0.25.0" - "@esbuild/linux-s390x": "npm:0.25.0" - "@esbuild/linux-x64": "npm:0.25.0" - "@esbuild/netbsd-arm64": "npm:0.25.0" - "@esbuild/netbsd-x64": "npm:0.25.0" - "@esbuild/openbsd-arm64": "npm:0.25.0" - "@esbuild/openbsd-x64": "npm:0.25.0" - "@esbuild/sunos-x64": "npm:0.25.0" - "@esbuild/win32-arm64": "npm:0.25.0" - "@esbuild/win32-ia32": "npm:0.25.0" - "@esbuild/win32-x64": "npm:0.25.0" + "@esbuild/aix-ppc64": "npm:0.25.6" + "@esbuild/android-arm": "npm:0.25.6" + "@esbuild/android-arm64": "npm:0.25.6" + "@esbuild/android-x64": "npm:0.25.6" + "@esbuild/darwin-arm64": "npm:0.25.6" + "@esbuild/darwin-x64": "npm:0.25.6" + "@esbuild/freebsd-arm64": "npm:0.25.6" + "@esbuild/freebsd-x64": "npm:0.25.6" + "@esbuild/linux-arm": "npm:0.25.6" + "@esbuild/linux-arm64": "npm:0.25.6" + "@esbuild/linux-ia32": "npm:0.25.6" + "@esbuild/linux-loong64": "npm:0.25.6" + "@esbuild/linux-mips64el": "npm:0.25.6" + "@esbuild/linux-ppc64": "npm:0.25.6" + "@esbuild/linux-riscv64": "npm:0.25.6" + "@esbuild/linux-s390x": "npm:0.25.6" + "@esbuild/linux-x64": "npm:0.25.6" + "@esbuild/netbsd-arm64": "npm:0.25.6" + "@esbuild/netbsd-x64": "npm:0.25.6" + "@esbuild/openbsd-arm64": "npm:0.25.6" + "@esbuild/openbsd-x64": "npm:0.25.6" + "@esbuild/openharmony-arm64": "npm:0.25.6" + "@esbuild/sunos-x64": "npm:0.25.6" + "@esbuild/win32-arm64": "npm:0.25.6" + "@esbuild/win32-ia32": "npm:0.25.6" + "@esbuild/win32-x64": "npm:0.25.6" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -16019,6 +15912,8 @@ __metadata: optional: true "@esbuild/openbsd-x64": optional: true + "@esbuild/openharmony-arm64": + optional: true "@esbuild/sunos-x64": optional: true "@esbuild/win32-arm64": @@ -16029,7 +15924,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/451daf6a442df29ec5d528587caa4ce783d41ff4acb93252da5a852b8d36c22e9f84d17f6721d4fbef9a1ba9855bc9fe1f167dd732c11665fe53032f2b89f114 + checksum: 10/b1c94893d53e39f3be02493c3a082bc9d090ca1702bf6017967ace1391d31e596da4dfd87e02efef3b7b4f0426918dbc5aa6909420a6ba42d51a872266ab6f2e languageName: node linkType: hard @@ -16248,11 +16143,11 @@ __metadata: linkType: hard "eslint-plugin-no-barrel-files@npm:^1.1.1": - version: 1.2.0 - resolution: "eslint-plugin-no-barrel-files@npm:1.2.0" + version: 1.2.1 + resolution: "eslint-plugin-no-barrel-files@npm:1.2.1" dependencies: "@typescript-eslint/utils": "npm:^8.13.0" - checksum: 10/da54a4930ddfd7d14bd82766e857af9eea5315125a2599d5ba03326530214e0d416f78915335d6c239b19df21b989196ffc4146919eff05477cef7e34261b946 + checksum: 10/22a888dd3b49527bed0f92758f4c7f34e78397c9197688db1936d2bdad8369f9ded2a1231a6b85e54791f059784f1d7bcdbeeb21cf0b453399044a1957a153c4 languageName: node linkType: hard @@ -16265,9 +16160,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react@npm:7.37.4": - version: 7.37.4 - resolution: "eslint-plugin-react@npm:7.37.4" +"eslint-plugin-react@npm:7.37.5": + version: 7.37.5 + resolution: "eslint-plugin-react@npm:7.37.5" dependencies: array-includes: "npm:^3.1.8" array.prototype.findlast: "npm:^1.2.5" @@ -16279,7 +16174,7 @@ __metadata: hasown: "npm:^2.0.2" jsx-ast-utils: "npm:^2.4.1 || ^3.0.0" minimatch: "npm:^3.1.2" - object.entries: "npm:^1.1.8" + object.entries: "npm:^1.1.9" object.fromentries: "npm:^2.0.8" object.values: "npm:^1.2.1" prop-types: "npm:^15.8.1" @@ -16289,7 +16184,7 @@ __metadata: string.prototype.repeat: "npm:^1.0.0" peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - checksum: 10/c538c10665c87cb90a0bcc4efe53a758570db10997d079d31474a9760116ef5584648fa22403d889ca672df8071bda10b40434ea0499e5ee8360bc5c8aba1679 + checksum: 10/ee1bd4e0ec64f29109d5a625bb703d179c82e0159c86c3f1b52fc1209d2994625a137dae303c333fb308a2e38315e44066d5204998177e31974382f9fda25d5c languageName: node linkType: hard @@ -16358,10 +16253,10 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.0": - version: 4.2.0 - resolution: "eslint-visitor-keys@npm:4.2.0" - checksum: 10/9651b3356b01760e586b4c631c5268c0e1a85236e3292bf754f0472f465bf9a856c0ddc261fceace155334118c0151778effafbab981413dbf9288349343fa25 +"eslint-visitor-keys@npm:^4.2.0, eslint-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 10/3ee00fc6a7002d4b0ffd9dc99e13a6a7882c557329e6c25ab254220d71e5c9c4f89dca4695352949ea678eb1f3ba912a18ef8aac0a7fe094196fd92f441bfce2 languageName: node linkType: hard @@ -16676,12 +16571,12 @@ __metadata: languageName: node linkType: hard -"expose-loader@npm:5.0.0": - version: 5.0.0 - resolution: "expose-loader@npm:5.0.0" +"expose-loader@npm:5.0.1": + version: 5.0.1 + resolution: "expose-loader@npm:5.0.1" peerDependencies: webpack: ^5.0.0 - checksum: 10/6a77c79744e41f81fd4da1eb07c319f13a097be85c4078380361fd6c69ac69576539bdd81a07d50b36b44ff1c2c5c97c1f4d2e75f0a14db391153bfb7035709b + checksum: 10/8e7c5eab2ed78f182845425e99b1ab270d71b9dfbf23b032839757d7aa2e8d8ddfd8aaeda996d26f4f6374c34e3293914db1d4b396f53be95241b85226cd40ff languageName: node linkType: hard @@ -16982,6 +16877,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.3": + version: 6.4.6 + resolution: "fdir@npm:6.4.6" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/c186ba387e7b75ccf874a098d9bc5fe0af0e9c52fc56f8eac8e80aa4edb65532684bf2bf769894ff90f53bf221d6136692052d31f07a9952807acae6cbe7ee50 + languageName: node + linkType: hard + "fflate@npm:^0.8.2": version: 0.8.2 resolution: "fflate@npm:0.8.2" @@ -17310,13 +17217,13 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0": - version: 3.1.1 - resolution: "foreground-child@npm:3.1.1" +"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" dependencies: - cross-spawn: "npm:^7.0.0" + cross-spawn: "npm:^7.0.6" signal-exit: "npm:^4.0.1" - checksum: 10/087edd44857d258c4f73ad84cb8df980826569656f2550c341b27adf5335354393eec24ea2fabd43a253233fb27cee177ebe46bd0b7ea129c77e87cb1e9936fb + checksum: 10/427b33f997a98073c0424e5c07169264a62cda806d8d2ded159b5b903fdfc8f0a1457e06b5fc35506497acb3f1e353f025edee796300209ac6231e80edece835 languageName: node linkType: hard @@ -17633,9 +17540,9 @@ __metadata: languageName: node linkType: hard -"geotiff@npm:^2.0.7": - version: 2.0.7 - resolution: "geotiff@npm:2.0.7" +"geotiff@npm:^2.1.3": + version: 2.1.3 + resolution: "geotiff@npm:2.1.3" dependencies: "@petamoriken/float16": "npm:^3.4.7" lerc: "npm:^3.0.0" @@ -17644,7 +17551,8 @@ __metadata: quick-lru: "npm:^6.1.1" web-worker: "npm:^1.2.0" xml-utils: "npm:^1.0.2" - checksum: 10/7d309ef9e7fee0aef39117ef045cf35d30be91621e0df643cd86d363519907706bdc6e0f6f4dc2ebcc0ce085eab18fd5801aca9c162606cdd293c7b616070d1a + zstddec: "npm:^0.1.0" + checksum: 10/91475680b882c11e6c28b4538c8703da03a13fa1b3f8f9d456aa3b274cb71fb01f04aadb29006c3a607c20b724b936a95899ad8ef65d26a11eed59ba9f81c61f languageName: node linkType: hard @@ -17665,10 +17573,10 @@ __metadata: languageName: node linkType: hard -"get-document@npm:1": +"get-document@github:webmodules/get-document#commit=a04ccb499d6e0433a368c3bb150b4899b698461f": version: 1.0.0 - resolution: "get-document@npm:1.0.0" - checksum: 10/94312db44ea9d070cca8cc14272582d04c7638494d6b5c9c0d6f1d949ef2605b4102ee52f0ded11e571357aba45e9fcc98a5b0467d61e4943a66002167eb6e7a + resolution: "get-document@https://github.com/webmodules/get-document.git#commit=a04ccb499d6e0433a368c3bb150b4899b698461f" + checksum: 10/5ec51321f3a5257a08c3ea9ac1984d64302a34c786f5d5691ae1ee00aba189eedac63f401e0cf746b0c77fb84091a4d2a9b66ac23dc0e872cbac860e5e0c5245 languageName: node linkType: hard @@ -17679,21 +17587,21 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7": - version: 1.2.7 - resolution: "get-intrinsic@npm:1.2.7" +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": + version: 1.3.0 + resolution: "get-intrinsic@npm:1.3.0" dependencies: - call-bind-apply-helpers: "npm:^1.0.1" + call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" + es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" - get-proto: "npm:^1.0.0" + get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10/4f7149c9a826723f94c6d49f70bcb3df1d3f9213994fab3668f12f09fa72074681460fb29ebb6f135556ec6372992d63802386098791a8f09cfa6f27090fa67b + checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10 languageName: node linkType: hard @@ -17788,6 +17696,15 @@ __metadata: languageName: node linkType: hard +"get-user-locale@npm:^3.0.0": + version: 3.0.0 + resolution: "get-user-locale@npm:3.0.0" + dependencies: + memoize: "npm:^10.0.0" + checksum: 10/b95ce2cc9105d81d57acc75930f1a965be273d694b10ce2d6b0fc7e7aee616dc947ed69c71ec0059823fabaf036b8145501a7ec08af68558df5a123bc0401013 + languageName: node + linkType: hard + "get-window@npm:^1.1.1": version: 1.1.2 resolution: "get-window@npm:1.1.2" @@ -17879,12 +17796,12 @@ __metadata: languageName: node linkType: hard -"gitconfiglocal@npm:^1.0.0": - version: 1.0.0 - resolution: "gitconfiglocal@npm:1.0.0" +"gitconfiglocal@npm:2.1.0": + version: 2.1.0 + resolution: "gitconfiglocal@npm:2.1.0" dependencies: ini: "npm:^1.3.2" - checksum: 10/e6d2764c15bbab6d1d1000d1181bb907f6b3796bb04f63614dba571b18369e0ecb1beaf27ce8da5b24307ef607e3a5f262a67cb9575510b9446aac697d421beb + checksum: 10/4b4b44d992a6abf2900eec8cfe960dc36e0d3c2467d20ec69e0a0f13b6b7645b926daa004df42f94c34ad28a58529cf2522fa0bf261e4e7b95958fb451dcedda languageName: node linkType: hard @@ -17944,19 +17861,19 @@ __metadata: languageName: node linkType: hard -"glob@npm:11.0.1, glob@npm:^11.0.0": - version: 11.0.1 - resolution: "glob@npm:11.0.1" +"glob@npm:11.0.3, glob@npm:^11.0.0": + version: 11.0.3 + resolution: "glob@npm:11.0.3" dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^4.0.1" - minimatch: "npm:^10.0.0" + foreground-child: "npm:^3.3.1" + jackspeak: "npm:^4.1.1" + minimatch: "npm:^10.0.3" minipass: "npm:^7.1.2" package-json-from-dist: "npm:^1.0.0" path-scurry: "npm:^2.0.0" bin: glob: dist/esm/bin.mjs - checksum: 10/57b12a05cc25f1c38f3b24cf6ea7a8bacef11e782c4b9a8c5b0bef3e6c5bcb8c4548cb31eb4115592e0490a024c1bde7359c470565608dd061d3b21179740457 + checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893 languageName: node linkType: hard @@ -18086,7 +18003,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:11.1.0, globby@npm:^11.0.0, globby@npm:^11.1.0": +"globby@npm:^11.0.0, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -18178,7 +18095,7 @@ __metadata: "@emotion/eslint-plugin": "npm:11.12.0" "@emotion/react": "npm:11.14.0" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" - "@floating-ui/react": "npm:0.27.12" + "@floating-ui/react": "npm:0.27.13" "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" @@ -18192,9 +18109,9 @@ __metadata: "@grafana/faro-web-sdk": "npm:^1.13.2" "@grafana/faro-web-tracing": "npm:^1.13.2" "@grafana/flamegraph": "workspace:*" - "@grafana/google-sdk": "npm:0.3.2" + "@grafana/google-sdk": "npm:0.3.4" "@grafana/i18n": "workspace:*" - "@grafana/lezer-logql": "npm:0.2.7" + "@grafana/lezer-logql": "npm:0.2.8" "@grafana/llm": "npm:0.22.1" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" @@ -18202,8 +18119,8 @@ __metadata: "@grafana/plugin-ui": "npm:0.10.7" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:^6.26.1" - "@grafana/scenes-react": "npm:^6.26.1" + "@grafana/scenes": "npm:^6.27.2" + "@grafana/scenes-react": "npm:^6.27.2" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/test-utils": "workspace:*" @@ -18250,14 +18167,13 @@ __metadata: "@testing-library/user-event": "npm:14.6.1" "@types/babel__core": "npm:^7" "@types/babel__preset-env": "npm:^7" - "@types/chance": "npm:^1.1.3" + "@types/chance": "npm:^1.1.7" "@types/common-tags": "npm:^1.8.0" "@types/confusing-browser-globals": "npm:^1" "@types/d3": "npm:7.4.3" "@types/d3-force": "npm:^3.0.0" "@types/d3-scale-chromatic": "npm:3.1.0" "@types/debounce-promise": "npm:3.1.9" - "@types/diff": "npm:^7.0.0" "@types/eslint": "npm:9.6.1" "@types/eslint-scope": "npm:^3.7.7" "@types/file-saver": "npm:2.0.7" @@ -18270,7 +18186,7 @@ __metadata: "@types/jquery": "npm:3.5.32" "@types/js-yaml": "npm:^4.0.5" "@types/jsurl": "npm:^1.2.28" - "@types/lodash": "npm:4.17.15" + "@types/lodash": "npm:4.17.20" "@types/logfmt": "npm:^1.2.3" "@types/lucene": "npm:^2" "@types/node": "npm:22.15.0" @@ -18287,7 +18203,7 @@ __metadata: "@types/react-router-dom": "npm:5.3.3" "@types/react-table": "npm:7.7.20" "@types/react-transition-group": "npm:4.4.12" - "@types/react-virtualized-auto-sizer": "npm:1.0.4" + "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/react-window": "npm:1.8.8" "@types/react-window-infinite-loader": "npm:^1" "@types/redux-mock-store": "npm:1.5.0" @@ -18296,14 +18212,14 @@ __metadata: "@types/slate-plain-serializer": "npm:0.7.5" "@types/slate-react": "npm:0.22.9" "@types/swagger-ui-react": "npm:5.18.0" - "@types/systemjs": "npm:6.15.1" + "@types/systemjs": "npm:6.15.3" "@types/tinycolor2": "npm:1.4.6" "@types/uuid": "npm:10.0.0" "@types/webpack-assets-manifest": "npm:^5" "@types/webpack-env": "npm:^1.18.4" "@types/yargs": "npm:17.0.33" - "@typescript-eslint/eslint-plugin": "npm:8.22.0" - "@typescript-eslint/parser": "npm:8.22.0" + "@typescript-eslint/eslint-plugin": "npm:8.35.1" + "@typescript-eslint/parser": "npm:8.35.1" "@visx/event": "npm:3.12.0" "@visx/gradient": "npm:3.12.0" "@visx/group": "npm:3.12.0" @@ -18311,15 +18227,15 @@ __metadata: "@visx/tooltip": "npm:3.12.0" "@welldone-software/why-did-you-render": "npm:8.0.3" ansicolor: "npm:2.0.3" - autoprefixer: "npm:10.4.20" + autoprefixer: "npm:10.4.21" babel-loader: "npm:9.2.1" baron: "npm:3.0.3" blob-polyfill: "npm:9.0.20240710" brace: "npm:0.11.1" browserslist: "npm:^4.21.4" centrifuge: "npm:5.3.5" - chance: "npm:^1.0.10" - chrome-remote-interface: "npm:0.33.2" + chance: "npm:^1.1.13" + chrome-remote-interface: "npm:0.33.3" classnames: "npm:2.5.1" codeowners: "npm:^5.1.1" combokeys: "npm:^3.0.0" @@ -18331,7 +18247,7 @@ __metadata: crashme: "npm:0.0.15" croner: "npm:^9.0.0" css-loader: "npm:7.1.2" - css-minimizer-webpack-plugin: "npm:7.0.0" + css-minimizer-webpack-plugin: "npm:7.0.2" cypress: "npm:14.3.2" cypress-file-upload: "npm:5.0.8" cypress-recurse: "npm:^1.35.3" @@ -18341,8 +18257,8 @@ __metadata: dangerously-set-html-content: "npm:1.1.0" date-fns: "npm:4.1.0" debounce-promise: "npm:3.1.2" - diff: "npm:^7.0.0" - esbuild: "npm:0.25.0" + diff: "npm:^8.0.0" + esbuild: "npm:0.25.6" esbuild-loader: "npm:4.3.0" esbuild-plugin-browserslist: "npm:^1.0.0" eslint: "npm:9.19.0" @@ -18354,24 +18270,24 @@ __metadata: eslint-plugin-jsx-a11y: "npm:6.10.2" eslint-plugin-lodash: "npm:8.0.0" eslint-plugin-no-barrel-files: "npm:^1.1.1" - eslint-plugin-react: "npm:7.37.4" + eslint-plugin-react: "npm:7.37.5" eslint-plugin-react-hooks: "npm:5.1.0" eslint-plugin-testing-library: "npm:^7.0.0" eslint-plugin-unicorn: "npm:^56.0.0" eslint-scope: "npm:^8.1.0" eslint-webpack-plugin: "npm:4.2.0" - expose-loader: "npm:5.0.0" + expose-loader: "npm:5.0.1" fast-deep-equal: "npm:^3.1.3" fast-json-patch: "npm:3.1.1" file-saver: "npm:2.0.5" fishery: "npm:^2.2.2" fork-ts-checker-webpack-plugin: "npm:9.0.2" - glob: "npm:11.0.1" + glob: "npm:11.0.3" history: "npm:4.10.1" html-loader: "npm:5.1.0" html-webpack-plugin: "npm:5.6.3" http-server: "npm:14.1.1" - i18next: "npm:^24.0.0" + i18next: "npm:^25.0.0" i18next-browser-languagedetector: "npm:^8.0.0" i18next-parser: "npm:9.3.0" i18next-pseudo: "npm:^2.2.1" @@ -18395,14 +18311,15 @@ __metadata: json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" kbar: "npm:0.1.0-beta.45" - lerna: "npm:8.2.1" + lerna: "npm:8.2.3" leven: "npm:^4.0.0" lodash: "npm:4.17.21" logfmt: "npm:^1.3.2" + lossless-json: "npm:^4.1.1" lru-cache: "npm:11.1.0" lru-memoize: "npm:^1.1.0" lucene: "npm:^2.1.1" - marked: "npm:15.0.12" + marked: "npm:16.0.0" memoize-one: "npm:6.0.0" micro-memoize: "npm:^4.1.2" mini-css-extract-plugin: "npm:2.9.2" @@ -18412,20 +18329,20 @@ __metadata: moment-timezone: "npm:0.5.47" monaco-editor: "npm:0.34.1" moveable: "npm:0.53.0" - msw: "npm:2.10.2" + msw: "npm:2.10.4" mutationobserver-shim: "npm:0.3.7" nanoid: "npm:^5.0.9" node-forge: "npm:^1.3.1" node-notifier: "npm:10.0.1" nx: "npm:20.7.1" - ol: "npm:7.4.0" + ol: "npm:10.6.0" ol-ext: "npm:4.0.33" openapi-types: "npm:^12.1.3" pa11y-ci: "npm:^3.1.0" pdf-parse: "npm:^1.1.1" plop: "npm:^4.0.1" pluralize: "npm:^8.0.0" - postcss: "npm:8.5.1" + postcss: "npm:8.5.6" postcss-loader: "npm:8.1.1" postcss-reporter: "npm:7.1.0" postcss-scss: "npm:4.0.9" @@ -18438,7 +18355,7 @@ __metadata: react: "npm:18.3.1" react-diff-viewer-continued: "npm:^3.4.0" react-dom: "npm:18.3.1" - react-draggable: "npm:4.4.6" + react-draggable: "npm:4.5.0" react-dropzone: "npm:^14.2.3" react-grid-layout: "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch" react-highlight-words: "npm:0.21.0" @@ -18454,14 +18371,14 @@ __metadata: react-router: "npm:5.3.4" react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" - react-select: "npm:5.10.1" + react-select: "npm:5.10.2" react-select-event: "npm:5.5.1" react-split-pane: "npm:0.1.92" react-table: "npm:7.8.0" react-transition-group: "npm:4.4.5" react-use: "npm:17.6.0" react-virtual: "npm:2.10.4" - react-virtualized-auto-sizer: "npm:1.0.25" + react-virtualized-auto-sizer: "npm:1.0.26" react-window: "npm:1.8.11" react-window-infinite-loader: "npm:1.0.10" react-zoom-pan-pinch: "npm:^3.3.0" @@ -18484,7 +18401,7 @@ __metadata: style-loader: "npm:4.0.0" stylelint: "npm:16.14.1" stylelint-config-sass-guidelines: "npm:12.1.0" - swagger-ui-react: "npm:5.25.2" + swagger-ui-react: "npm:5.26.2" symbol-observable: "npm:4.0.0" systemjs: "npm:6.15.1" terser-webpack-plugin: "npm:5.3.11" @@ -18496,10 +18413,11 @@ __metadata: tslib: "npm:2.8.1" tween-functions: "npm:^1.2.0" type-fest: "npm:^4.18.2" - typescript: "npm:5.7.3" + typescript: "npm:5.8.3" uplot: "npm:1.6.32" uuid: "npm:11.1.0" - visjs-network: "npm:4.25.0" + vis-data: "npm:^7.1.10" + vis-network: "npm:9.1.13" webpack: "npm:5.97.1" webpack-assets-manifest: "npm:^5.1.0" webpack-cli: "npm:6.0.1" @@ -18558,13 +18476,6 @@ __metadata: languageName: node linkType: hard -"hammerjs@npm:^2.0.8": - version: 2.0.8 - resolution: "hammerjs@npm:2.0.8" - checksum: 10/9155d056f252ef35e8ca258dbb5ee2c9d8794f6805d083da7d1d9763d185e3e149459ecc2b36ccce584e3cd5f099fd9fa55056e3bcc7724046390f2e5ae25815 - languageName: node - linkType: hard - "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -19297,17 +19208,31 @@ __metadata: languageName: node linkType: hard -"i18next@npm:^23.5.1 || ^24.2.0, i18next@npm:^24.0.0": - version: 24.2.2 - resolution: "i18next@npm:24.2.2" +"i18next@npm:^23.5.1 || ^24.2.0": + version: 24.2.3 + resolution: "i18next@npm:24.2.3" dependencies: - "@babel/runtime": "npm:^7.23.2" + "@babel/runtime": "npm:^7.26.10" peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true - checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 + checksum: 10/6c73d964f2a98b1aa2c2717fe6da66fc265bcbbc5fcd52b2bfff51ff013d30d4f7d7449c4eb7f464d27af43e2e73f2e7f1d46a144d731bd3bdb1385d4c199e4c + languageName: node + linkType: hard + +"i18next@npm:^25.0.0": + version: 25.3.2 + resolution: "i18next@npm:25.3.2" + dependencies: + "@babel/runtime": "npm:^7.27.6" + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/fb6b2035cc8f3bcc89f56e164d22cefbefd54e5a569315b20ddcfa6e1b68c48962307181b271fea7e5ee37ddfaa90721a4a7f7814b739f24bf00a3a670b8eb93 languageName: node linkType: hard @@ -19338,7 +19263,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.1.12, ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 @@ -19361,17 +19286,17 @@ __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.2.0, ignore@npm:^5.2.4": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 languageName: node linkType: hard -"ignore@npm:^7.0.3": - version: 7.0.3 - resolution: "ignore@npm:7.0.3" - checksum: 10/ce5e812af3acd6607a3fe0a9f9b5f01d53f009a5ace8cbf5b6491d05a481b55d65186e6a7eaa13126e93f15276bcf3d1e8d6ff3ce5549c312f9bb313fff33365 +"ignore@npm:^7.0.0, ignore@npm:^7.0.3": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 languageName: node linkType: hard @@ -20520,16 +20445,12 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^4.0.1": - version: 4.0.1 - resolution: "jackspeak@npm:4.0.1" +"jackspeak@npm:^4.1.1": + version: 4.1.1 + resolution: "jackspeak@npm:4.1.1" dependencies: "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10/b20dc0df0dbb2903e4d540ae68308ec7d1dd60944b130e867e218c98b5d77481d65ea734b6c81c812d481500076e8b3fdfccfb38fc81cb1acf165e853da3e26c + checksum: 10/ffceb270ec286841f48413bfb4a50b188662dfd599378ce142b6540f3f0a66821dc9dcb1e9ebc55c6c3b24dc2226c96e5819ba9bd7a241bd29031b61911718c7 languageName: node linkType: hard @@ -21385,13 +21306,6 @@ __metadata: languageName: node linkType: hard -"json-stringify-pretty-compact@npm:^2.0.0": - version: 2.0.0 - resolution: "json-stringify-pretty-compact@npm:2.0.0" - checksum: 10/08f2725eef0a37bfd554aaa1ce551bc973e13f2c3dc86dd40b10ae6c08b3851b84946db672877e994e1fd1d904a578941c176609864e4f1a48b6d8f51fc2ac5f - languageName: node - linkType: hard - "json-stringify-safe@npm:^5.0.1, json-stringify-safe@npm:~5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" @@ -21519,13 +21433,6 @@ __metadata: languageName: node linkType: hard -"keycharm@npm:^0.2.0": - version: 0.2.0 - resolution: "keycharm@npm:0.2.0" - checksum: 10/9c0f227fddfb8f82dbe6ff6450c1d3110d10715eaab2034ef656ce1be4900a979ce7994ecef0ef8bfc190c9f11c1a82e5f454052fa92142c44e6843bcff1249f - languageName: node - linkType: hard - "keycode@npm:^2.2.0": version: 2.2.0 resolution: "keycode@npm:2.2.0" @@ -21651,11 +21558,11 @@ __metadata: languageName: node linkType: hard -"lerna@npm:8.2.1": - version: 8.2.1 - resolution: "lerna@npm:8.2.1" +"lerna@npm:8.2.3": + version: 8.2.3 + resolution: "lerna@npm:8.2.3" dependencies: - "@lerna/create": "npm:8.2.1" + "@lerna/create": "npm:8.2.3" "@npmcli/arborist": "npm:7.5.4" "@npmcli/package-json": "npm:5.2.0" "@npmcli/run-script": "npm:8.1.0" @@ -21682,7 +21589,6 @@ __metadata: get-stream: "npm:6.0.0" git-url-parse: "npm:14.0.0" glob-parent: "npm:6.0.2" - globby: "npm:11.1.0" graceful-fs: "npm:4.2.11" has-unicode: "npm:2.0.1" import-local: "npm:3.1.0" @@ -21722,9 +21628,10 @@ __metadata: slash: "npm:3.0.0" ssri: "npm:^10.0.6" string-width: "npm:^4.2.3" - strong-log-transformer: "npm:2.1.0" tar: "npm:6.2.1" temp-dir: "npm:1.0.0" + through: "npm:2.3.8" + tinyglobby: "npm:0.2.12" typescript: "npm:>=3 < 6" upath: "npm:2.0.1" uuid: "npm:^10.0.0" @@ -21737,7 +21644,7 @@ __metadata: yargs-parser: "npm:21.1.1" bin: lerna: dist/cli.js - checksum: 10/ebf9fd1af102a8b7e89dcf05e32f92dfa2ce13e77c9788a86eb4828e6a5269e7bf85edf1bcdb4e4ea383f42d872880ad61fc26d304276715b3757fb54cd60d94 + checksum: 10/3ef9e5c6e2ee20cad0c750817cf628dffa0056f9b87ee4956f641833ac3b06a8fdf50d4cd6ba63a818427c7e6c1482568c9e184f0535fd23239ed55e5eae57a7 languageName: node linkType: hard @@ -21859,7 +21766,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^3.1.2, lilconfig@npm:^3.1.3": +"lilconfig@npm:^3.1.3": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3 @@ -22207,6 +22114,13 @@ __metadata: languageName: node linkType: hard +"lossless-json@npm:^4.1.1": + version: 4.1.1 + resolution: "lossless-json@npm:4.1.1" + checksum: 10/1e22e9e4ad8ebf169567a8c24553403bfd7abc33c141f4918f1b9626417f80477b849e6175a315a224e0c68072759c69a0e332a925de80269a7f9720af3a7fbd + languageName: node + linkType: hard + "loupe@npm:^3.1.0, loupe@npm:^3.1.1, loupe@npm:^3.1.2": version: 3.1.3 resolution: "loupe@npm:3.1.3" @@ -22451,19 +22365,12 @@ __metadata: languageName: node linkType: hard -"mapbox-to-css-font@npm:^2.4.1": - version: 2.4.1 - resolution: "mapbox-to-css-font@npm:2.4.1" - checksum: 10/224fc0f39a2f546bbe5b69a67a4e8c54928606cac44490650ee711723747edb393b4936c0005bcc72cb12f70b31fab50078a65e3e27e0f3e7d65f1ab8a3a253b - languageName: node - linkType: hard - -"marked-mangle@npm:1.1.10": - version: 1.1.10 - resolution: "marked-mangle@npm:1.1.10" +"marked-mangle@npm:1.1.11": + version: 1.1.11 + resolution: "marked-mangle@npm:1.1.11" peerDependencies: - marked: ">=4 <16" - checksum: 10/76ca4736e7f3839a5866329bfa2c274fb5699ef4962ed32b16b10b76c0aeb3ca5f67461041c9d7b8ab56900e84cb377aaa596a097f7327baa89342de442a138b + marked: ">=4 <17" + checksum: 10/e2872cbe62dc60347eaef2494049a3fb65bcef3f1097407dd8e3f946856085d3ce05b516ba0d2307cbf91019fd38ad25704200c9dc601a0da29d64c76c803027 languageName: node linkType: hard @@ -22484,12 +22391,12 @@ __metadata: languageName: node linkType: hard -"marked@npm:15.0.12": - version: 15.0.12 - resolution: "marked@npm:15.0.12" +"marked@npm:16.0.0": + version: 16.0.0 + resolution: "marked@npm:16.0.0" bin: marked: bin/marked.js - checksum: 10/deeb619405c0c46af00c99b18b3365450abeb309104b24e3658f46142344f6b7c4117608c3b5834084d8738e92f81240c19f596e6ee369260f96e52b3457eaee + checksum: 10/aed6501ff5d49def83f0c79fab698666338a555cae32f83ba52369c9a04aa8be4fa4c908367d12f81f7d1c1a83d89ffe47aa1e7b023da308591e03ea82d5ca6d languageName: node linkType: hard @@ -22607,6 +22514,15 @@ __metadata: languageName: node linkType: hard +"memoize@npm:^10.0.0": + version: 10.1.0 + resolution: "memoize@npm:10.1.0" + dependencies: + mimic-function: "npm:^5.0.1" + checksum: 10/77ac790f6f9ffa6dc666e5e47cf145106aacfec01a53e33738952f516914550813b864cd98f353233f871e1f0331195f5c8d1698ea0bdbdc978d7c171c8c167e + languageName: node + linkType: hard + "memoizerific@npm:^1.11.3": version: 1.11.3 resolution: "memoizerific@npm:1.11.3" @@ -22678,9 +22594,9 @@ __metadata: linkType: hard "micro-memoize@npm:^4.1.2": - version: 4.1.2 - resolution: "micro-memoize@npm:4.1.2" - checksum: 10/027e90c3147c97c07224440ea50ede27eb7d888149e4925820397b466d16efc525f5ec3981e4cadec3258a8d36dfd5e7e7c8e660879fbe2e47106785be9bc570 + version: 4.1.3 + resolution: "micro-memoize@npm:4.1.3" + checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402 languageName: node linkType: hard @@ -22751,7 +22667,7 @@ __metadata: languageName: node linkType: hard -"mimic-function@npm:^5.0.0": +"mimic-function@npm:^5.0.0, mimic-function@npm:^5.0.1": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c @@ -22820,12 +22736,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.0.0": - version: 10.0.1 - resolution: "minimatch@npm:10.0.1" +"minimatch@npm:^10.0.3": + version: 10.0.3 + resolution: "minimatch@npm:10.0.3" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/082e7ccbc090d5f8c4e4e029255d5a1d1e3af37bda837da2b8b0085b1503a1210c91ac90d9ebfe741d8a5f286ece820a1abb4f61dc1f82ce602a055d461d93f3 + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10/d5b8b2538b367f2cfd4aeef27539fddeee58d1efb692102b848e4a968a09780a302c530eb5aacfa8c57f7299155fb4b4e85219ad82664dcef5c66f657111d9b8 languageName: node linkType: hard @@ -23151,7 +23067,7 @@ __metadata: languageName: node linkType: hard -"moment@npm:2.30.1, moment@npm:^2.20.1, moment@npm:^2.29.4, moment@npm:^2.30.1": +"moment@npm:2.30.1, moment@npm:^2.29.4, moment@npm:^2.30.1": version: 2.30.1 resolution: "moment@npm:2.30.1" checksum: 10/ae42d876d4ec831ef66110bdc302c0657c664991e45cf2afffc4b0f6cd6d251dde11375c982a5c0564ccc0fa593fc564576ddceb8c8845e87c15f58aa6baca69 @@ -23233,9 +23149,9 @@ __metadata: languageName: node linkType: hard -"msw@npm:2.10.2, msw@npm:^2.10.2": - version: 2.10.2 - resolution: "msw@npm:2.10.2" +"msw@npm:2.10.4, msw@npm:^2.10.2": + version: 2.10.4 + resolution: "msw@npm:2.10.4" dependencies: "@bundled-es-modules/cookie": "npm:^2.0.1" "@bundled-es-modules/statuses": "npm:^1.0.1" @@ -23262,7 +23178,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: 10/bc90bc34a0b9e978e662f33fa630a0de66c4b6eff3a92b41efa08bf67d79b43e4a961a17b0d274580393c912f4ac6226c76a8bc33148799b1ee43477df712c26 + checksum: 10/e2f25dda1aba66c7444c29c41d3157cb15c0332055ab7ebfb74ef4b506e7b90098cf37c577768edb5b2b2dbf0d6ed6a7a3ca8ee6da3d72df5a25823d82f33316 languageName: node linkType: hard @@ -23358,12 +23274,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.8": - version: 3.3.8 - resolution: "nanoid@npm:3.3.8" +"nanoid@npm:^3.3.11": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" bin: nanoid: bin/nanoid.cjs - checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 + checksum: 10/73b5afe5975a307aaa3c95dfe3334c52cdf9ae71518176895229b8d65ab0d1c0417dd081426134eb7571c055720428ea5d57c645138161e7d10df80815527c48 languageName: node linkType: hard @@ -24095,14 +24011,15 @@ __metadata: languageName: node linkType: hard -"object.entries@npm:^1.1.8": - version: 1.1.8 - resolution: "object.entries@npm:1.1.8" +"object.entries@npm:^1.1.9": + version: 1.1.9 + resolution: "object.entries@npm:1.1.9" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/2301918fbd1ee697cf6ff7cd94f060c738c0a7d92b22fd24c7c250e9b593642c9707ad2c44d339303c1439c5967d8964251cdfc855f7f6ec55db2dd79e8dc2a7 + es-object-atoms: "npm:^1.1.1" + checksum: 10/24163ab1e1e013796693fc5f5d349e8b3ac0b6a34a7edb6c17d3dd45c6a8854145780c57d302a82512c1582f63720f4b4779d6c1cfba12cbb1420b978802d8a3 languageName: node linkType: hard @@ -24192,27 +24109,16 @@ __metadata: languageName: node linkType: hard -"ol-mapbox-style@npm:^10.1.0": +"ol@npm:10.6.0": version: 10.6.0 - resolution: "ol-mapbox-style@npm:10.6.0" + resolution: "ol@npm:10.6.0" dependencies: - "@mapbox/mapbox-gl-style-spec": "npm:^13.23.1" - mapbox-to-css-font: "npm:^2.4.1" - ol: "npm:^7.3.0" - checksum: 10/069421f5260b6d7b02e6f753eef5e9e10d3a86df8fe425aab1317b5e3d75684709b4b506e4c47e9840abeb503066c64a73dd29ec4a1cf65a8cbc484e91c70bfb - languageName: node - linkType: hard - -"ol@npm:7.4.0, ol@npm:^7.3.0": - version: 7.4.0 - resolution: "ol@npm:7.4.0" - dependencies: - earcut: "npm:^2.2.3" - geotiff: "npm:^2.0.7" - ol-mapbox-style: "npm:^10.1.0" - pbf: "npm:3.2.1" - rbush: "npm:^3.0.1" - checksum: 10/c83698fddd02fdd497e92bffee7ca7d4059f64d28a0be7a7781981f129b6636cacbee894aed331a572f066fe1e56a4cf22861c51e1efddb7fbb58c4bf5606ece + "@types/rbush": "npm:4.0.0" + earcut: "npm:^3.0.0" + geotiff: "npm:^2.1.3" + pbf: "npm:4.0.1" + rbush: "npm:^4.0.0" + checksum: 10/942fe6c0b55b24a6b1538d78ad221962476bfcba5b1316f427235d50aa817ca5ed805e8d3b5c6129b5da245ba0b66cd8bf359403230bb939648a1a78bffb0ca3 languageName: node linkType: hard @@ -25118,15 +25024,14 @@ __metadata: languageName: node linkType: hard -"pbf@npm:3.2.1": - version: 3.2.1 - resolution: "pbf@npm:3.2.1" +"pbf@npm:4.0.1": + version: 4.0.1 + resolution: "pbf@npm:4.0.1" dependencies: - ieee754: "npm:^1.1.12" resolve-protobuf-schema: "npm:^2.1.0" bin: pbf: bin/pbf - checksum: 10/566a64424063b07f46d3e2cb2288094a60b0a45efea48fd4030f527143b0e9c611399ebd3a6fd56db51908f2006defef5e09a1b2a027db9481a59a41156a540c + checksum: 10/9e2821fec1511cdaacf205c1279a33991119a2b8469208071d22e6204162dc8a4d216f647ce92759601e3d685fa913d4739278a86aa9ac126d04a1a3ac66bf12 languageName: node linkType: hard @@ -25168,7 +25073,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.1": +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 @@ -25366,79 +25271,79 @@ __metadata: languageName: node linkType: hard -"postcss-calc@npm:^10.0.2": - version: 10.0.2 - resolution: "postcss-calc@npm:10.0.2" +"postcss-calc@npm:^10.1.1": + version: 10.1.1 + resolution: "postcss-calc@npm:10.1.1" dependencies: - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4.38 - checksum: 10/12d497e632b4a12f7d33507ed6f74db2dd01f9b9cc1f9986271af16b118d25f959dc255777a91d742e0431f400a90b8540d00533fc0513f34c1840a491cf2bee + checksum: 10/16a25ec594cfbbda439fd2939820f78ed4e7b8b5ab458aed7283b05fffabe68e1d4e1f4821fac798095f10539371676cd690bd27927adefab1911ff69b33d62c languageName: node linkType: hard -"postcss-colormin@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-colormin@npm:7.0.2" +"postcss-colormin@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-colormin@npm:7.0.3" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" caniuse-api: "npm:^3.0.0" colord: "npm:^2.9.3" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/cb83d95d21668c770e5268f50ec6f8cd5d991d65123bafd3aa4a697580609c62d0078e704c4b7820db57638bf386084b253885b1e86263f580e8a393a687e973 + postcss: ^8.4.32 + checksum: 10/b9016d205eaf61a25efb187264a2ce35cb59aa1734b946268abcd747b5796e0d855c081b460ead4042a17c6806e011b57ee543b9e1f6312620f8daf661a7e40c languageName: node linkType: hard -"postcss-convert-values@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-convert-values@npm:7.0.4" +"postcss-convert-values@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-convert-values@npm:7.0.5" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/077481cc98514965acf335cdacae4f604be86f4153ed3bcfdd2c4c54058182d0b472f859931d55d9aeb01600f08fff6a88a21539adbb6169019fda8b22f064ef + postcss: ^8.4.32 + checksum: 10/67920f9ba823a6f6aa3b46c3a098c2d4a7a2a32349971cfa6ce986e08e7cbae6badeb23de680d36d1439e7d3f2cdbf26f5ee080a66f2823931c1d3f8146bc2a6 languageName: node linkType: hard -"postcss-discard-comments@npm:^7.0.3": - version: 7.0.3 - resolution: "postcss-discard-comments@npm:7.0.3" +"postcss-discard-comments@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-discard-comments@npm:7.0.4" dependencies: - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/f7c994df0d2de75d876f0db7ebd5b63718ef7ee5336a35f5f753f8ea115ecf8be26d5d2ad8800e833f18b33da6e018af82de7b5f0aa69e6338d3e0aff46348d4 + postcss: ^8.4.32 + checksum: 10/a09ac248bfbd6f2baa72b84873a876f4113df0fb5e9dd10808f6bbb310473fcd7905cc4639dbfd3ad8a5444053d42f7bb644a6934e95305820bdedc731d3c80a languageName: node linkType: hard -"postcss-discard-duplicates@npm:^7.0.1": +"postcss-discard-duplicates@npm:^7.0.2": + version: 7.0.2 + resolution: "postcss-discard-duplicates@npm:7.0.2" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/2da841b5c0117528e56e1ccda28924339c03fdb93dab61b767cebb9a9e4a2a077498d00e0c97c9ec36a534f98d6f358e6236f30913c184f90d51f6d302f4f0f6 + languageName: node + linkType: hard + +"postcss-discard-empty@npm:^7.0.1": version: 7.0.1 - resolution: "postcss-discard-duplicates@npm:7.0.1" + resolution: "postcss-discard-empty@npm:7.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/0c757bb542caf017740157a2e29186ae83085bb42cd8e5ea3649fa039cc3d505ccaca739b1aed6c89e1f0a7f18440f77c3f49e4b99f45efd767c863d6647af94 + postcss: ^8.4.32 + checksum: 10/39977000657e78202da891ae6300593e40e1c8a756f1d9707087390e47a410739c394c35e902130556efb5808e6701b3b34b89facf7a9e56533d617dd9597049 languageName: node linkType: hard -"postcss-discard-empty@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-discard-empty@npm:7.0.0" +"postcss-discard-overridden@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-discard-overridden@npm:7.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/0c5cea198057727765855dbb43b5f16bd4d7da8c783fea8d18ad445ad3457681a7bc1696fda6bf16313e6fadaf86d519470aff68f02378b8b413e60023b70d57 - languageName: node - linkType: hard - -"postcss-discard-overridden@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-discard-overridden@npm:7.0.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/e41c448305f96a93ec97a4a8ce2932a123283898041ff38ed2f7a35fcb76d937f448c2c8efb7d74d53d38b4ebf9163ae12935297bb99baec2f6751776b0ea29b + postcss: ^8.4.32 + checksum: 10/a0e67314b696591396e6bb371cdd57537e06f63e9fa0d742fe678decf600bed0cdcfa481487bce91b3732bdd7c46338f9102ccc8180c41032811e99962883715 languageName: node linkType: hard @@ -25469,78 +25374,78 @@ __metadata: languageName: node linkType: hard -"postcss-merge-longhand@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-merge-longhand@npm:7.0.4" +"postcss-merge-longhand@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-merge-longhand@npm:7.0.5" dependencies: postcss-value-parser: "npm:^4.2.0" - stylehacks: "npm:^7.0.4" + stylehacks: "npm:^7.0.5" peerDependencies: - postcss: ^8.4.31 - checksum: 10/b94b98a9b21bc8671aa0fba96491e8e2deea57c9bbfe9a74305400a36035b63764d8bfbdc6bda047887b665b92b91361a8bbc1cb9c14f80b3792feef19881005 + postcss: ^8.4.32 + checksum: 10/3378fc3a196082dfdb9acff94efbfa0de95ed86bf87f485285e775fd3c21218e5a243e363ad80b96237edb454776f7c1deea28c37afb8b96ddfaf5cfe8bd606b languageName: node linkType: hard -"postcss-merge-rules@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-merge-rules@npm:7.0.4" +"postcss-merge-rules@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-merge-rules@npm:7.0.5" dependencies: - browserslist: "npm:^4.23.3" + browserslist: "npm:^4.24.5" caniuse-api: "npm:^3.0.0" - cssnano-utils: "npm:^5.0.0" - postcss-selector-parser: "npm:^6.1.2" + cssnano-utils: "npm:^5.0.1" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/f67a4f6e814c5e7ce990a3c3d699e1c1dba7e79c5cb3a11795534d47b0fa257d27465e248546b45104d8278dfcbd07d9fbceb8046fcdfac86fe6340ca3c85f9a + postcss: ^8.4.32 + checksum: 10/fa490791ea5e907e4498701593252ce33df468a821e5f3acf5f126f73c8262189c13ca7a0c1645ae3d66a46a03cf930048e10d808182a3e9bec78af30a02893a languageName: node linkType: hard -"postcss-minify-font-values@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-minify-font-values@npm:7.0.0" +"postcss-minify-font-values@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-minify-font-values@npm:7.0.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/8578c1d1d4d65ca34db5ac0cccc7b73500040e52a3abb8abc7e5b6e47e5f72c88bfe5f3b19847556a2a68082245009d693a7c098b8bc58e7f9640abba4e80194 + postcss: ^8.4.32 + checksum: 10/6578a1fd293e202e738ce38d91d71c08ba970f4a998edff48022cb21ec23ef26bf7d284ddb41d6e51bf20b5b5676fe142de1bd092a76d2ef982d5ee1d6b00190 languageName: node linkType: hard -"postcss-minify-gradients@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-minify-gradients@npm:7.0.0" +"postcss-minify-gradients@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-minify-gradients@npm:7.0.1" dependencies: colord: "npm:^2.9.3" - cssnano-utils: "npm:^5.0.0" + cssnano-utils: "npm:^5.0.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/9649e255ad954e67e0d7c2111b0f1681a93e8cba7179a547491eacf135d64596dfee9774b589d7a46ee3ace673a026113e56e734d6ab19297367f11dd3104c0e + postcss: ^8.4.32 + checksum: 10/4aa782331c5d1826e549b3940eefb54e2d51f5c5a2c5f5537384bfe6eac45bfe7ba4535c03cd1642d8a27ab088f56c3682b55f5dd2c3f7969b715692e0c1102b languageName: node linkType: hard -"postcss-minify-params@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-minify-params@npm:7.0.2" +"postcss-minify-params@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-minify-params@npm:7.0.3" dependencies: - browserslist: "npm:^4.23.3" - cssnano-utils: "npm:^5.0.0" + browserslist: "npm:^4.24.5" + cssnano-utils: "npm:^5.0.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/26b6ce4db3cdefcceb7a00b64dfbd27dee4194b55708937dddd5c4000c1f02013dc0659e62e799dc1ce1f1a697961cec55a2a746a4f59d54ccae4b68adf41768 + postcss: ^8.4.32 + checksum: 10/97de22d6ba0310685d33b530dbfeefa930f7ac48effe623fc8a4a59d2b98bed221d0d2edad4f2e1f4590322240d0e1e94bdb162069c40b5d7ae00c58637c90c9 languageName: node linkType: hard -"postcss-minify-selectors@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-minify-selectors@npm:7.0.4" +"postcss-minify-selectors@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-minify-selectors@npm:7.0.5" dependencies: cssesc: "npm:^3.0.0" - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/54c74dcb098819417e95ec2b5ecdd33a2c6fdccea2346e110037c762d37644e11f83d67e6b0c93405f2b7cc28880ca0a07ad4d6618330436f7b8b84d719b85fb + postcss: ^8.4.32 + checksum: 10/12580d9a17c146c9e9bb604b4887085d897554317590cee91e0f28e2a4757c18e09299365a44eae25e848e65d53b845928dfa56a9d0199d0e159d525732fbf89 languageName: node linkType: hard @@ -25588,136 +25493,136 @@ __metadata: languageName: node linkType: hard -"postcss-normalize-charset@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-charset@npm:7.0.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/a41043fb81a1d5b3b05e8b317de7fe123854a4535f9ce2904a16196a32b3565d2fd6ac59a9842e337cf1bb298dcc108cbdbc6a5d4a500aec3520d759e951a8de - languageName: node - linkType: hard - -"postcss-normalize-display-values@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-display-values@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/55bbfb4dac3bf9bcc2aed30057c0bc968927b5337b372ee2dd825d6ec626c18d1481b0e8dd928d4cab70c3e8a2e6708d6115b14bebd34fe4462eb15aacff35f4 - languageName: node - linkType: hard - -"postcss-normalize-positions@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-positions@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/a6b982e567ddf1ad4120aaf898056f2fdbe5f6cae1d475fef22cb1f025c9bfe37df5511a4353b9f13d01feae8b1d9638c1deb70537058312262647052d004f64 - languageName: node - linkType: hard - -"postcss-normalize-repeat-style@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-repeat-style@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/f8ef8cf5ac6232f1d0615a97f21ea464a6930484b58421c87e0f9e626b1bb52916592f25e4f9874f424b1529807b170d8805d45878aa8293ea0608dd753230c8 - languageName: node - linkType: hard - -"postcss-normalize-string@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-string@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/23ea7dd7b28880dfafd0880ab782d65186ab94a4cf789b8723f9666020c7f7c8b97546e0dc46d08da3f71a873bb6db41cd69a4cafb4fde4a85f97ef83ee38bae - languageName: node - linkType: hard - -"postcss-normalize-timing-functions@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-timing-functions@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/f85870b3c8132b530fb8e5c8474f1eea1d0ef69a374d5867d0300f7501803bffa55f7fad34f662d88a747ce73d552ec0f818722d2d5157cf8e5dc45a98fa552b - languageName: node - linkType: hard - -"postcss-normalize-unicode@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-normalize-unicode@npm:7.0.2" - dependencies: - browserslist: "npm:^4.23.3" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/cb342f7507f28c8e9c500a2d6369c6b04a85f6c6f93aaa1ab6768d0e097453480834d3f7c5fad503f9fb9e178d9011df50ceaeebe2ac68d5daaa7c8a63ad3b3f - languageName: node - linkType: hard - -"postcss-normalize-url@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-url@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/c5edca0646a13d76c5347fffaaa828184e035486d7eeb2a8b31781d30de6a90f7ad3f0cffe59e8fd4c31f1525fdb85b45777745685603ac533a151c42691f601 - languageName: node - linkType: hard - -"postcss-normalize-whitespace@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-normalize-whitespace@npm:7.0.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10/c409362e3256ed66629fc48c63e834c9bfb598ca20587adb620bbc04fdccef4cd0d08b1f485eb8290d6a30e8dd836fecb0def38c3a49fe8503e2579e60f5bccf - languageName: node - linkType: hard - -"postcss-ordered-values@npm:^7.0.1": +"postcss-normalize-charset@npm:^7.0.1": version: 7.0.1 - resolution: "postcss-ordered-values@npm:7.0.1" - dependencies: - cssnano-utils: "npm:^5.0.0" - postcss-value-parser: "npm:^4.2.0" + resolution: "postcss-normalize-charset@npm:7.0.1" peerDependencies: - postcss: ^8.4.31 - checksum: 10/048082c09eee021d97def02eb8fc03fb0414402b1f6925af29a862f537b66b43d7a8e8d94c552ca67cd6172230873260f4ad44f1d5bac81c553afb054d80e6a8 + postcss: ^8.4.32 + checksum: 10/bcec822491e3421b009c688473433164b5c80bbef48af4e47f704bee68f0b7ba2009aaf46788e698dd233d5f4e1cf444a4f59a901623c73f8458c2227b15db57 languageName: node linkType: hard -"postcss-reduce-initial@npm:^7.0.2": - version: 7.0.2 - resolution: "postcss-reduce-initial@npm:7.0.2" +"postcss-normalize-display-values@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-display-values@npm:7.0.1" dependencies: - browserslist: "npm:^4.23.3" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/53f341c17a5487639e6f7c917ad695e059bf4aff66b3c971e008163f774337444753310def9f38dd26066ea96b136422592fc74077c38c40b3bfdfaa338d5b58 + languageName: node + linkType: hard + +"postcss-normalize-positions@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-positions@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/72b23ab87c97c155d2ec475fba8a8b968f7c7b42d055a79b267449d570c328d5ea4cb0002428cf26e9daa70c58655e0b931d2a5801cc407554d3f03a21ac041b + languageName: node + linkType: hard + +"postcss-normalize-repeat-style@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-repeat-style@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/db677bceec8c00a1860b64932b99af937e7674b3e5c5ac333c95efb090e9abd747eca4ad51855f0fe73fbe544c3d21e58d06b39e03fd525945309743e31ec235 + languageName: node + linkType: hard + +"postcss-normalize-string@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-string@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/48df2eaca6f5365af31ad46fd60a32dc7b714cc5ec8ba80980e65855ddc47c03ac82077ce7ca04c90898f73d173410d1d6a104754ff487e7e5a59e3eae8325b3 + languageName: node + linkType: hard + +"postcss-normalize-timing-functions@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-timing-functions@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/31fb88489244334295918fa7d6af2d76c310a83abd20be0a7f1c408c54ac0c0f81b0ae7877698bf66de1f76495766e159c8871387407dfcafa0cb1a53f5f0460 + languageName: node + linkType: hard + +"postcss-normalize-unicode@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-normalize-unicode@npm:7.0.3" + dependencies: + browserslist: "npm:^4.24.5" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/fc10205655f77d6467da811fbd26aa607c519cbf162ae2ba40821cf64227233445490881119c820c6988c0943cb2f4dc755abe94cb30637001ca35cce5d07b61 + languageName: node + linkType: hard + +"postcss-normalize-url@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-url@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/975dd0d1b55b637d45756ec57e554b2134f77368dd3ae09be9fa6636f2f41e72422505409d7fca75c635b9b1b8ec8ec2607d84c6c85497bbfd4e7748a2992882 + languageName: node + linkType: hard + +"postcss-normalize-whitespace@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-normalize-whitespace@npm:7.0.1" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/05a0fa74f4c8e93243053b9cc865cbddddb309b2ccb08271ca9c38ea7ece2ff43d5faa12cce87f06e40cbcf22c94443c9fa2b74ed0c6b94d72a9e67ea0381626 + languageName: node + linkType: hard + +"postcss-ordered-values@npm:^7.0.2": + version: 7.0.2 + resolution: "postcss-ordered-values@npm:7.0.2" + dependencies: + cssnano-utils: "npm:^5.0.1" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4.32 + checksum: 10/be8fb13639fb0e1ffd7d4e9bb4824d3a283c8a63a8b0dd1a654435fff1e019007c79be877940bb101bb9ebd8ba3ac18bcffd144e939890bedeb40044dcc2b9cc + languageName: node + linkType: hard + +"postcss-reduce-initial@npm:^7.0.3": + version: 7.0.3 + resolution: "postcss-reduce-initial@npm:7.0.3" + dependencies: + browserslist: "npm:^4.24.5" caniuse-api: "npm:^3.0.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/5a8260cbf7fa6ea12908debe23e191bb45109b29048d15e63c60df42c4ed62c860273ce9b37172d5f31c4bdb965e984962e4e6f506939a1fc49202dd7bf520c5 + postcss: ^8.4.32 + checksum: 10/8fd9ff4b49a2f7e1b7c51b7da637578e32a178363e3e932c80565241454dca306658dacd390ad3d73647d55dace8be8fe29278668afa32fd9d872ee7026bdbf7 languageName: node linkType: hard -"postcss-reduce-transforms@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-reduce-transforms@npm:7.0.0" +"postcss-reduce-transforms@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-reduce-transforms@npm:7.0.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/1c369a1be820a80e8bf06376476190fe2ae5a0b5a7459257d7d9b5bc0c9aed79f46026e8558fca088f7a814e632c678f67749b246901a3839f2d50b7b9ec2d41 + postcss: ^8.4.32 + checksum: 10/a22d07559859b9d4313d579104a25aa254695bc37dec5134de1064d1bd52b9d1f33f050fbf330170ef1105ede9aad7741bbcf9cad2221a6a5c8d529fd3cf0259 languageName: node linkType: hard @@ -25768,36 +25673,36 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-selector-parser@npm:7.0.0" +"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0": + version: 7.1.0 + resolution: "postcss-selector-parser@npm:7.1.0" dependencies: cssesc: "npm:^3.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10/0e92be7281e2b440a8be8cf207de40a24ca7bc765577916499614d5a47827a3e658206728cc559db96803e554270516104aad919a04f91bfa8914ccef1ba14ca + checksum: 10/2caf09e66e2be81d45538f8afdc5439298c89bea71e9943b364e69dce9443d9c5ab33f4dd8b237f1ed7d2f38530338dcc189c1219d888159e6afb5b0afe58b19 languageName: node linkType: hard -"postcss-svgo@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-svgo@npm:7.0.1" +"postcss-svgo@npm:^7.0.2": + version: 7.0.2 + resolution: "postcss-svgo@npm:7.0.2" dependencies: postcss-value-parser: "npm:^4.2.0" svgo: "npm:^3.3.2" peerDependencies: - postcss: ^8.4.31 - checksum: 10/4196d9b7ec37ea7c427b6d3d40fa75bdae6d1fdf5a814481202138fb9b074ecc1e442b8e0202aa8c76eaaff747e2f6bfec968cfe7bc774d8a58faf8bd945ff4e + postcss: ^8.4.32 + checksum: 10/8615877dffbac2bb2b971fb0e8c882ebff479c2529a0fc20937d09623fcaf35a2d934c4046188bae2534729aba1de5a1ba227630aaf96a800b6f2acdbfbf1d32 languageName: node linkType: hard -"postcss-unique-selectors@npm:^7.0.3": - version: 7.0.3 - resolution: "postcss-unique-selectors@npm:7.0.3" +"postcss-unique-selectors@npm:^7.0.4": + version: 7.0.4 + resolution: "postcss-unique-selectors@npm:7.0.4" dependencies: - postcss-selector-parser: "npm:^6.1.2" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/c38ca6b5f539cae1e0e8ef0efa338f91e4e054dbd9c619e26708d787e94ce788739bbe782103f2cf35c38819233897901038292255a1726905bd04433ac9e5f2 + postcss: ^8.4.32 + checksum: 10/b880f96fdb20037b16ae21b48f5240a4cf8585bf3133c7894dd869711b14f3a1a82bbdecd36adc78f8c34553a46fc2199ed3e92d5031b0267ff6f43894fc00f7 languageName: node linkType: hard @@ -25808,14 +25713,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.1": - version: 8.5.1 - resolution: "postcss@npm:8.5.1" +"postcss@npm:8.5.6, postcss@npm:^8.4.33, postcss@npm:^8.4.40, postcss@npm:^8.5.1": + version: 8.5.6 + resolution: "postcss@npm:8.5.6" dependencies: - nanoid: "npm:^3.3.8" + nanoid: "npm:^3.3.11" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/1fbd28753143f7f03e4604813639918182b15343c7ad0f4e72f3875fc2cc0b8494c887f55dc05008fad5fbf1e1e908ce2edbbce364a91f84dcefb71edf7cd31d + checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86 languageName: node linkType: hard @@ -26021,15 +25926,6 @@ __metadata: languageName: node linkType: hard -"propagating-hammerjs@npm:^1.4.6": - version: 1.5.0 - resolution: "propagating-hammerjs@npm:1.5.0" - dependencies: - hammerjs: "npm:^2.0.8" - checksum: 10/ff83ae333e69942af80af5ce53d240a63a8d63c96cbee1771bbe194ef70ee2977b31f909ea952c13a0d155ffc059e6f71253f04f40299842354b2b8ffc081d48 - languageName: node - linkType: hard - "property-information@npm:^5.0.0": version: 5.6.0 resolution: "property-information@npm:5.6.0" @@ -26259,10 +26155,10 @@ __metadata: languageName: node linkType: hard -"quickselect@npm:^2.0.0": - version: 2.0.0 - resolution: "quickselect@npm:2.0.0" - checksum: 10/ed2e78431050d223fb75da20ee98011aef1a03f7cb04e1a32ee893402e640be3cfb76d72e9dbe01edf3bb457ff6a62e5c2d85748424d1aa531f6ba50daef098c +"quickselect@npm:^3.0.0": + version: 3.0.0 + resolution: "quickselect@npm:3.0.0" + checksum: 10/8f72bedb8bb14bce5c3767c55f567bc296fa3ca9d98ba385e3867e434463bc633feee1eddf3dfec17914b7e88feeb08c7b313cf47114a8ff11bf964f77f51cfc languageName: node linkType: hard @@ -26358,12 +26254,12 @@ __metadata: languageName: node linkType: hard -"rbush@npm:^3.0.1": - version: 3.0.1 - resolution: "rbush@npm:3.0.1" +"rbush@npm:^4.0.0": + version: 4.0.1 + resolution: "rbush@npm:4.0.1" dependencies: - quickselect: "npm:^2.0.0" - checksum: 10/489e2e7d9889888ad533518f194e3ab7cc19b1f1365a38ee99fbdda542a47f41cda7dc89870180050f4d04ea402e9ff294e1d767d03c0f1694e0028b7609eec9 + quickselect: "npm:^3.0.0" + checksum: 10/8db2f9b464ee31cbb13237e762140c27f4de517a470d1639f840cc606b1f917f64f6273178b2f07c93d95c44cc068cc0c25c84ad40c4a483404e60d17c054eec languageName: node linkType: hard @@ -26594,13 +26490,13 @@ __metadata: languageName: node linkType: hard -"react-calendar@npm:^5.1.0": - version: 5.1.0 - resolution: "react-calendar@npm:5.1.0" +"react-calendar@npm:^6.0.0": + version: 6.0.0 + resolution: "react-calendar@npm:6.0.0" dependencies: - "@wojtekmaj/date-utils": "npm:^1.1.3" + "@wojtekmaj/date-utils": "npm:^2.0.2" clsx: "npm:^2.0.0" - get-user-locale: "npm:^2.2.1" + get-user-locale: "npm:^3.0.0" warning: "npm:^4.0.0" peerDependencies: "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -26609,7 +26505,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/69c5f809646edae64dc75d8709ccac979723fde2d5752f0992e429d09a1caf45e6c2d340563dfec2cf27e22a19b5f51f65c05decfcae9860c34470c08261de43 + checksum: 10/ba555e280a829b84a9675636c3166ef728cb40f200cdf31696fe4f45e09a39fd73594b764e5b2793c35414e96101c472332507e5bb42e5e182d43a85b060de40 languageName: node linkType: hard @@ -26749,16 +26645,16 @@ __metadata: languageName: node linkType: hard -"react-draggable@npm:4.4.6, react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5": - version: 4.4.6 - resolution: "react-draggable@npm:4.4.6" +"react-draggable@npm:4.5.0, react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5": + version: 4.5.0 + resolution: "react-draggable@npm:4.5.0" dependencies: - clsx: "npm:^1.1.1" + clsx: "npm:^2.1.1" prop-types: "npm:^15.8.1" peerDependencies: react: ">= 16.3.0" react-dom: ">= 16.3.0" - checksum: 10/51b9ac7f913797fc1cebc30ae383f346883033c45eb91e9b0b92e9ebd224bb1545b4ae2391825b649b798cc711a38351a5f41be24d949c64c6703ebc24eba661 + checksum: 10/fd4080a082fad199103c6978c8a78c80154a0205dbd14ca2c7bb75634132a6afd83a3e4669f2d8fe54efe68569ef24a992439183289d83e71030b0a1791d3476 languageName: node linkType: hard @@ -26847,11 +26743,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.58.1 - resolution: "react-hook-form@npm:7.58.1" + version: 7.60.0 + resolution: "react-hook-form@npm:7.60.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10/94b8af3976f21241adf3ca67d08929c8e66139c45f78632f5e563b83956000c5c30025dfacbe74602670d1f4c13dea5f3d2d8c03344698d193ae0214455196ad + checksum: 10/cc86ef4029f7b06f82dff919c8933156683b70669a6306d2471706443192b8916afaa07bc64452b590ecb18ea94f3d156f3fb38d59ada3462f5b108e89e186c9 languageName: node linkType: hard @@ -26865,8 +26761,8 @@ __metadata: linkType: hard "react-i18next@npm:^15.0.0": - version: 15.5.3 - resolution: "react-i18next@npm:15.5.3" + version: 15.6.0 + resolution: "react-i18next@npm:15.6.0" dependencies: "@babel/runtime": "npm:^7.27.6" html-parse-stringify: "npm:^3.0.1" @@ -26881,7 +26777,7 @@ __metadata: optional: true typescript: optional: true - checksum: 10/7e93c2fe408c0a56ce57be4ff87d8cbf58ea181380d4b366f73142e45ddc10b3a435feb76447ad3c5e5320e7fc10d61596113405e89b97bc23e140613f5b19a4 + checksum: 10/d8549c364eb32ea078be0e87df35ee23b8c9d6a30af4fe2a41099c12f62b5ce4268f9997132a9b5abcb4556e30fb37f4ba7f5548429333548b5b9856627c6a50 languageName: node linkType: hard @@ -27190,9 +27086,9 @@ __metadata: languageName: node linkType: hard -"react-select@npm:5.10.1": - version: 5.10.1 - resolution: "react-select@npm:5.10.1" +"react-select@npm:5.10.2": + version: 5.10.2 + resolution: "react-select@npm:5.10.2" dependencies: "@babel/runtime": "npm:^7.12.0" "@emotion/cache": "npm:^11.4.0" @@ -27206,7 +27102,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/c0c4d798e9b1f654b56c26bffb149ab811d24ca0950c1e8c3675438065628561eb132c6a5982cea893a288a0e9af6f9d9db290f4e53da71570dada0c1147f146 + checksum: 10/9c7f488a9774f566f60e882343dc26276eed866669304805638378b3bafb87fb1f6189d1aacde5eba4f637a7ff022f8e24caec541d12c254bafc2eb9658e14d2 languageName: node linkType: hard @@ -27379,6 +27275,16 @@ __metadata: languageName: node linkType: hard +"react-virtualized-auto-sizer@npm:*, react-virtualized-auto-sizer@npm:1.0.26": + version: 1.0.26 + resolution: "react-virtualized-auto-sizer@npm:1.0.26" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/8f8127369fb3ae1a49987a1e7d2d136f379b71d1b031a4c169389274946eca0248e3fb9130aa8a7ebe82f4ca8aaab58c37252398f8f6220a6eafaa78b8464b6b + languageName: node + linkType: hard + "react-virtualized-auto-sizer@npm:1.0.24": version: 1.0.24 resolution: "react-virtualized-auto-sizer@npm:1.0.24" @@ -27389,7 +27295,7 @@ __metadata: languageName: node linkType: hard -"react-virtualized-auto-sizer@npm:1.0.25, react-virtualized-auto-sizer@npm:^1.0.6": +"react-virtualized-auto-sizer@npm:^1.0.6": version: 1.0.25 resolution: "react-virtualized-auto-sizer@npm:1.0.25" peerDependencies: @@ -28361,7 +28267,7 @@ __metadata: languageName: node linkType: hard -"rw@npm:1, rw@npm:^1.3.3": +"rw@npm:1": version: 1.3.3 resolution: "rw@npm:1.3.3" checksum: 10/e90985d64777a00f4ab5f8c0bfea2fb5645c6bda5238840afa339c8a4f86f776e8ce83731155643a7425a0b27ce89077dab27b2f57519996ba4d2fe54cac1941 @@ -28882,13 +28788,6 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.2.1": - version: 1.2.1 - resolution: "shimmer@npm:1.2.1" - checksum: 10/aa0d6252ad1c682a4fdfda69e541be987f7a265ac7b00b1208e5e48cc68dc55f293955346ea4c71a169b7324b82c70f8400b3d3d2d60b2a7519f0a3522423250 - languageName: node - linkType: hard - "short-unique-id@npm:^5.3.2": version: 5.3.2 resolution: "short-unique-id@npm:5.3.2" @@ -29369,20 +29268,6 @@ __metadata: languageName: node linkType: hard -"sort-asc@npm:^0.1.0": - version: 0.1.0 - resolution: "sort-asc@npm:0.1.0" - checksum: 10/c130223336a9431edd4c12cf0a4893eb9327591f8c9d9ccd1b27a42f7657b4bb84807d7150d79fc52ab7d1e9691684897de7ef2a6a3516d32349af68d2705d57 - languageName: node - linkType: hard - -"sort-desc@npm:^0.1.1": - version: 0.1.1 - resolution: "sort-desc@npm:0.1.1" - checksum: 10/f2a2568f4eaf9a7315700ae9e46e74ae7b395f24323bf807f07b20269a7c162b549b0bac6d5bb8e26f7ee15af72a74fdaf09d47257227dbe31c4ac8f13b19e1d - languageName: node - linkType: hard - "sort-keys@npm:^2.0.0": version: 2.0.0 resolution: "sort-keys@npm:2.0.0" @@ -29401,16 +29286,6 @@ __metadata: languageName: node linkType: hard -"sort-object@npm:^0.3.2": - version: 0.3.2 - resolution: "sort-object@npm:0.3.2" - dependencies: - sort-asc: "npm:^0.1.0" - sort-desc: "npm:^0.1.1" - checksum: 10/701fac8b49e9c720f4e5f4b70e995a795771dbecab2368b2e362c5c02cc269418a061f3303c47137d22fc3fe1ded03b4fd4215edec0960c4721274d320dae2b4 - languageName: node - linkType: hard - "source-list-map@npm:^2.0.0, source-list-map@npm:^2.0.1": version: 2.0.1 resolution: "source-list-map@npm:2.0.1" @@ -30072,19 +29947,6 @@ __metadata: languageName: node linkType: hard -"strong-log-transformer@npm:2.1.0": - version: 2.1.0 - resolution: "strong-log-transformer@npm:2.1.0" - dependencies: - duplexer: "npm:^0.1.1" - minimist: "npm:^1.2.0" - through: "npm:^2.3.4" - bin: - sl-log-transformer: bin/sl-log-transformer.js - checksum: 10/2fd14eb0a68893fdadefd89f964df404e3d637729c48aca015eb12d1c47455dee28b2522ad7150de23f7a57cce503656585e7644c9cd8532023ea572f8cc5a80 - languageName: node - linkType: hard - "strtok3@npm:^6.2.4": version: 6.3.0 resolution: "strtok3@npm:6.3.0" @@ -30120,15 +29982,15 @@ __metadata: languageName: node linkType: hard -"stylehacks@npm:^7.0.4": - version: 7.0.4 - resolution: "stylehacks@npm:7.0.4" +"stylehacks@npm:^7.0.5": + version: 7.0.5 + resolution: "stylehacks@npm:7.0.5" dependencies: - browserslist: "npm:^4.23.3" - postcss-selector-parser: "npm:^6.1.2" + browserslist: "npm:^4.24.5" + postcss-selector-parser: "npm:^7.1.0" peerDependencies: - postcss: ^8.4.31 - checksum: 10/fc9d6b1e0b996d139a77f391df6db49ee1ab7e8fdeb32a8fa6b4c11512e72eb072470c32080171e46ebe123c9c96d763b9e4421b09c9c428985077940b6ba085 + postcss: ^8.4.32 + checksum: 10/798ac0f92ff4489c251550d64b903f1aa8b5946e5b09b33ebf68290b5a345257cecf98c989526a5d462b560081194fead38c4f804ec016ceb8b1b3f17ec74fc5 languageName: node linkType: hard @@ -30317,9 +30179,9 @@ __metadata: languageName: node linkType: hard -"swagger-ui-react@npm:5.25.2": - version: 5.25.2 - resolution: "swagger-ui-react@npm:5.25.2" +"swagger-ui-react@npm:5.26.2": + version: 5.26.2 + resolution: "swagger-ui-react@npm:5.26.2" dependencies: "@babel/runtime-corejs3": "npm:^7.27.1" "@scarf/scarf": "npm:=1.4.0" @@ -30357,7 +30219,7 @@ __metadata: peerDependencies: react: ">=16.8.0 <19" react-dom: ">=16.8.0 <19" - checksum: 10/0b145421d757611e28219aedd763a7a76d8e40cec5147f361aa3d90273a0874d52907e3ec7514d6431954f2c179b29a3199ef2e0fdf86b41e1a5eb5205b5eeee + checksum: 10/4ca99974df01fda16adcfd16458ee8703f1f1df5112e80dd2373bcaca0404f188ce1b96ae8ac4838311f2ec5b6073220966ba4089cc67ea378a84990976c6d40 languageName: node linkType: hard @@ -30628,7 +30490,7 @@ __metadata: languageName: node linkType: hard -"through@npm:2, through@npm:2.3.x, through@npm:>=2.2.7 <3, through@npm:^2.3.4, through@npm:^2.3.6, through@npm:^2.3.8": +"through@npm:2, through@npm:2.3.8, through@npm:2.3.x, through@npm:>=2.2.7 <3, through@npm:^2.3.6, through@npm:^2.3.8": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 @@ -30642,13 +30504,6 @@ __metadata: languageName: node linkType: hard -"timsort@npm:^0.3.0": - version: 0.3.0 - resolution: "timsort@npm:0.3.0" - checksum: 10/f4b8e0afa770440660b98034d7170333033b96fb6cb32d2fdfab65f78ba7741b9e271e2351631daacfa78a471d33f8ea1f5a29f94e960621f25045bfada46f3f - languageName: node - linkType: hard - "tiny-invariant@npm:^1.0.1, tiny-invariant@npm:^1.0.2, tiny-invariant@npm:^1.0.6, tiny-invariant@npm:^1.2.0, tiny-invariant@npm:^1.3.1, tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -30691,6 +30546,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:0.2.12": + version: 0.2.12 + resolution: "tinyglobby@npm:0.2.12" + dependencies: + fdir: "npm:^6.4.3" + picomatch: "npm:^4.0.2" + checksum: 10/4ad28701fa9118b32ef0e27f409e0a6c5741e8b02286d50425c1f6f71e6d6c6ded9dd5bbbbb714784b08623c4ec4d150151f1d3d996cfabe0495f908ab4f7002 + languageName: node + linkType: hard + "tinyrainbow@npm:^1.2.0": version: 1.2.0 resolution: "tinyrainbow@npm:1.2.0" @@ -30959,7 +30824,7 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.0.0, ts-api-utils@npm:^2.1.0": +"ts-api-utils@npm:^2.1.0": version: 2.1.0 resolution: "ts-api-utils@npm:2.1.0" peerDependencies: @@ -31389,13 +31254,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:>=2.7, typescript@npm:>=3 < 6, typescript@npm:^5.0.4, typescript@npm:^5.4.5, typescript@npm:^5.5.4": - version: 5.8.2 - resolution: "typescript@npm:5.8.2" +"typescript@npm:5.8.3, typescript@npm:>=2.7, typescript@npm:>=3 < 6, typescript@npm:^5.0.4, typescript@npm:^5.4.5, typescript@npm:^5.5.4": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/dbc2168a55d56771f4d581997be52bab5cbc09734fec976cfbaabd787e61fb4c6cf9125fd48c6f98054ce549c77ecedefc7f64252a830dd8e9c3381f61fbeb78 + checksum: 10/65c40944c51b513b0172c6710ee62e951b70af6f75d5a5da745cb7fab132c09ae27ffdf7838996e3ed603bb015dadd099006658046941bd0ba30340cc563ae92 languageName: node linkType: hard @@ -31429,13 +31294,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A>=2.7#optional!builtin, typescript@patch:typescript@npm%3A>=3 < 6#optional!builtin, typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": - version: 5.8.2 - resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5" +"typescript@patch:typescript@npm%3A5.8.3#optional!builtin, typescript@patch:typescript@npm%3A>=2.7#optional!builtin, typescript@patch:typescript@npm%3A>=3 < 6#optional!builtin, typescript@patch:typescript@npm%3A^5.0.4#optional!builtin, typescript@patch:typescript@npm%3A^5.4.5#optional!builtin, typescript@patch:typescript@npm%3A^5.5.4#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/97920a082ffc57583b1cb6bc4faa502acc156358e03f54c7fc7fdf0b61c439a717f4c9070c449ee9ee683d4cfc3bb203127c2b9794b2950f66d9d307a4ff262c + checksum: 10/b9b1e73dabac5dc730c041325dbd9c99467c1b0d239f1b74ec3b90d831384af3e2ba973946232df670519147eb51a2c20f6f96163cea2b359f03de1e2091cc4f languageName: node linkType: hard @@ -32007,17 +31872,27 @@ __metadata: languageName: node linkType: hard -"visjs-network@npm:4.25.0": - version: 4.25.0 - resolution: "visjs-network@npm:4.25.0" - dependencies: - emitter-component: "npm:^1.1.1" - hammerjs: "npm:^2.0.8" - keycharm: "npm:^0.2.0" - moment: "npm:^2.20.1" - propagating-hammerjs: "npm:^1.4.6" - timsort: "npm:^0.3.0" - checksum: 10/9914c6df0e56c22bebf43ed4a3799ecc84bc2430de776cb0b3b3b51e02454b3fe7093bb8017b6ac43717aa1458a839a4ffcf30feae2fae31b4d88358f3e6d34a +"vis-data@npm:^7.1.10": + version: 7.1.10 + resolution: "vis-data@npm:7.1.10" + peerDependencies: + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + vis-util: ^5.0.1 + checksum: 10/23fb2ef26864153013372e1d95107765be86dd9ce96f987bf99fdd93759fbe5ec1bd2603d354ca18a03f0fb607b829396ec02fe005aead63ef24599512f21402 + languageName: node + linkType: hard + +"vis-network@npm:9.1.13": + version: 9.1.13 + resolution: "vis-network@npm:9.1.13" + peerDependencies: + "@egjs/hammerjs": ^2.0.0 + component-emitter: ^1.3.0 || ^2.0.0 + keycharm: ^0.2.0 || ^0.3.0 || ^0.4.0 + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + vis-data: ^6.3.0 || ^7.0.0 + vis-util: ^5.0.1 + checksum: 10/7fd7264ee0a79282596efece988a78c037c418fa5e79aba632521a3093e9742a438b254dc698f488eb7707e3f7033352c25e7651a8d49b42662af0b368a12db7 languageName: node linkType: hard @@ -33071,3 +32946,10 @@ __metadata: checksum: 10/0e35432dcca7f053e63f5dd491a87c78abe0d981817547252c3b6d05f0f58788695d1a69724759c6501dff3fd62929be24c9f314a3625179bee889150f7a61fa languageName: node linkType: hard + +"zstddec@npm:^0.1.0": + version: 0.1.0 + resolution: "zstddec@npm:0.1.0" + checksum: 10/818d7a7b910509bed32a5ae42352dc45e87e4ee8470c27352859824540efceb9643864afbbb93564beb76822d72a946a6ee7cfbf01627ee34a1cd9656f2857b8 + languageName: node + linkType: hard