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 ae5e0e479e4..e29d4c2f65b 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
@@ -7,6 +7,8 @@ import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../mockApi';
import { grantUserPermissions } from '../mocks';
import { alertingFactory } from '../mocks/server/db';
+import { RulesFilter } from '../search/rulesSearchParser';
+import { testWithFeatureToggles } from '../test/test-utils';
import RuleList, { RuleListActions } from './RuleList.v2';
@@ -23,12 +25,18 @@ jest.mock('./GroupedView', () => ({
const ui = {
filterView: byTestId('filter-view'),
groupedView: byTestId('grouped-view'),
+ modeSelector: {
+ grouped: byRole('radio', { name: /grouped/i }),
+ list: byRole('radio', { name: /list/i }),
+ },
+ searchInput: byTestId('search-query-input'),
};
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]);
+testWithFeatureToggles(['alertingListViewV2']);
setupMswServer();
@@ -61,8 +69,84 @@ describe('RuleList v2', () => {
expect(ui.groupedView.query()).not.toBeInTheDocument();
});
- it('should show list view when a filter is applied', () => {
- render(
, { historyOptions: { initialEntries: ['/?search=rule:cpu-alert'] } });
+ it('should show grouped view when only group filter is applied', () => {
+ render(
, { historyOptions: { initialEntries: ['/?search=group:cpu-usage'] } });
+
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show grouped view when only namespace filter is applied', () => {
+ render(
, { historyOptions: { initialEntries: ['/?search=namespace:global'] } });
+
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show grouped view when both group and namespace filters are applied', () => {
+ render(
, { historyOptions: { initialEntries: ['/?search=group:cpu-usage namespace:global'] } });
+
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show list view when group and namespace filters are combined with other filter types', () => {
+ render(
, {
+ historyOptions: { initialEntries: ['/?search=group:cpu-usage namespace:global state:firing'] },
+ });
+
+ expect(ui.filterView.get()).toBeInTheDocument();
+ expect(ui.groupedView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show grouped view when view parameter is empty', () => {
+ render(
, { historyOptions: { initialEntries: ['/?view='] } });
+
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show grouped view when search parameter is empty', () => {
+ render(
, { historyOptions: { initialEntries: ['/?search='] } });
+
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+ });
+
+ it.each<{ filterType: keyof RulesFilter; searchQuery: string }>([
+ { filterType: 'freeFormWords', searchQuery: 'cpu alert' },
+ { filterType: 'ruleName', searchQuery: 'rule:"cpu 80%"' },
+ { filterType: 'ruleState', searchQuery: 'state:firing' },
+ { filterType: 'ruleType', searchQuery: 'type:alerting' },
+ { filterType: 'dataSourceNames', searchQuery: 'datasource:prometheus' },
+ { filterType: 'labels', searchQuery: 'label:team=backend' },
+ { filterType: 'ruleHealth', searchQuery: 'health:error' },
+ { filterType: 'contactPoint', searchQuery: 'contactPoint:slack' },
+ ])('should show list view when %s filter is applied', ({ filterType, searchQuery }) => {
+ render(
, { historyOptions: { initialEntries: [`/?search=${encodeURIComponent(searchQuery)}`] } });
+
+ expect(ui.filterView.get()).toBeInTheDocument();
+ expect(ui.groupedView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show list view when "view=list" URL parameter is present with group filter', () => {
+ render(
, { historyOptions: { initialEntries: ['/?view=list&search=group:cpu-usage'] } });
+
+ expect(ui.filterView.get()).toBeInTheDocument();
+ expect(ui.groupedView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show list view when "view=list" URL parameter is present with namespace filter', () => {
+ render(
, { historyOptions: { initialEntries: ['/?view=list&search=namespace:global'] } });
+
+ expect(ui.filterView.get()).toBeInTheDocument();
+ expect(ui.groupedView.query()).not.toBeInTheDocument();
+ });
+
+ it('should show list view when "view=list" URL parameter is present with both group and namespace filters', () => {
+ render(
, {
+ historyOptions: { initialEntries: ['/?view=list&search=group:cpu-usage namespace:global'] },
+ });
expect(ui.filterView.get()).toBeInTheDocument();
expect(ui.groupedView.query()).not.toBeInTheDocument();
@@ -160,3 +244,47 @@ describe('RuleListActions', () => {
expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).toBeInTheDocument();
});
});
+
+describe('RuleList v2 - View switching', () => {
+ it('should preserve both group and namespace filters when switching from list view to grouped view', async () => {
+ // Start with list view and both group and namespace filters
+ const { user } = render(
, {
+ historyOptions: { initialEntries: ['/?view=list&search=group:cpu-usage namespace:global'] },
+ });
+ expect(ui.filterView.get()).toBeInTheDocument();
+
+ // Click the "Grouped" view button
+ const groupedButton = await ui.modeSelector.grouped.find();
+ await user.click(groupedButton);
+
+ // Should preserve both filters and switch to grouped view
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+
+ // Verify filters are preserved
+ expect(ui.searchInput.get()).toHaveValue('group:cpu-usage namespace:global');
+ expect(ui.modeSelector.list.query()).not.toBeChecked();
+ });
+
+ it('should clear all filters when switching from list view to grouped view with group, namespace and other filters', async () => {
+ // Start with list view with all types of filters
+ const { user } = render(
, {
+ historyOptions: {
+ initialEntries: ['/?view=list&search=group:cpu-usage namespace:global state:firing rule:"test"'],
+ },
+ });
+ expect(ui.filterView.get()).toBeInTheDocument();
+
+ // Click the "Grouped" view button
+ const groupedButton = await ui.modeSelector.grouped.find();
+ await user.click(groupedButton);
+
+ // Should clear all filters because other filters are present
+ expect(ui.groupedView.get()).toBeInTheDocument();
+ expect(ui.filterView.query()).not.toBeInTheDocument();
+
+ // Verify all filters are cleared
+ expect(ui.searchInput.get()).toHaveValue('');
+ expect(ui.modeSelector.list.query()).not.toBeChecked();
+ });
+});
diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx
index 124e230aeef..f3c79b42b4b 100644
--- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx
+++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx
@@ -6,10 +6,9 @@ import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui';
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
import RulesFilter from '../components/rules/Filter/RulesFilter';
-import { SupportedView } from '../components/rules/Filter/RulesViewModeSelector';
+import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelector';
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
import { useRulesFilter } from '../hooks/useFilteredRules';
-import { useURLSearchParams } from '../hooks/useURLSearchParams';
import { isAdmin } from '../utils/misc';
import { FilterView } from './FilterView';
@@ -17,16 +16,17 @@ import { GroupedView } from './GroupedView';
import { RuleListPageTitle } from './RuleListPageTitle';
function RuleList() {
- const [queryParams] = useURLSearchParams();
- const { filterState, hasActiveFilters } = useRulesFilter();
-
- const view: SupportedView = queryParams.get('view') === 'list' ? 'list' : 'grouped';
- const showListView = hasActiveFilters || view === 'list';
+ const { filterState } = useRulesFilter();
+ const { viewMode, handleViewChange } = useListViewMode();
return (
<>
-
{}} />
- {showListView ? : }
+
+ {viewMode === 'list' ? (
+
+ ) : (
+
+ )}
>
);
}
diff --git a/public/app/features/alerting/unified/rule-list/components/NoRulesFound.tsx b/public/app/features/alerting/unified/rule-list/components/NoRulesFound.tsx
new file mode 100644
index 00000000000..f8778712dd9
--- /dev/null
+++ b/public/app/features/alerting/unified/rule-list/components/NoRulesFound.tsx
@@ -0,0 +1,24 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Trans } from '@grafana/i18n';
+import { Text, useStyles2 } from '@grafana/ui';
+
+// @TODO I don't like applying the margins to this component here, ideally the parent component should be layouting this.
+export const NoRulesFound = () => {
+ const styles = useStyles2(getStyles);
+
+ return (
+
+
+ No rules found
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ noRules: css({
+ margin: theme.spacing(1.5, 0, 0.5, 4),
+ }),
+});
diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.ts
index e66ddb559a9..975216e498d 100644
--- a/public/app/features/alerting/unified/rule-list/hooks/filters.ts
+++ b/public/app/features/alerting/unified/rule-list/hooks/filters.ts
@@ -14,16 +14,20 @@ import { isPluginProvidedRule, prometheusRuleType } from '../../utils/rules';
/**
* @returns True if the group matches the filter, false otherwise. Keeps rules intact
*/
-export function groupFilter(group: PromRuleGroupDTO, filterState: RulesFilter): boolean {
+export function groupFilter(
+ group: PromRuleGroupDTO,
+ filterState: Pick
+): boolean {
const { name, file } = group;
+ const { namespace, groupName } = filterState;
// Add fuzzy search for namespace
- if (filterState.namespace && !file.toLowerCase().includes(filterState.namespace)) {
+ if (namespace && !file.toLocaleLowerCase().includes(namespace.toLocaleLowerCase())) {
return false;
}
// Add fuzzy search for group name
- if (filterState.groupName && !name.toLowerCase().includes(filterState.groupName)) {
+ if (groupName && !name.toLocaleLowerCase().includes(groupName.toLocaleLowerCase())) {
return false;
}
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 5165996b05c..649594f893e 100644
--- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts
+++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts
@@ -10,6 +10,10 @@ import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi';
const { useLazyGetGroupsQuery, useLazyGetGrafanaGroupsQuery } = prometheusApi;
interface UseGeneratorHookOptions {
+ /**
+ * Whether to populate the RTKQ cache with the groups.
+ * Populating cache might harm performance when fetching a lot of groups or fetching multiple pages
+ */
populateCache?: boolean;
limitAlerts?: number;
}
diff --git a/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx b/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx
index df24ed95571..5181e1a1cd9 100644
--- a/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx
+++ b/public/app/features/alerting/unified/rule-list/hooks/useLazyLoadPrometheusGroups.tsx
@@ -16,7 +16,8 @@ import { isLoading as isLoadingState, useAsync } from '../../hooks/useAsync';
*/
export function useLazyLoadPrometheusGroups(
groupsGenerator: AsyncIterator,
- pageSize: number
+ pageSize: number,
+ filter?: (group: TGroup) => boolean
) {
const [groups, setGroups] = useState([]);
const [hasMoreGroups, setHasMoreGroups] = useState(true);
@@ -31,7 +32,12 @@ export function useLazyLoadPrometheusGroups(
done = true;
break;
}
+
const group = generatorResult.value;
+ if (filter && !filter(group)) {
+ continue;
+ }
+
currentGroups.push(group);
}
diff --git a/public/app/features/alerting/unified/rule-list/paginationLimits.ts b/public/app/features/alerting/unified/rule-list/paginationLimits.ts
new file mode 100644
index 00000000000..41c5293cbde
--- /dev/null
+++ b/public/app/features/alerting/unified/rule-list/paginationLimits.ts
@@ -0,0 +1,9 @@
+export const FRONTEND_LIST_PAGE_SIZE = 100;
+
+export const FILTERED_GROUPS_API_PAGE_SIZE = 2000;
+export const DEFAULT_GROUPS_API_PAGE_SIZE = 40;
+export const FRONTED_GROUPED_PAGE_SIZE = DEFAULT_GROUPS_API_PAGE_SIZE;
+
+export function getApiGroupPageSize(hasFilters: boolean) {
+ return hasFilters ? FILTERED_GROUPS_API_PAGE_SIZE : DEFAULT_GROUPS_API_PAGE_SIZE;
+}
From f71a2062eb6d0f355792bc8b406583ed54371ccb Mon Sep 17 00:00:00 2001
From: Victor Ubahakwe
Date: Wed, 11 Jun 2025 09:29:37 +0100
Subject: [PATCH 07/32] VQB: Allow custom table names in TableSelector
(#106420)
* feat(sql): allow custom table names in TableSelector
Restores the ability to enter custom table names not present in the database
by adding `allowCustomValue` to the Select component. This matches previous
functionality where users could manually specify table names not returned
by db.tables().
fixes: #106348
* empty line 45
---
packages/grafana-sql/src/components/TableSelector.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/grafana-sql/src/components/TableSelector.tsx b/packages/grafana-sql/src/components/TableSelector.tsx
index 9801f98d12c..fb1bac8e71f 100644
--- a/packages/grafana-sql/src/components/TableSelector.tsx
+++ b/packages/grafana-sql/src/components/TableSelector.tsx
@@ -38,6 +38,7 @@ export const TableSelector = ({ db, dataset, table, className, onChange, inputId
isLoading={state.loading}
menuShouldPortal={true}
placeholder={state.loading ? 'Loading tables' : 'Select table'}
+ allowCustomValue={true}
/>
);
};
From 8d0f911cfe2654be70f316d614d5ce110488a95f Mon Sep 17 00:00:00 2001
From: Mariell Hoversholm
Date: Wed, 11 Jun 2025 10:31:07 +0200
Subject: [PATCH 08/32] Actions: Propagate exit code in unit tests (#106528)
---
.github/workflows/backend-unit-tests.yml | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/.github/workflows/backend-unit-tests.yml b/.github/workflows/backend-unit-tests.yml
index e53be4d1d00..713155d3a45 100644
--- a/.github/workflows/backend-unit-tests.yml
+++ b/.github/workflows/backend-unit-tests.yml
@@ -74,6 +74,7 @@ jobs:
contents: read
id-token: write
steps:
+ # Set up repository clone
- name: Checkout code
uses: actions/checkout@v4
with:
@@ -86,18 +87,28 @@ jobs:
uses: ./.github/actions/setup-enterprise
with:
github-app-name: 'grafana-ci-bot'
+
+ # Prepare what we need to upload test results
- run: echo "RESULTS_FILE=$(date --rfc-3339=seconds --utc | sed -s 's/ /-/g')_${SHARD/\//_}.xml" >> "$GITHUB_ENV"
env:
SHARD: ${{ matrix.shard }}
- run: go install github.com/jstemmer/go-junit-report/v2@85bf4716ac1f025f2925510a9f5e9f5bb347c009
+
+ # Run code
- name: Generate Go code
run: make gen-go
- name: Run unit tests
env:
SHARD: ${{ matrix.shard }}
run: |
+ set -euo pipefail
+
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/shard.sh -N"$SHARD")"
+ # This tee requires pipefail to be set, otherwise `go test`'s exit code is thrown away.
+ # That means having no `-o pipefail` => failing tests => exit code 0, which is wrong.
go test -short -v -timeout=30m "${PACKAGES[@]}" | tee >(go-junit-report -set-exit-code > "$RESULTS_FILE")
+
+ # Upload results to GCS
- name: Log in to GCS
if: github.repository == 'grafana/grafana' && (success() || failure())
uses: grafana/shared-workflows/actions/login-to-gcs@login-to-gcs-v0.2.0
From 279bdc26365afe68720bb946e538a20531bb763b Mon Sep 17 00:00:00 2001
From: Jack Westbrook
Date: Wed, 11 Jun 2025 11:05:42 +0200
Subject: [PATCH 09/32] CI: Use publint to validate npm packages (#106521)
* ci(packages): use publint to validate npm packages are good to publish
* style(validate-npm-packages): remove extra line
---
package.json | 2 +
scripts/validate-npm-packages.sh | 66 +------
yarn.lock | 314 ++++++++++++++++++++++++++++++-
3 files changed, 312 insertions(+), 70 deletions(-)
diff --git a/package.json b/package.json
index 78fc8fe2b01..e5152879e2f 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/"
},
"devDependencies": {
+ "@arethetypeswrong/cli": "^0.18.2",
"@babel/core": "7.26.10",
"@babel/preset-env": "7.26.9",
"@babel/runtime": "7.27.0",
@@ -229,6 +230,7 @@
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",
"prettier": "3.4.2",
+ "publint": "^0.3.12",
"react-refresh": "0.14.0",
"react-select-event": "5.5.1",
"redux-mock-store": "1.5.5",
diff --git a/scripts/validate-npm-packages.sh b/scripts/validate-npm-packages.sh
index 2f0f1764597..47b82a49717 100755
--- a/scripts/validate-npm-packages.sh
+++ b/scripts/validate-npm-packages.sh
@@ -9,72 +9,10 @@ for file in "$ARTIFACTS_DIR"/*.tgz; do
echo "🔍 Checking NPM package: $file"
# Ignore named-exports for now as builds aren't compatible yet.
- yarn dlx @arethetypeswrong/cli "$file" --ignore-rules "named-exports"
-
- # get filename then strip everything after package name.
- dir_name=$(basename "$file" .tgz | sed -E 's/@([a-zA-Z0-9-]+)-[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9-]+)?/\1/')
- mkdir -p "./npm-artifacts/$dir_name"
- tar -xzf "$file" -C "./npm-artifacts/$dir_name" --strip-components=1
-
- # Make sure the tar wasn't empty
- if [ ! -d "./npm-artifacts/$dir_name" ]; then
- echo -e "❌ Failed: Empty package $dir_name.\n"
- exit 1
- fi
-
- # Navigate inside the new extracted directory
- pushd "./npm-artifacts/$dir_name" || exit
-
- # Check for required files
- check_files=("package.json" "README.md" "CHANGELOG.md")
- for check_file in "${check_files[@]}"; do
- if [ ! -f "$check_file" ]; then
- echo -e "❌ Failed: Missing required file $check_file in package $dir_name.\n"
- exit 1
- fi
- done
-
- # Check license files
- if [ -f "LICENSE_APACHE2" ] || [ -f "LICENSE_AGPL" ]; then
- echo -e "Found required license file in package $dir_name.\n"
- else
- echo -e "❌ Failed: Missing required license file in package $dir_name.\n"
- exit 1
- fi
-
- # Assert commonjs builds
- if [ ! -d dist ] || [ ! -f dist/cjs/index.cjs ] || [ ! -f dist/cjs/index.d.cts ]; then
- echo -e "❌ Failed: Missing 'dist' directory or required commonjs files in package $dir_name.\n"
- exit 1
- fi
-
- if [ "$(jq -r '.main' package.json)" != "./dist/cjs/index.cjs" ] || \
- [ "$(jq -r '.types' package.json)" != "./dist/cjs/index.d.cts" ]; then
- echo -e "❌ Failed: Incorrect cjs package.json properties in package $dir_name.\n"
- exit 1
- fi
-
- # Assert esm builds
- esm_packages=("grafana-data" "grafana-ui" "grafana-runtime" "grafana-e2e-selectors" "grafana-schema")
- for esm_package in "${esm_packages[@]}"; do
- if [[ "$dir_name" == "$esm_package" ]]; then
- if [ ! -d dist/esm ] || [ ! -f dist/esm/index.mjs ]; then
- echo -e "❌ Failed: Missing 'dist/esm' directory or required esm files in package $dir_name.\n"
- exit 1
- fi
-
- if [ "$(jq -r '.module' package.json)" != "./dist/esm/index.mjs" ]; then
- echo -e "❌ Failed: Incorrect esm package.json properties in package $dir_name.\n"
- exit 1
- fi
- fi
- done
-
- echo -e "✅ Passed: package checks for $file.\n"
- popd || exit
+ yarn attw "$file" --ignore-rules "named-exports"
+ yarn publint "$file"
done
echo "🚀 All NPM package checks passed! 🚀"
-rm -rf "${ARTIFACTS_DIR:?}/"*/
exit 0
diff --git a/yarn.lock b/yarn.lock
index 962df61f595..6a139363016 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -29,6 +29,13 @@ __metadata:
languageName: node
linkType: hard
+"@andrewbranch/untar.js@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "@andrewbranch/untar.js@npm:1.0.3"
+ checksum: 10/a32de53839fc61af90a394cf93d4368aacd167c9c80f0b3ba0c268460942a6ce2bfe257b6d3f03986b9dcb7368f10b9dc7f66c2f94254d2662da8278454e7d12
+ languageName: node
+ linkType: hard
+
"@apidevtools/json-schema-ref-parser@npm:9.0.6":
version: 9.0.6
resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6"
@@ -71,6 +78,39 @@ __metadata:
languageName: node
linkType: hard
+"@arethetypeswrong/cli@npm:^0.18.2":
+ version: 0.18.2
+ resolution: "@arethetypeswrong/cli@npm:0.18.2"
+ dependencies:
+ "@arethetypeswrong/core": "npm:0.18.2"
+ chalk: "npm:^4.1.2"
+ cli-table3: "npm:^0.6.3"
+ commander: "npm:^10.0.1"
+ marked: "npm:^9.1.2"
+ marked-terminal: "npm:^7.1.0"
+ semver: "npm:^7.5.4"
+ bin:
+ attw: dist/index.js
+ checksum: 10/8b4506edeb37d58f15e347302df68981c5aefecce973e746026d46370bb560c1ebc05ef8a38eba3102881df4cd3c901961186e3df41077efca4e58adffd455a1
+ languageName: node
+ linkType: hard
+
+"@arethetypeswrong/core@npm:0.18.2":
+ version: 0.18.2
+ resolution: "@arethetypeswrong/core@npm:0.18.2"
+ dependencies:
+ "@andrewbranch/untar.js": "npm:^1.0.3"
+ "@loaderkit/resolve": "npm:^1.0.2"
+ cjs-module-lexer: "npm:^1.2.3"
+ fflate: "npm:^0.8.2"
+ lru-cache: "npm:^11.0.1"
+ semver: "npm:^7.5.4"
+ typescript: "npm:5.6.1-rc"
+ validate-npm-package-name: "npm:^5.0.0"
+ checksum: 10/9c3edeb8e09e572682e37f55bd523d0dad45388232d31fa1d8875f7f5c414a184070c2bb6d0c8f254dfce4ed9248da373d549ecdeaa571a0cfff04c387c94cf1
+ languageName: node
+ linkType: hard
+
"@babel/code-frame@npm:7.25.7":
version: 7.25.7
resolution: "@babel/code-frame@npm:7.25.7"
@@ -1587,6 +1627,13 @@ __metadata:
languageName: node
linkType: hard
+"@braidai/lang@npm:^1.0.0":
+ version: 1.1.1
+ resolution: "@braidai/lang@npm:1.1.1"
+ checksum: 10/3d6b1827aabe8b4b39d938e1fecb747d3d45ea621958f8366d48b97e18b6b1edf68328ae57b8c0fb3d4b9757757a86761f911d9e15cde69bdbd28482952e43d6
+ languageName: node
+ linkType: hard
+
"@braintree/sanitize-url@npm:7.0.1":
version: 7.0.1
resolution: "@braintree/sanitize-url@npm:7.0.1"
@@ -4803,6 +4850,15 @@ __metadata:
languageName: node
linkType: hard
+"@loaderkit/resolve@npm:^1.0.2":
+ version: 1.0.4
+ resolution: "@loaderkit/resolve@npm:1.0.4"
+ dependencies:
+ "@braidai/lang": "npm:^1.0.0"
+ checksum: 10/e999f0fc289c2e3f9f80ec92db69c123a5a74b5db7c4bc10292658fc9ef2e1afe6430346ca6cd52d941d7fc407bf28188c95bbbe0aa212c02c8716b5c4b03316
+ languageName: node
+ linkType: hard
+
"@locker/near-membrane-base@npm:0.13.6":
version: 0.13.6
resolution: "@locker/near-membrane-base@npm:0.13.6"
@@ -6128,6 +6184,13 @@ __metadata:
languageName: node
linkType: hard
+"@publint/pack@npm:^0.1.2":
+ version: 0.1.2
+ resolution: "@publint/pack@npm:0.1.2"
+ checksum: 10/83e1de31ae29a0e651f7f91ebe6ad1fdf8cbb61d1eb056476586a234d05fa6fde9f34d3a0e36fbf18a2e9affa1082f758833242fd285637d303130f1a286b928
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-compose-refs@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-compose-refs@npm:1.0.1"
@@ -7031,6 +7094,13 @@ __metadata:
languageName: node
linkType: hard
+"@sindresorhus/is@npm:^4.6.0":
+ version: 4.6.0
+ resolution: "@sindresorhus/is@npm:4.6.0"
+ checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0
+ languageName: node
+ linkType: hard
+
"@sindresorhus/merge-streams@npm:^2.1.0":
version: 2.3.0
resolution: "@sindresorhus/merge-streams@npm:2.3.0"
@@ -11138,6 +11208,15 @@ __metadata:
languageName: node
linkType: hard
+"ansi-escapes@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "ansi-escapes@npm:7.0.0"
+ dependencies:
+ environment: "npm:^1.0.0"
+ checksum: 10/2d0e2345087bd7ae6bf122b9cc05ee35560d40dcc061146edcdc02bc2d7c7c50143cd12a22e69a0b5c0f62b948b7bc9a4539ee888b80f5bd33cdfd82d01a70ab
+ languageName: node
+ linkType: hard
+
"ansi-html-community@npm:0.0.8, ansi-html-community@npm:^0.0.8":
version: 0.0.8
resolution: "ansi-html-community@npm:0.0.8"
@@ -11177,6 +11256,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-regex@npm:^6.1.0":
+ version: 6.1.0
+ resolution: "ansi-regex@npm:6.1.0"
+ checksum: 10/495834a53b0856c02acd40446f7130cb0f8284f4a39afdab20d5dc42b2e198b1196119fe887beed8f9055c4ff2055e3b2f6d4641d0be018cdfb64fedf6fc1aac
+ languageName: node
+ linkType: hard
+
"ansi-styles@npm:^3.2.1":
version: 3.2.1
resolution: "ansi-styles@npm:3.2.1"
@@ -11230,6 +11316,13 @@ __metadata:
languageName: node
linkType: hard
+"any-promise@npm:^1.0.0":
+ version: 1.3.0
+ resolution: "any-promise@npm:1.3.0"
+ checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb
+ languageName: node
+ linkType: hard
+
"anymatch@npm:^3.0.3, anymatch@npm:^3.1.1, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2":
version: 3.1.3
resolution: "anymatch@npm:3.1.3"
@@ -12522,7 +12615,7 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^5.2.0, chalk@npm:^5.3.0":
+"chalk@npm:^5.2.0, chalk@npm:^5.3.0, chalk@npm:^5.4.1":
version: 5.4.1
resolution: "chalk@npm:5.4.1"
checksum: 10/29df3ffcdf25656fed6e95962e2ef86d14dfe03cd50e7074b06bad9ffbbf6089adbb40f75c00744d843685c8d008adaf3aed31476780312553caf07fa86e5bc7
@@ -12786,6 +12879,22 @@ __metadata:
languageName: node
linkType: hard
+"cli-highlight@npm:^2.1.11":
+ version: 2.1.11
+ resolution: "cli-highlight@npm:2.1.11"
+ dependencies:
+ chalk: "npm:^4.0.0"
+ highlight.js: "npm:^10.7.1"
+ mz: "npm:^2.4.0"
+ parse5: "npm:^5.1.1"
+ parse5-htmlparser2-tree-adapter: "npm:^6.0.0"
+ yargs: "npm:^16.0.0"
+ bin:
+ highlight: bin/highlight
+ checksum: 10/05d2b5beb8a4d3259f693517d013bf53d04ad20f470b77c3d02e051963092fae388388e3127f67d3679884a0c32cb855bf590292017c5e68c0f8d86f4b8e146e
+ languageName: node
+ linkType: hard
+
"cli-spinners@npm:2.6.1":
version: 2.6.1
resolution: "cli-spinners@npm:2.6.1"
@@ -12800,7 +12909,7 @@ __metadata:
languageName: node
linkType: hard
-"cli-table3@npm:~0.6.5":
+"cli-table3@npm:^0.6.3, cli-table3@npm:^0.6.5, cli-table3@npm:~0.6.5":
version: 0.6.5
resolution: "cli-table3@npm:0.6.5"
dependencies:
@@ -13082,7 +13191,7 @@ __metadata:
languageName: node
linkType: hard
-"commander@npm:^10.0.0":
+"commander@npm:^10.0.0, commander@npm:^10.0.1":
version: 10.0.1
resolution: "commander@npm:10.0.1"
checksum: 10/8799faa84a30da985802e661cc9856adfaee324d4b138413013ef7f087e8d7924b144c30a1f1405475f0909f467665cd9e1ce13270a2f41b141dab0b7a58f3fb
@@ -15241,6 +15350,13 @@ __metadata:
languageName: node
linkType: hard
+"emojilib@npm:^2.4.0":
+ version: 2.4.0
+ resolution: "emojilib@npm:2.4.0"
+ checksum: 10/bef767eca49acaa881388d91bee6936ea57ae367d603d5227ff0a9da3e2d1e774a61c447e5f2f4901797d023c4b5239bc208285b6172a880d3655024a0f44980
+ languageName: node
+ linkType: hard
+
"emojis-list@npm:^3.0.0":
version: 3.0.0
resolution: "emojis-list@npm:3.0.0"
@@ -15415,6 +15531,13 @@ __metadata:
languageName: node
linkType: hard
+"environment@npm:^1.0.0":
+ version: 1.1.0
+ resolution: "environment@npm:1.1.0"
+ checksum: 10/dd3c1b9825e7f71f1e72b03c2344799ac73f2e9ef81b78ea8b373e55db021786c6b9f3858ea43a436a2c4611052670ec0afe85bc029c384cc71165feee2f4ba6
+ languageName: node
+ linkType: hard
+
"eol@npm:^0.9.1":
version: 0.9.1
resolution: "eol@npm:0.9.1"
@@ -16700,6 +16823,13 @@ __metadata:
languageName: node
linkType: hard
+"fflate@npm:^0.8.2":
+ version: 0.8.2
+ resolution: "fflate@npm:0.8.2"
+ checksum: 10/2bd26ba6d235d428de793c6a0cd1aaa96a06269ebd4e21b46c8fd1bd136abc631acf27e188d47c3936db090bf3e1ede11d15ce9eae9bffdc4bfe1b9dc66ca9cb
+ languageName: node
+ linkType: hard
+
"figures@npm:3.2.0, figures@npm:^3.0.0, figures@npm:^3.2.0":
version: 3.2.0
resolution: "figures@npm:3.2.0"
@@ -17856,6 +17986,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "grafana@workspace:."
dependencies:
+ "@arethetypeswrong/cli": "npm:^0.18.2"
"@babel/core": "npm:7.26.10"
"@babel/preset-env": "npm:7.26.9"
"@babel/runtime": "npm:7.27.0"
@@ -18120,6 +18251,7 @@ __metadata:
postcss-scss: "npm:4.0.9"
prettier: "npm:3.4.2"
prismjs: "npm:1.30.0"
+ publint: "npm:^0.3.12"
rc-slider: "npm:11.1.8"
rc-tree: "npm:5.13.1"
re-resizable: "npm:6.10.3"
@@ -18428,7 +18560,7 @@ __metadata:
languageName: node
linkType: hard
-"highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0":
+"highlight.js@npm:^10.4.1, highlight.js@npm:^10.7.1, highlight.js@npm:~10.7.0":
version: 10.7.3
resolution: "highlight.js@npm:10.7.3"
checksum: 10/db8d10a541936b058e221dbde77869664b2b45bca75d660aa98065be2cd29f3924755fbc7348213f17fd931aefb6e6597448ba6fe82afba6d8313747a91983ee
@@ -21895,6 +22027,13 @@ __metadata:
languageName: node
linkType: hard
+"lru-cache@npm:^11.0.1":
+ version: 11.1.0
+ resolution: "lru-cache@npm:11.1.0"
+ checksum: 10/5011011675ca98428902de774d0963b68c3a193cd959347cb63b781dad4228924124afab82159fd7b8b4db18285d9aff462b877b8f6efd2b41604f806c1d9db4
+ languageName: node
+ linkType: hard
+
"lru-cache@npm:^5.1.1":
version: 5.1.1
resolution: "lru-cache@npm:5.1.1"
@@ -22115,6 +22254,23 @@ __metadata:
languageName: node
linkType: hard
+"marked-terminal@npm:^7.1.0":
+ version: 7.3.0
+ resolution: "marked-terminal@npm:7.3.0"
+ dependencies:
+ ansi-escapes: "npm:^7.0.0"
+ ansi-regex: "npm:^6.1.0"
+ chalk: "npm:^5.4.1"
+ cli-highlight: "npm:^2.1.11"
+ cli-table3: "npm:^0.6.5"
+ node-emoji: "npm:^2.2.0"
+ supports-hyperlinks: "npm:^3.1.0"
+ peerDependencies:
+ marked: ">=1 <16"
+ checksum: 10/1dfdfe752a4ebe6aec8de4a51180612a5f29982026b104a86215efb46b82b2a1942531a6bb840163c8d827e3eadc5cf93272e6eb29ec549f72b73b8b2eb97cfe
+ languageName: node
+ linkType: hard
+
"marked@npm:15.0.12":
version: 15.0.12
resolution: "marked@npm:15.0.12"
@@ -22124,6 +22280,15 @@ __metadata:
languageName: node
linkType: hard
+"marked@npm:^9.1.2":
+ version: 9.1.6
+ resolution: "marked@npm:9.1.6"
+ bin:
+ marked: bin/marked.js
+ checksum: 10/29d073500c70b6b53cd35a8d19f5e43df6e2819ddeca8848a31901b87b82ca0ea46a8a831920c656c69c33ad5dce4b75654c4c4ced34a67f4e4e4a31c7620cfe
+ languageName: node
+ linkType: hard
+
"matcher-collection@npm:^2.0.0":
version: 2.0.1
resolution: "matcher-collection@npm:2.0.1"
@@ -22809,6 +22974,13 @@ __metadata:
languageName: node
linkType: hard
+"mri@npm:^1.1.0":
+ version: 1.2.0
+ resolution: "mri@npm:1.2.0"
+ checksum: 10/6775a1d2228bb9d191ead4efc220bd6be64f943ad3afd4dcb3b3ac8fc7b87034443f666e38805df38e8d047b29f910c3cc7810da0109af83e42c82c73bd3f6bc
+ languageName: node
+ linkType: hard
+
"mrmime@npm:^2.0.0":
version: 2.0.0
resolution: "mrmime@npm:2.0.0"
@@ -22916,6 +23088,17 @@ __metadata:
languageName: node
linkType: hard
+"mz@npm:^2.4.0":
+ version: 2.7.0
+ resolution: "mz@npm:2.7.0"
+ dependencies:
+ any-promise: "npm:^1.0.0"
+ object-assign: "npm:^4.0.1"
+ thenify-all: "npm:^1.0.0"
+ checksum: 10/8427de0ece99a07e9faed3c0c6778820d7543e3776f9a84d22cf0ec0a8eb65f6e9aee9c9d353ff9a105ff62d33a9463c6ca638974cc652ee8140cd1e35951c87
+ languageName: node
+ linkType: hard
+
"nano-css@npm:^5.6.1, nano-css@npm:^5.6.2":
version: 5.6.2
resolution: "nano-css@npm:5.6.2"
@@ -23030,6 +23213,18 @@ __metadata:
languageName: node
linkType: hard
+"node-emoji@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "node-emoji@npm:2.2.0"
+ dependencies:
+ "@sindresorhus/is": "npm:^4.6.0"
+ char-regex: "npm:^1.0.2"
+ emojilib: "npm:^2.4.0"
+ skin-tone: "npm:^2.0.0"
+ checksum: 10/2548668f5cc9f781c94dc39971a630b2887111e0970c29fc523e924819d1b39b53a2694a4d1046861adf538c4462d06ee0269c48717ccad30336a918d9a911d5
+ languageName: node
+ linkType: hard
+
"node-ensure@npm:^0.0.0":
version: 0.0.0
resolution: "node-ensure@npm:0.0.0"
@@ -23579,7 +23774,7 @@ __metadata:
languageName: node
linkType: hard
-"object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
+"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f
@@ -24156,6 +24351,13 @@ __metadata:
languageName: node
linkType: hard
+"package-manager-detector@npm:^1.1.0":
+ version: 1.3.0
+ resolution: "package-manager-detector@npm:1.3.0"
+ checksum: 10/b21155d53a8ab96d5be3bfae43cc1d397bf363782b922d1f6967d220d2a9f08234ebb76035318bf92822ce761d10451959f01019faebc08fdb4d4a8bc3103da6
+ languageName: node
+ linkType: hard
+
"pacote@npm:^18.0.0, pacote@npm:^18.0.6":
version: 18.0.6
resolution: "pacote@npm:18.0.6"
@@ -24356,6 +24558,15 @@ __metadata:
languageName: node
linkType: hard
+"parse5-htmlparser2-tree-adapter@npm:^6.0.0":
+ version: 6.0.1
+ resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1"
+ dependencies:
+ parse5: "npm:^6.0.1"
+ checksum: 10/3400a2cd1ad450b2fe148544154f86ea53d3ed6b6eab56c78bb43b9629d3dfe9f580dffd75bbf32be134ffef645b68081fc764bf75c210f236ab9c5c8c38c252
+ languageName: node
+ linkType: hard
+
"parse5-htmlparser2-tree-adapter@npm:^7.0.0":
version: 7.0.0
resolution: "parse5-htmlparser2-tree-adapter@npm:7.0.0"
@@ -24375,6 +24586,20 @@ __metadata:
languageName: node
linkType: hard
+"parse5@npm:^5.1.1":
+ version: 5.1.1
+ resolution: "parse5@npm:5.1.1"
+ checksum: 10/5b509744cfe81488a33be05578df490c460690e64519fa67f0a0acb9c1bca05914e8acad17a977e2cf5964a000e43959b40024f0c243dd6595dd0cca8a32f71b
+ languageName: node
+ linkType: hard
+
+"parse5@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "parse5@npm:6.0.1"
+ checksum: 10/dfb110581f62bd1425725a7c784ae022a24669bd0efc24b58c71fc731c4d868193e2ebd85b74cde2dbb965e4dcf07059b1e651adbec1b3b5267531bd132fdb75
+ languageName: node
+ linkType: hard
+
"parse5@npm:^7.0.0, parse5@npm:^7.1.1, parse5@npm:^7.1.2":
version: 7.1.2
resolution: "parse5@npm:7.1.2"
@@ -25536,6 +25761,20 @@ __metadata:
languageName: node
linkType: hard
+"publint@npm:^0.3.12":
+ version: 0.3.12
+ resolution: "publint@npm:0.3.12"
+ dependencies:
+ "@publint/pack": "npm:^0.1.2"
+ package-manager-detector: "npm:^1.1.0"
+ picocolors: "npm:^1.1.1"
+ sade: "npm:^1.8.1"
+ bin:
+ publint: src/cli.js
+ checksum: 10/77153a20821b58fbe57e3d90e2ddd7c014c4a7dd4b506f8919ef0ecbb1e14745514c89be9d72623dcad014f0c11dbc0b204c05f713297c4435b695ba9f874c21
+ languageName: node
+ linkType: hard
+
"pump@npm:^3.0.0":
version: 3.0.0
resolution: "pump@npm:3.0.0"
@@ -27794,6 +28033,15 @@ __metadata:
languageName: node
linkType: hard
+"sade@npm:^1.8.1":
+ version: 1.8.1
+ resolution: "sade@npm:1.8.1"
+ dependencies:
+ mri: "npm:^1.1.0"
+ checksum: 10/1c67ba03c94083e0ae307ff5564ecb86c2104c0f558042fdaa40ea0054f91a63a9783f14069870f2f784336adabb70f90f22a84dc457b5a25e859aaadefe0910
+ languageName: node
+ linkType: hard
+
"safe-array-concat@npm:^1.1.3":
version: 1.1.3
resolution: "safe-array-concat@npm:1.1.3"
@@ -28455,6 +28703,15 @@ __metadata:
languageName: node
linkType: hard
+"skin-tone@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "skin-tone@npm:2.0.0"
+ dependencies:
+ unicode-emoji-modifier-base: "npm:^1.0.0"
+ checksum: 10/19de157586b8019cacc55eb25d9d640f00fc02415761f3e41a4527142970fd4e7f6af0333bc90e879858766c20a976107bb386ffd4c812289c01d51f2c8d182c
+ languageName: node
+ linkType: hard
+
"slash@npm:3.0.0, slash@npm:^3.0.0":
version: 3.0.0
resolution: "slash@npm:3.0.0"
@@ -29946,6 +30203,24 @@ __metadata:
languageName: node
linkType: hard
+"thenify-all@npm:^1.0.0":
+ version: 1.6.0
+ resolution: "thenify-all@npm:1.6.0"
+ dependencies:
+ thenify: "npm:>= 3.1.0 < 4"
+ checksum: 10/dba7cc8a23a154cdcb6acb7f51d61511c37a6b077ec5ab5da6e8b874272015937788402fd271fdfc5f187f8cb0948e38d0a42dcc89d554d731652ab458f5343e
+ languageName: node
+ linkType: hard
+
+"thenify@npm:>= 3.1.0 < 4":
+ version: 3.3.1
+ resolution: "thenify@npm:3.3.1"
+ dependencies:
+ any-promise: "npm:^1.0.0"
+ checksum: 10/486e1283a867440a904e36741ff1a177faa827cf94d69506f7e3ae4187b9afdf9ec368b3d8da225c192bfe2eb943f3f0080594156bf39f21b57cd1411e2e7f6d
+ languageName: node
+ linkType: hard
+
"throttle-debounce@npm:^3.0.1":
version: 3.0.1
resolution: "throttle-debounce@npm:3.0.1"
@@ -30702,6 +30977,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@npm:5.6.1-rc":
+ version: 5.6.1-rc
+ resolution: "typescript@npm:5.6.1-rc"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10/5716659d5baf142b5c84b96209b30730a5e9dcc0202f879349f9974823f7452ec4ef3904397b6d89d861c688acdbb1dad0a449d753163519fae2ee06ea4a68be
+ languageName: node
+ linkType: hard
+
"typescript@npm:5.7.3":
version: 5.7.3
resolution: "typescript@npm:5.7.3"
@@ -30732,6 +31017,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@patch:typescript@npm%3A5.6.1-rc#optional!builtin":
+ version: 5.6.1-rc
+ resolution: "typescript@patch:typescript@npm%3A5.6.1-rc#optional!builtin::version=5.6.1-rc&hash=8c6c40"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10/462e0bb46c63abfc5bfc43f2bb00b9777a4228f3ed52d8930b46404dce71dbada63c27a99262ff4570b5ff7d01455701bfd36823bd3c766e443b6fa33cd31dea
+ languageName: node
+ linkType: hard
+
"typescript@patch:typescript@npm%3A5.7.3#optional!builtin":
version: 5.7.3
resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5"
@@ -30825,6 +31120,13 @@ __metadata:
languageName: node
linkType: hard
+"unicode-emoji-modifier-base@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "unicode-emoji-modifier-base@npm:1.0.0"
+ checksum: 10/6e1521d35fa69493207eb8b41f8edb95985d8b3faf07c01d820a1830b5e8403e20002563e2f84683e8e962a49beccae789f0879356bf92a4ec7a4dd8e2d16fdb
+ languageName: node
+ linkType: hard
+
"unicode-match-property-ecmascript@npm:^2.0.0":
version: 2.0.0
resolution: "unicode-match-property-ecmascript@npm:2.0.0"
@@ -32301,7 +32603,7 @@ __metadata:
languageName: node
linkType: hard
-"yargs@npm:^16.2.0":
+"yargs@npm:^16.0.0, yargs@npm:^16.2.0":
version: 16.2.0
resolution: "yargs@npm:16.2.0"
dependencies:
From 21297b90faaa51e032b1b5374e9474677cb5e17b Mon Sep 17 00:00:00 2001
From: Tania <10127682+undef1nd@users.noreply.github.com>
Date: Wed, 11 Jun 2025 11:25:35 +0200
Subject: [PATCH 10/32] Chore: Fix feature flags template for docs gen
(#106531)
---
pkg/services/featuremgmt/toggles_gen_test.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go
index bae02255b88..d3c7f88f746 100644
--- a/pkg/services/featuremgmt/toggles_gen_test.go
+++ b/pkg/services/featuremgmt/toggles_gen_test.go
@@ -393,6 +393,7 @@ func generateDocsMD() string {
buf := `---
aliases:
- /docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/
+ - ../../administration/feature-toggles/ # /docs/grafana/latest/administration/feature-toggles/
description: Learn about feature toggles, which you can enable or disable.
title: Configure feature toggles
weight: 150
From 6af09ed763a9b28b741ab35069697c6da00b03b2 Mon Sep 17 00:00:00 2001
From: Alexa Vargas <239999+axelavargas@users.noreply.github.com>
Date: Wed, 11 Jun 2025 11:28:13 +0200
Subject: [PATCH 11/32] Dashboard: Schema V2 - Auto-transform V2 dashboards in
V1Resource export mode (#105997)
* experiment v2 to v1 in exporting
* refactor code to export to v1 resource
* Add unit test and fix linting
* fix typescript
* fix linting
* handle error gracefully when is not possible to convert to v1
---
.../sharing/ExportButton/ResourceExport.tsx | 23 +-
.../sharing/ShareExportTab.test.tsx | 333 ++++++++++++++++++
.../sharing/ShareExportTab.tsx | 83 ++++-
3 files changed, 424 insertions(+), 15 deletions(-)
create mode 100644 public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx
diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx
index a44652a0fe3..fae5d4a5991 100644
--- a/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx
+++ b/public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx
@@ -74,6 +74,25 @@ export function ResourceExport({
/>
)}
+ {initialSaveModelVersion === 'v2' && (
+
+
+ onExportModeChange(value)}
+ />
+
+ )}
{exportMode !== ExportMode.Classic && (
@@ -87,7 +106,9 @@ export function ResourceExport({
/>
)}
- {(isV2Dashboard || exportMode === ExportMode.Classic) && (
+ {(isV2Dashboard ||
+ exportMode === ExportMode.Classic ||
+ (initialSaveModelVersion === 'v2' && exportMode === ExportMode.V1Resource)) && (
diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx
new file mode 100644
index 00000000000..fb416a280fb
--- /dev/null
+++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.test.tsx
@@ -0,0 +1,333 @@
+import { config } from '@grafana/runtime';
+import { SceneTimeRange } from '@grafana/scenes';
+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 * as ResponseTransformers from 'app/features/dashboard/api/ResponseTransformers';
+import { DashboardJson } from 'app/features/manage-dashboards/types';
+import { DashboardDataDTO } from 'app/types/dashboard';
+
+import { DashboardScene } from '../scene/DashboardScene';
+import * as exporters from '../scene/export/exporters';
+import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
+import * as sceneToV1 from '../serialization/transformSceneToSaveModel';
+import * as sceneToV2 from '../serialization/transformSceneToSaveModelSchemaV2';
+
+import { ExportMode } from './ExportButton/ResourceExport';
+import { ShareExportTab } from './ShareExportTab';
+
+describe('ShareExportTab', () => {
+ // Spies to track function calls
+ let transformV2ToV1Spy: jest.SpyInstance;
+ let makeExportableV1Spy: jest.SpyInstance;
+ let transformSceneToV1Spy: jest.SpyInstance;
+ let transformSceneToV2Spy: jest.SpyInstance;
+
+ beforeEach(() => {
+ config.featureToggles.kubernetesDashboards = true;
+
+ // Set up spies on the functions we want to track
+ transformV2ToV1Spy = jest.spyOn(ResponseTransformers, 'transformDashboardV2SpecToV1').mockReturnValue({
+ title: 'Transformed V1',
+ uid: 'transformed-uid',
+ version: 1,
+ panels: [],
+ time: { from: 'now-6h', to: 'now' },
+ timepicker: {},
+ timezone: '',
+ weekStart: '',
+ fiscalYearStartMonth: 0,
+ refresh: '',
+ schemaVersion: 30,
+ tags: [],
+ templating: { list: [] },
+ } as DashboardDataDTO);
+
+ makeExportableV1Spy = jest.spyOn(exporters, 'makeExportableV1').mockImplementation(async (dashboard) => dashboard);
+
+ transformSceneToV1Spy = jest.spyOn(sceneToV1, 'transformSceneToSaveModel').mockReturnValue({
+ title: 'Scene V1',
+ uid: 'scene-v1-uid',
+ version: 1,
+ panels: [],
+ time: { from: 'now-6h', to: 'now' },
+ timepicker: {},
+ timezone: '',
+ weekStart: '',
+ fiscalYearStartMonth: 0,
+ refresh: '',
+ schemaVersion: 30,
+ tags: [],
+ templating: { list: [] },
+ } as Dashboard);
+
+ transformSceneToV2Spy = jest.spyOn(sceneToV2, 'transformSceneToSaveModelSchemaV2').mockReturnValue({
+ title: 'Scene V2',
+ annotations: [],
+ cursorSync: 'Off',
+ description: '',
+ editable: true,
+ elements: {},
+ layout: { kind: 'GridLayout', spec: { items: [] } },
+ links: [],
+ liveNow: false,
+ preload: false,
+ tags: [],
+ timeSettings: {
+ from: 'now-6h',
+ to: 'now',
+ autoRefresh: '',
+ autoRefreshIntervals: [],
+ hideTimepicker: false,
+ timezone: '',
+ weekStart: 'saturday',
+ fiscalYearStartMonth: 0,
+ },
+ variables: [],
+ } as DashboardV2Spec);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ describe('V1Resource export mode', () => {
+ // If V1 dashboard → V1 Resource should export with V1 apiVersion
+ it('should export V1 dashboard as V1 resource with correct apiVersion', async () => {
+ const tab = buildV1DashboardScenario();
+ tab.setState({ exportMode: ExportMode.V1Resource });
+
+ const result = await tab.getExportableDashboardJson();
+
+ // Should use V1 API version
+ expect(result.json).toMatchObject({
+ apiVersion: 'dashboard.grafana.app/v1beta1',
+ kind: 'Dashboard',
+ status: {},
+ });
+
+ // Should call transformSceneToV1 (not transform V2→V1)
+ expect(transformSceneToV1Spy).toHaveBeenCalled();
+ expect(transformV2ToV1Spy).not.toHaveBeenCalled();
+
+ // Should report correct initial version
+ expect(result.initialSaveModelVersion).toBe('v1');
+ });
+
+ // If V2 dashboard → V1 Resource should auto-transform with V1 apiVersion
+ it('should auto-transform V2 dashboard to V1 resource with correct apiVersion', async () => {
+ const tab = buildV2DashboardScenario();
+ // user selects V1Resource even though is V2 dashboard
+ tab.setState({ exportMode: ExportMode.V1Resource });
+
+ const result = await tab.getExportableDashboardJson();
+
+ // Should use V1 API version (not V2!)
+ expect(result.json).toMatchObject({
+ apiVersion: 'dashboard.grafana.app/v1beta1',
+ kind: 'Dashboard',
+ status: {},
+ });
+
+ // Should auto-transform V2→V1
+ expect(transformSceneToV2Spy).toHaveBeenCalled(); // Get V2 spec first
+ expect(transformV2ToV1Spy).toHaveBeenCalled(); // Then transform to V1
+
+ // Should report correct initial version
+ expect(result.initialSaveModelVersion).toBe('v2');
+ });
+
+ // If V2 dashboard → V1 Resource with external sharing should transform and apply external sharing
+ it('should handle external sharing when transforming V2 to V1', async () => {
+ const tab = buildV2DashboardScenario();
+ tab.setState({
+ exportMode: ExportMode.V1Resource,
+ isSharingExternally: true,
+ });
+
+ const result = await tab.getExportableDashboardJson();
+
+ // Should use V1 API version
+ expect(result.json).toMatchObject({
+ apiVersion: 'dashboard.grafana.app/v1beta1',
+ kind: 'Dashboard',
+ status: {},
+ });
+
+ // Should auto-transform V2→V1
+ expect(transformSceneToV2Spy).toHaveBeenCalled();
+ expect(transformV2ToV1Spy).toHaveBeenCalled();
+
+ // Should call makeExportableV1 for external sharing
+ expect(makeExportableV1Spy).toHaveBeenCalled();
+
+ // Should report correct initial version
+ expect(result.initialSaveModelVersion).toBe('v2');
+ });
+ });
+
+ describe('V2Resource export mode', () => {
+ // If V2 dashboard → V2 Resource should export with V2 apiVersion
+ it('should export V2 dashboard as V2 resource with correct apiVersion', async () => {
+ const tab = buildV2DashboardScenario();
+ tab.setState({ exportMode: ExportMode.V2Resource });
+
+ const result = await tab.getExportableDashboardJson();
+
+ // Should use V2 API version
+ expect(result.json).toMatchObject({
+ apiVersion: 'dashboard.grafana.app/v2alpha1',
+ kind: 'Dashboard',
+ status: {},
+ });
+
+ // Should not call V2→V1 transformation since source is already V2
+ expect(transformV2ToV1Spy).not.toHaveBeenCalled();
+
+ // Should report correct initial version
+ expect(result.initialSaveModelVersion).toBe('v2');
+ });
+ });
+
+ describe('Classic export mode', () => {
+ // If V1 dashboard → Classic should export plain dashboard JSON
+ it('should export V1 dashboard in classic format', async () => {
+ const tab = buildV1DashboardScenario();
+ tab.setState({ exportMode: ExportMode.Classic });
+
+ const result = await tab.getExportableDashboardJson();
+
+ // Should return plain dashboard JSON (not wrapped in resource)
+ expect(result.json).toMatchObject({
+ title: 'Test Dashboard V1',
+ uid: 'test-uid-v1',
+ panels: expect.any(Array),
+ });
+
+ // Should NOT have resource wrapper properties
+ expect(result.json).not.toHaveProperty('apiVersion');
+ expect(result.json).not.toHaveProperty('kind');
+ expect(result.json).not.toHaveProperty('status');
+
+ // Should report correct initial version
+ expect(result.initialSaveModelVersion).toBe('v1');
+ });
+ });
+
+ describe('Export mode state management', () => {
+ // If switching to Classic mode should disable YAML viewing
+ it('should disable YAML viewing when switching to Classic mode', async () => {
+ const tab = buildV1DashboardScenario();
+
+ // Start with YAML viewing enabled
+ tab.setState({ isViewingYAML: true });
+ expect(tab.state.isViewingYAML).toBe(true);
+
+ // Switch to Classic mode
+ tab.onExportModeChange(ExportMode.Classic);
+
+ // Should disable YAML viewing
+ expect(tab.state.isViewingYAML).toBe(false);
+ });
+
+ // If switching to resource modes should preserve YAML viewing
+ it('should preserve YAML viewing when switching to resource modes', async () => {
+ const tab = buildV2DashboardScenario();
+
+ // Start with YAML viewing enabled
+ tab.setState({ isViewingYAML: true });
+ expect(tab.state.isViewingYAML).toBe(true);
+
+ // Switch to V1Resource mode
+ tab.onExportModeChange(ExportMode.V1Resource);
+ expect(tab.state.isViewingYAML).toBe(true); // Should preserve
+
+ // Switch to V2Resource mode
+ tab.onExportModeChange(ExportMode.V2Resource);
+ expect(tab.state.isViewingYAML).toBe(true); // Should preserve
+ });
+ });
+
+ // Helper functions to create test scenarios
+ function buildV1DashboardScenario(): ShareExportTab {
+ const mockV1Dashboard: DashboardDataDTO = {
+ title: 'Test Dashboard V1',
+ uid: 'test-uid-v1',
+ version: 1,
+ panels: [],
+ time: { from: 'now-6h', to: 'now' },
+ timepicker: {},
+ timezone: '',
+ weekStart: '',
+ fiscalYearStartMonth: 0,
+ refresh: '',
+ schemaVersion: 30,
+ tags: [],
+ templating: { list: [] },
+ };
+
+ const tab = new ShareExportTab({});
+ const scene = new DashboardScene({
+ title: 'Test Dashboard V1',
+ uid: 'test-uid-v1',
+ meta: { canEdit: true },
+ $timeRange: new SceneTimeRange({}),
+ body: DefaultGridLayoutManager.fromVizPanels([]),
+ overlay: tab,
+ });
+
+ const mockExportableDashboard: DashboardJson = {
+ ...mockV1Dashboard,
+ panels: [],
+ } as DashboardJson;
+ scene.serializer.getSaveModel = jest.fn(() => mockV1Dashboard);
+ scene.serializer.makeExportableExternally = jest.fn(() => Promise.resolve(mockExportableDashboard));
+ scene.serializer.apiVersion = 'dashboard.grafana.app/v1beta1';
+ scene.getInitialSaveModel = jest.fn(() => mockV1Dashboard);
+
+ return tab;
+ }
+
+ function buildV2DashboardScenario(): ShareExportTab {
+ const mockV2Dashboard: DashboardV2Spec = {
+ title: 'Test Dashboard V2',
+ annotations: [],
+ cursorSync: 'Off',
+ description: 'Test V2 dashboard',
+ editable: true,
+ elements: {},
+ layout: { kind: 'GridLayout', spec: { items: [] } },
+ links: [],
+ liveNow: false,
+ preload: false,
+ tags: [],
+ timeSettings: {
+ from: 'now-6h',
+ to: 'now',
+ autoRefresh: '',
+ autoRefreshIntervals: [],
+ hideTimepicker: false,
+ timezone: '',
+ weekStart: 'saturday',
+ fiscalYearStartMonth: 0,
+ },
+ variables: [],
+ };
+
+ const tab = new ShareExportTab({});
+ const scene = new DashboardScene({
+ title: 'Test Dashboard V2',
+ uid: 'test-uid-v2',
+ meta: { canEdit: true },
+ $timeRange: new SceneTimeRange({}),
+ body: DefaultGridLayoutManager.fromVizPanels([]),
+ overlay: tab,
+ });
+
+ scene.serializer.getSaveModel = jest.fn(() => mockV2Dashboard);
+ scene.serializer.makeExportableExternally = jest.fn(() => Promise.resolve(mockV2Dashboard));
+ scene.serializer.apiVersion = 'dashboard.grafana.app/v2alpha1';
+ scene.getInitialSaveModel = jest.fn(() => mockV2Dashboard);
+
+ return tab;
+ }
+});
diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx
index c6878c3fcd4..4cb3d0951ea 100644
--- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx
@@ -12,12 +12,15 @@ 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 { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui';
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 { 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';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { DashboardJson } from 'app/features/manage-dashboards/types';
+import { DashboardDataDTO } from 'app/types/dashboard';
import { DashboardScene } from '../scene/DashboardScene';
import { makeExportableV1, makeExportableV2 } from '../scene/export/exporters';
@@ -34,7 +37,7 @@ export interface ExportableResource {
apiVersion: string;
kind: 'Dashboard';
metadata: DashboardWithAccessInfo['metadata'] | Partial;
- spec: Dashboard | DashboardModel | DashboardV2Spec | { error: unknown };
+ spec: Dashboard | DashboardModel | DashboardV2Spec | DashboardJson | DashboardDataDTO | { error: unknown };
// A placeholder for now because as code tooling expects it
status: {};
}
@@ -112,7 +115,12 @@ export class ShareExportTab extends SceneObjectBase impleme
const exportable = isSharingExternally ? exportableDashboard : origDashboard;
const metadata = getMetadata(scene, Boolean(isSharingExternally));
- if (isDashboardV2Spec(origDashboard) && 'elements' in exportable && initialSaveModelVersion === 'v2') {
+ if (
+ isDashboardV2Spec(origDashboard) &&
+ 'elements' in exportable &&
+ initialSaveModelVersion === 'v2' &&
+ exportMode !== ExportMode.V1Resource
+ ) {
this.setState({
exportMode: ExportMode.V2Resource,
});
@@ -131,19 +139,66 @@ export class ShareExportTab extends SceneObjectBase impleme
}
if (exportMode === ExportMode.V1Resource) {
- const spec = transformSceneToSaveModel(scene);
+ // Check if source is V2 and auto-transform to V1
+ if (isDashboardV2Spec(origDashboard) && initialSaveModelVersion === 'v2') {
+ try {
+ const spec = transformSceneToSaveModelSchemaV2(scene);
+ const metadata = getMetadata(scene, Boolean(isSharingExternally));
+ const spec1 = transformDashboardV2SpecToV1(spec, {
+ name: metadata.name ?? '',
+ generation: metadata.generation ?? 0,
+ resourceVersion: metadata.resourceVersion ?? '0',
+ creationTimestamp: metadata.creationTimestamp ?? '',
+ });
- return {
- json: {
- apiVersion: scene.serializer.apiVersion ?? '',
- kind: 'Dashboard',
- metadata,
- spec,
- status: {},
- },
- initialSaveModelVersion,
- hasLibraryPanels: undefined,
- };
+ let exportableV1: Dashboard | DashboardDataDTO | DashboardJson | { error: unknown };
+ if (isSharingExternally) {
+ const oldModel = new DashboardModel(spec1, undefined, {
+ getVariablesFromState: () => {
+ return getVariablesCompatibility(window.__grafanaSceneContext);
+ },
+ });
+ exportableV1 = await makeExportableV1(oldModel);
+ } else {
+ exportableV1 = spec1;
+ }
+ return {
+ json: {
+ // Forcing V1 version here to match export mode selection
+ apiVersion: `${K8S_V1_DASHBOARD_API_CONFIG.group}/${K8S_V1_DASHBOARD_API_CONFIG.version}`,
+ kind: 'Dashboard',
+ metadata,
+ spec: exportableV1,
+ status: {},
+ },
+ initialSaveModelVersion,
+ hasLibraryPanels: undefined,
+ };
+ } catch (err) {
+ return {
+ json: {
+ error: `Failed to convert dashboard to v1. ${err}`,
+ },
+ initialSaveModelVersion,
+ hasLibraryPanels: undefined,
+ };
+ }
+ } else {
+ // Source is already V1, export as-is
+ const spec = transformSceneToSaveModel(scene);
+ return {
+ json: {
+ // Forcing V1 version here to match export mode selection
+ apiVersion: `${K8S_V1_DASHBOARD_API_CONFIG.group}/${K8S_V1_DASHBOARD_API_CONFIG.version}`,
+ kind: 'Dashboard',
+ metadata,
+ spec,
+ status: {},
+ },
+ initialSaveModelVersion,
+ hasLibraryPanels: undefined,
+ };
+ }
}
if (exportMode === ExportMode.V2Resource) {
From 7688089a5763cb5417d6ff61bc4d23dbb6d896d5 Mon Sep 17 00:00:00 2001
From: Tito Lins
Date: Wed, 11 Jun 2025 11:49:45 +0200
Subject: [PATCH 12/32] alerting: stop using rule group idx to calculate alert
fingerprint (#106407)
---
pkg/services/ngalert/schedule/registry.go | 1 -
pkg/services/ngalert/schedule/registry_test.go | 3 ++-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go
index 9586e826176..240c62bdc40 100644
--- a/pkg/services/ngalert/schedule/registry.go
+++ b/pkg/services/ngalert/schedule/registry.go
@@ -318,7 +318,6 @@ func (r ruleWithFolder) Fingerprint() fingerprint {
writeInt(*rule.PanelID)
}
writeString(rule.RuleGroup)
- writeInt(int64(rule.RuleGroupIndex))
writeString(string(rule.NoDataState))
writeString(string(rule.ExecErrState))
if rule.Record != nil {
diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go
index d4c835cf6f6..9d838556ed7 100644
--- a/pkg/services/ngalert/schedule/registry_test.go
+++ b/pkg/services/ngalert/schedule/registry_test.go
@@ -152,13 +152,14 @@ func TestRuleWithFolderFingerprint(t *testing.T) {
f2 := ruleWithFolder{rule: rule, folderTitle: uuid.NewString()}.Fingerprint()
require.NotEqual(t, f, f2)
})
- t.Run("Version, Updated, IntervalSeconds, GUID and Annotations should be excluded from fingerprint", func(t *testing.T) {
+ t.Run("Version, Updated, IntervalSeconds, GUID, Annotations and RuleGroupIndex should be excluded from fingerprint", func(t *testing.T) {
cp := models.CopyRule(rule)
cp.Version++
cp.Updated = cp.Updated.Add(1 * time.Second)
cp.IntervalSeconds++
cp.Annotations = make(map[string]string)
cp.Annotations["test"] = "test"
+ cp.RuleGroupIndex++
cp.GUID = uuid.NewString()
f2 := ruleWithFolder{rule: cp, folderTitle: title}.Fingerprint()
From 291f33541df8374f48eb294be93c98d775f5f68e Mon Sep 17 00:00:00 2001
From: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Date: Wed, 11 Jun 2025 11:53:30 +0100
Subject: [PATCH 13/32] Dashboards: Add `id`s to auto grid inputs for improved
a11y (#106430)
---
.../AutoGridLayoutManagerEditor.tsx | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx
index 5db18bd4553..f861aa45d46 100644
--- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx
@@ -104,9 +104,15 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan
className={styles.wideSelector}
>
{isStandardMinWidth ? (
-
+
) : (
setInputRef(ref)}
@@ -130,6 +136,7 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan
layoutManager.onMaxColumnCountChanged(parseInt(value, 10))}
@@ -210,9 +217,10 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage
className={styles.wideSelector}
>
{isStandardHeight ? (
-
+
) : (
setInputRef(ref)}
@@ -235,7 +243,11 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage
)}
- layoutManager.onFillScreenChanged(!fillScreen)} />
+ layoutManager.onFillScreenChanged(!fillScreen)}
+ />
);
From c611021d7d68806c2056f0e3f2e5592cffe92aec Mon Sep 17 00:00:00 2001
From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
Date: Wed, 11 Jun 2025 06:58:34 -0400
Subject: [PATCH 14/32] Docs: Update experimental note (#106489)
---
.../build-dashboards/create-dynamic-dashboard/index.md | 8 +++++++-
docs/sources/observability-as-code/schema-v2/_index.md | 8 +++++++-
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md b/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md
index 47c4492204e..6c12e68e724 100644
--- a/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md
+++ b/docs/sources/dashboards/build-dashboards/create-dynamic-dashboard/index.md
@@ -77,7 +77,13 @@ refs:
# Create and edit dynamic dashboards
-{{< docs/experimental product="Dynamic dashboards" featureFlag="dashboardNewLayouts" >}}
+{{< admonition type="caution" >}}
+
+Dynamic dashboards 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.**
+
+{{< /admonition >}}
Dashboards and panels allow you to show your data in visual form. Each panel needs at least one query to display a visualization.
diff --git a/docs/sources/observability-as-code/schema-v2/_index.md b/docs/sources/observability-as-code/schema-v2/_index.md
index c86fd130818..d7c2728947b 100644
--- a/docs/sources/observability-as-code/schema-v2/_index.md
+++ b/docs/sources/observability-as-code/schema-v2/_index.md
@@ -18,7 +18,13 @@ weight: 200
# Dashboard JSON schema v2
-{{< docs/experimental product="Dashboard JSON schema v2" featureFlag="`dashboardNewLayouts`" >}}
+{{< 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. 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.**
+
+{{< /admonition >}}
Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings.
From eaac4a69fb191b368b581e1ab98943c1e6ab80fa Mon Sep 17 00:00:00 2001
From: Alexander Akhmetov
Date: Wed, 11 Jun 2025 13:45:02 +0200
Subject: [PATCH 15/32] Alerting: Empty endpoints to manage alertmanager
configurations (#106546)
---
.../ngalert/api/api_convert_prometheus.go | 12 +-
pkg/services/ngalert/api/authorization.go | 5 +-
.../generated_base_api_convert_prometheus.go | 32 +++++
.../ngalert/api/prometheus_conversion.go | 8 ++
pkg/services/ngalert/api/tooling/api.json | 45 ++++--
.../definitions/convert_prometheus_api.go | 87 +++++++++++-
pkg/services/ngalert/api/tooling/post.json | 128 ++++++++++++++++--
pkg/services/ngalert/api/tooling/spec.json | 128 ++++++++++++++++--
public/api-merged.json | 45 ++++--
public/openapi3.json | 45 ++++--
10 files changed, 463 insertions(+), 72 deletions(-)
diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go
index 24b269cdfa5..5141ef89c0a 100644
--- a/pkg/services/ngalert/api/api_convert_prometheus.go
+++ b/pkg/services/ngalert/api/api_convert_prometheus.go
@@ -513,8 +513,16 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
return grafanaGroup, nil
}
-func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, config apimodels.AlertmanagerUserConfig) response.Response {
- return response.Error(501, "Not implemented", nil)
+func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, amCfg apimodels.AlertmanagerUserConfig) response.Response {
+ return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
+}
+
+func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
+ return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
+}
+
+func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
+ return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
// parseBooleanHeader parses a boolean header value, returning an error if the header
diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go
index 9eccfa36d2e..46168688ee6 100644
--- a/pkg/services/ngalert/api/authorization.go
+++ b/pkg/services/ngalert/api/authorization.go
@@ -147,8 +147,11 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
)
- case http.MethodPost + "/api/convert/api/v1/alerts":
+ case http.MethodPost + "/api/convert/api/v1/alerts",
+ http.MethodDelete + "/api/convert/api/v1/alerts":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)
+ case http.MethodGet + "/api/convert/api/v1/alerts":
+ eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
// Alert Instances and Silences
diff --git a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go
index a414823d62e..aaf31682e85 100644
--- a/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go
+++ b/pkg/services/ngalert/api/generated_base_api_convert_prometheus.go
@@ -26,8 +26,10 @@ type ConvertPrometheusApi interface {
RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexPostRuleGroups(*contextmodel.ReqContext) response.Response
+ RouteConvertPrometheusDeleteAlertmanagerConfig(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response
+ RouteConvertPrometheusGetAlertmanagerConfig(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response
@@ -69,6 +71,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusCortexPostRuleGroups(ctx)
}
+func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
+ return f.handleRouteConvertPrometheusDeleteAlertmanagerConfig(ctx)
+}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@@ -80,6 +85,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteRuleGroup(ctx
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitleParam, groupParam)
}
+func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
+ return f.handleRouteConvertPrometheusGetAlertmanagerConfig(ctx)
+}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@@ -192,6 +200,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
m,
),
)
+ group.Delete(
+ toMacaronPath("/api/convert/api/v1/alerts"),
+ requestmeta.SetOwner(requestmeta.TeamAlerting),
+ requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
+ api.authorize(http.MethodDelete, "/api/convert/api/v1/alerts"),
+ metrics.Instrument(
+ http.MethodDelete,
+ "/api/convert/api/v1/alerts",
+ api.Hooks.Wrap(srv.RouteConvertPrometheusDeleteAlertmanagerConfig),
+ m,
+ ),
+ )
group.Delete(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
@@ -216,6 +236,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
m,
),
)
+ group.Get(
+ toMacaronPath("/api/convert/api/v1/alerts"),
+ requestmeta.SetOwner(requestmeta.TeamAlerting),
+ requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
+ api.authorize(http.MethodGet, "/api/convert/api/v1/alerts"),
+ metrics.Instrument(
+ http.MethodGet,
+ "/api/convert/api/v1/alerts",
+ api.Hooks.Wrap(srv.RouteConvertPrometheusGetAlertmanagerConfig),
+ m,
+ ),
+ )
group.Get(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go
index 332f2b39f14..4006dcb0e10 100644
--- a/pkg/services/ngalert/api/prometheus_conversion.go
+++ b/pkg/services/ngalert/api/prometheus_conversion.go
@@ -134,3 +134,11 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostAlertmanag
return f.svc.RouteConvertPrometheusPostAlertmanagerConfig(ctx, config)
}
+
+func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
+ return f.svc.RouteConvertPrometheusGetAlertmanagerConfig(ctx)
+}
+
+func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusDeleteAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
+ return f.svc.RouteConvertPrometheusDeleteAlertmanagerConfig(ctx)
+}
diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json
index 84979f94fb4..ddbb155712a 100644
--- a/pkg/services/ngalert/api/tooling/api.json
+++ b/pkg/services/ngalert/api/tooling/api.json
@@ -580,6 +580,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
+ },
+ "template_files": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
}
},
"type": "object"
@@ -1408,6 +1414,20 @@
"title": "Frames is a slice of Frame pointers.",
"type": "array"
},
+ "GettableAlertmanagerUserConfig": {
+ "properties": {
+ "alertmanager_config": {
+ "type": "string"
+ },
+ "template_files": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
"GettableAlertmanagers": {
"properties": {
"data": {
@@ -2038,7 +2058,11 @@
"description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.",
"properties": {
"equal": {
- "$ref": "#/definitions/LabelNames"
+ "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
"source_match": {
"additionalProperties": {
@@ -2162,17 +2186,6 @@
"title": "Label is a key/value pair of strings.",
"type": "object"
},
- "LabelName": {
- "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
- "type": "string"
- },
- "LabelNames": {
- "items": {
- "$ref": "#/definitions/LabelName"
- },
- "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
- "type": "array"
- },
"LabelSet": {
"additionalProperties": {
"$ref": "#/definitions/LabelValue"
@@ -3674,6 +3687,7 @@
"type": "object"
},
"Route": {
+ "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
@@ -3715,6 +3729,12 @@
},
"type": "array"
},
+ "object_matchers": {
+ "$ref": "#/definitions/ObjectMatchers"
+ },
+ "provenance": {
+ "$ref": "#/definitions/Provenance"
+ },
"receiver": {
"type": "string"
},
@@ -3728,7 +3748,6 @@
"type": "array"
}
},
- "title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object"
},
"RouteExport": {
diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go
index 813c99e8cbc..c4609cbc5e4 100644
--- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go
+++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go
@@ -3,6 +3,7 @@ package definitions
import (
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model"
+ "gopkg.in/yaml.v3"
)
// Route for mimirtool
@@ -204,7 +205,10 @@ import (
// Route for `mimirtool alertmanager load`
// swagger:route POST /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusPostAlertmanagerConfig
//
-// Load Alertmanager configuration to Grafana and merge it with the existing configuration.
+// Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.
+// This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by
+// a unique identifier and can include merge matchers to select which alerts should be handled by
+// this specific configuration.
//
// Produces:
// - application/json
@@ -216,6 +220,32 @@ import (
// Extensions:
// x-raw-request: true
+// Route for `mimirtool alertmanager get`
+// swagger:route GET /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusGetAlertmanagerConfig
+//
+// Get extra Alertmanager configuration from Grafana.
+// Returns a specific imported Alertmanager configuration by its identifier.
+//
+// Produces:
+// - application/yaml
+//
+// Responses:
+// 200: GettableAlertmanagerUserConfig
+// 403: ForbiddenError
+
+// Route for `mimirtool alertmanager delete`
+// swagger:route DELETE /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusDeleteAlertmanagerConfig
+//
+// Delete extra Alertmanager configuration from Grafana by its identifier.
+// The main Grafana Alertmanager configuration remains unaffected.
+//
+// Produces:
+// - application/json
+//
+// Responses:
+// 202: ConvertPrometheusResponse
+// 403: ForbiddenError
+
// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup
type RouteConvertPrometheusPostRuleGroupParams struct {
// in: path
@@ -286,11 +316,64 @@ type ConvertPrometheusResponse struct {
// swagger:parameters RouteConvertPrometheusPostAlertmanagerConfig
type RouteConvertPrometheusPostAlertmanagerConfigParams struct {
+ // Unique identifier for this Alertmanager configuration.
+ // This identifier is used to distinguish between different imported configurations.
+ // in: header
+ Identifier string `json:"x-grafana-alerting-config-identifier"`
+ // Comma-separated list of label matchers in 'key=value' format.
+ // These matchers determine which alerts this configuration should handle.
+ // For example: 'environment=production,team=backend' will only apply this
+ // configuration to alerts matching both environment=production AND team=backend.
+ // in: header
+ MergeMatchers string `json:"x-grafana-alerting-merge-matchers"`
+ // Alertmanager configuration including routing rules, receivers, and template files
// in:body
Body AlertmanagerUserConfig
}
+// swagger:parameters RouteConvertPrometheusGetAlertmanagerConfig
+type RouteConvertPrometheusGetAlertmanagerConfigParams struct {
+ // Unique identifier for the Alertmanager configuration to retrieve.
+ // in: header
+ Identifier string `json:"x-grafana-alerting-config-identifier"`
+}
+
+// swagger:parameters RouteConvertPrometheusDeleteAlertmanagerConfig
+type RouteConvertPrometheusDeleteAlertmanagerConfigParams struct {
+ // Unique identifier for the Alertmanager configuration to delete.
+ // in: header
+ Identifier string `json:"x-grafana-alerting-config-identifier"`
+}
+
// swagger:model
type AlertmanagerUserConfig struct {
- AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"`
+ AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"`
+ TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
+}
+
+func (c *AlertmanagerUserConfig) UnmarshalYAML(value *yaml.Node) error {
+ // mimirtool sends alertmanager_config as a string
+ type cortexAlertmanagerUserConfig struct {
+ TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
+ AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
+ }
+
+ var tmp cortexAlertmanagerUserConfig
+
+ if err := value.Decode(&tmp); err != nil {
+ return err
+ }
+
+ if err := yaml.Unmarshal([]byte(tmp.AlertmanagerConfig), &c.AlertmanagerConfig); err != nil {
+ return err
+ }
+ c.TemplateFiles = tmp.TemplateFiles
+
+ return nil
+}
+
+// swagger:model
+type GettableAlertmanagerUserConfig struct {
+ AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
+ TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
}
diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json
index 455cf5cb50c..62a11b3e3c1 100644
--- a/pkg/services/ngalert/api/tooling/post.json
+++ b/pkg/services/ngalert/api/tooling/post.json
@@ -580,6 +580,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
+ },
+ "template_files": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
}
},
"type": "object"
@@ -1408,6 +1414,20 @@
"title": "Frames is a slice of Frame pointers.",
"type": "array"
},
+ "GettableAlertmanagerUserConfig": {
+ "properties": {
+ "alertmanager_config": {
+ "type": "string"
+ },
+ "template_files": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
"GettableAlertmanagers": {
"properties": {
"data": {
@@ -2038,7 +2058,11 @@
"description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.",
"properties": {
"equal": {
- "$ref": "#/definitions/LabelNames"
+ "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
"source_match": {
"additionalProperties": {
@@ -2162,17 +2186,6 @@
"title": "Label is a key/value pair of strings.",
"type": "object"
},
- "LabelName": {
- "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
- "type": "string"
- },
- "LabelNames": {
- "items": {
- "$ref": "#/definitions/LabelName"
- },
- "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
- "type": "array"
- },
"LabelSet": {
"additionalProperties": {
"$ref": "#/definitions/LabelValue"
@@ -3674,6 +3687,7 @@
"type": "object"
},
"Route": {
+ "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
@@ -3715,6 +3729,12 @@
},
"type": "array"
},
+ "object_matchers": {
+ "$ref": "#/definitions/ObjectMatchers"
+ },
+ "provenance": {
+ "$ref": "#/definitions/Provenance"
+ },
"receiver": {
"type": "string"
},
@@ -3728,7 +3748,6 @@
"type": "array"
}
},
- "title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object"
},
"RouteExport": {
@@ -6823,10 +6842,91 @@
}
},
"/convert/api/v1/alerts": {
+ "delete": {
+ "description": "The main Grafana Alertmanager configuration remains unaffected.",
+ "operationId": "RouteConvertPrometheusDeleteAlertmanagerConfig",
+ "parameters": [
+ {
+ "description": "Unique identifier for the Alertmanager configuration to delete.",
+ "in": "header",
+ "name": "x-grafana-alerting-config-identifier",
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "202": {
+ "description": "ConvertPrometheusResponse",
+ "schema": {
+ "$ref": "#/definitions/ConvertPrometheusResponse"
+ }
+ },
+ "403": {
+ "description": "ForbiddenError",
+ "schema": {
+ "$ref": "#/definitions/ForbiddenError"
+ }
+ }
+ },
+ "summary": "Delete extra Alertmanager configuration from Grafana by its identifier.",
+ "tags": [
+ "convert_prometheus"
+ ]
+ },
+ "get": {
+ "description": "Returns a specific imported Alertmanager configuration by its identifier.",
+ "operationId": "RouteConvertPrometheusGetAlertmanagerConfig",
+ "parameters": [
+ {
+ "description": "Unique identifier for the Alertmanager configuration to retrieve.",
+ "in": "header",
+ "name": "x-grafana-alerting-config-identifier",
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/yaml"
+ ],
+ "responses": {
+ "200": {
+ "description": "GettableAlertmanagerUserConfig",
+ "schema": {
+ "$ref": "#/definitions/GettableAlertmanagerUserConfig"
+ }
+ },
+ "403": {
+ "description": "ForbiddenError",
+ "schema": {
+ "$ref": "#/definitions/ForbiddenError"
+ }
+ }
+ },
+ "summary": "Get extra Alertmanager configuration from Grafana.",
+ "tags": [
+ "convert_prometheus"
+ ]
+ },
"post": {
+ "description": "This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by\na unique identifier and can include merge matchers to select which alerts should be handled by\nthis specific configuration.",
"operationId": "RouteConvertPrometheusPostAlertmanagerConfig",
"parameters": [
{
+ "description": "Unique identifier for this Alertmanager configuration.\nThis identifier is used to distinguish between different imported configurations.",
+ "in": "header",
+ "name": "x-grafana-alerting-config-identifier",
+ "type": "string"
+ },
+ {
+ "description": "Comma-separated list of label matchers in 'key=value' format.\nThese matchers determine which alerts this configuration should handle.",
+ "example": "'environment=production,team=backend' will only apply this",
+ "in": "header",
+ "name": "x-grafana-alerting-merge-matchers",
+ "type": "string"
+ },
+ {
+ "description": "Alertmanager configuration including routing rules, receivers, and template files",
"in": "body",
"name": "Body",
"schema": {
@@ -6851,7 +6951,7 @@
}
}
},
- "summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.",
+ "summary": "Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.",
"tags": [
"convert_prometheus"
],
diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json
index f40362f55dd..addee647af0 100644
--- a/pkg/services/ngalert/api/tooling/spec.json
+++ b/pkg/services/ngalert/api/tooling/spec.json
@@ -1361,17 +1361,65 @@
}
},
"/convert/api/v1/alerts": {
+ "get": {
+ "description": "Returns a specific imported Alertmanager configuration by its identifier.",
+ "produces": [
+ "application/yaml"
+ ],
+ "tags": [
+ "convert_prometheus"
+ ],
+ "summary": "Get extra Alertmanager configuration from Grafana.",
+ "operationId": "RouteConvertPrometheusGetAlertmanagerConfig",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Unique identifier for the Alertmanager configuration to retrieve.",
+ "name": "x-grafana-alerting-config-identifier",
+ "in": "header"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "GettableAlertmanagerUserConfig",
+ "schema": {
+ "$ref": "#/definitions/GettableAlertmanagerUserConfig"
+ }
+ },
+ "403": {
+ "description": "ForbiddenError",
+ "schema": {
+ "$ref": "#/definitions/ForbiddenError"
+ }
+ }
+ }
+ },
"post": {
+ "description": "This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by\na unique identifier and can include merge matchers to select which alerts should be handled by\nthis specific configuration.",
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
- "summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.",
+ "summary": "Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.",
"operationId": "RouteConvertPrometheusPostAlertmanagerConfig",
"parameters": [
{
+ "type": "string",
+ "description": "Unique identifier for this Alertmanager configuration.\nThis identifier is used to distinguish between different imported configurations.",
+ "name": "x-grafana-alerting-config-identifier",
+ "in": "header"
+ },
+ {
+ "type": "string",
+ "example": "'environment=production,team=backend' will only apply this",
+ "description": "Comma-separated list of label matchers in 'key=value' format.\nThese matchers determine which alerts this configuration should handle.",
+ "name": "x-grafana-alerting-merge-matchers",
+ "in": "header"
+ },
+ {
+ "description": "Alertmanager configuration including routing rules, receivers, and template files",
"name": "Body",
"in": "body",
"schema": {
@@ -1394,6 +1442,39 @@
}
},
"x-raw-request": "true"
+ },
+ "delete": {
+ "description": "The main Grafana Alertmanager configuration remains unaffected.",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "convert_prometheus"
+ ],
+ "summary": "Delete extra Alertmanager configuration from Grafana by its identifier.",
+ "operationId": "RouteConvertPrometheusDeleteAlertmanagerConfig",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Unique identifier for the Alertmanager configuration to delete.",
+ "name": "x-grafana-alerting-config-identifier",
+ "in": "header"
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "ConvertPrometheusResponse",
+ "schema": {
+ "$ref": "#/definitions/ConvertPrometheusResponse"
+ }
+ },
+ "403": {
+ "description": "ForbiddenError",
+ "schema": {
+ "$ref": "#/definitions/ForbiddenError"
+ }
+ }
+ }
}
},
"/convert/prometheus/config/v1/rules": {
@@ -4788,6 +4869,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
+ },
+ "template_files": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
}
}
},
@@ -5616,6 +5703,20 @@
"$ref": "#/definitions/Frame"
}
},
+ "GettableAlertmanagerUserConfig": {
+ "type": "object",
+ "properties": {
+ "alertmanager_config": {
+ "type": "string"
+ },
+ "template_files": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ },
"GettableAlertmanagers": {
"type": "object",
"properties": {
@@ -6247,7 +6348,11 @@
"type": "object",
"properties": {
"equal": {
- "$ref": "#/definitions/LabelNames"
+ "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
},
"source_match": {
"description": "SourceMatch defines a set of labels that have to equal the given\nvalue for source alerts. Deprecated. Remove before v1.0 release.",
@@ -6370,17 +6475,6 @@
}
}
},
- "LabelName": {
- "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
- "type": "string"
- },
- "LabelNames": {
- "type": "array",
- "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
- "items": {
- "$ref": "#/definitions/LabelName"
- }
- },
"LabelSet": {
"description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.",
"type": "object",
@@ -7883,8 +7977,8 @@
}
},
"Route": {
+ "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object",
- "title": "A Route is a node that contains definitions of how to handle alerts.",
"properties": {
"active_time_intervals": {
"type": "array",
@@ -7926,6 +8020,12 @@
"type": "string"
}
},
+ "object_matchers": {
+ "$ref": "#/definitions/ObjectMatchers"
+ },
+ "provenance": {
+ "$ref": "#/definitions/Provenance"
+ },
"receiver": {
"type": "string"
},
diff --git a/public/api-merged.json b/public/api-merged.json
index 624c147aaa1..ae2060e66de 100644
--- a/public/api-merged.json
+++ b/public/api-merged.json
@@ -12999,6 +12999,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
+ },
+ "template_files": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
}
}
},
@@ -15855,6 +15861,20 @@
}
}
},
+ "GettableAlertmanagerUserConfig": {
+ "type": "object",
+ "properties": {
+ "alertmanager_config": {
+ "type": "string"
+ },
+ "template_files": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ },
"GettableAlertmanagers": {
"type": "object",
"properties": {
@@ -16709,7 +16729,11 @@
"type": "object",
"properties": {
"equal": {
- "$ref": "#/definitions/LabelNames"
+ "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
},
"source_match": {
"description": "SourceMatch defines a set of labels that have to equal the given\nvalue for source alerts. Deprecated. Remove before v1.0 release.",
@@ -16884,17 +16908,6 @@
}
}
},
- "LabelName": {
- "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
- "type": "string"
- },
- "LabelNames": {
- "type": "array",
- "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
- "items": {
- "$ref": "#/definitions/LabelName"
- }
- },
"LabelSet": {
"description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.",
"type": "object",
@@ -20027,8 +20040,8 @@
}
},
"Route": {
+ "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object",
- "title": "A Route is a node that contains definitions of how to handle alerts.",
"properties": {
"active_time_intervals": {
"type": "array",
@@ -20070,6 +20083,12 @@
"type": "string"
}
},
+ "object_matchers": {
+ "$ref": "#/definitions/ObjectMatchers"
+ },
+ "provenance": {
+ "$ref": "#/definitions/Provenance"
+ },
"receiver": {
"type": "string"
},
diff --git a/public/openapi3.json b/public/openapi3.json
index b1273b6913c..412cdd89dc7 100644
--- a/public/openapi3.json
+++ b/public/openapi3.json
@@ -3048,6 +3048,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/components/schemas/Config"
+ },
+ "template_files": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
}
},
"type": "object"
@@ -5905,6 +5911,20 @@
},
"type": "object"
},
+ "GettableAlertmanagerUserConfig": {
+ "properties": {
+ "alertmanager_config": {
+ "type": "string"
+ },
+ "template_files": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
"GettableAlertmanagers": {
"properties": {
"data": {
@@ -6758,7 +6778,11 @@
"description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.",
"properties": {
"equal": {
- "$ref": "#/components/schemas/LabelNames"
+ "description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
"source_match": {
"additionalProperties": {
@@ -6934,17 +6958,6 @@
"title": "Label is a key/value pair of strings.",
"type": "object"
},
- "LabelName": {
- "description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
- "type": "string"
- },
- "LabelNames": {
- "items": {
- "$ref": "#/components/schemas/LabelName"
- },
- "title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
- "type": "array"
- },
"LabelSet": {
"additionalProperties": {
"$ref": "#/components/schemas/LabelValue"
@@ -10077,6 +10090,7 @@
"type": "object"
},
"Route": {
+ "description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
@@ -10118,6 +10132,12 @@
},
"type": "array"
},
+ "object_matchers": {
+ "$ref": "#/components/schemas/ObjectMatchers"
+ },
+ "provenance": {
+ "$ref": "#/components/schemas/Provenance"
+ },
"receiver": {
"type": "string"
},
@@ -10131,7 +10151,6 @@
"type": "array"
}
},
- "title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object"
},
"RouteExport": {
From 66f79e53e514119f7c5c4009cec720a759f51259 Mon Sep 17 00:00:00 2001
From: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Date: Wed, 11 Jun 2025 12:45:14 +0100
Subject: [PATCH 16/32] Dashboards: Pass id prop to Switch component for bool
inputs (#106438)
---
public/app/core/components/OptionsUI/registry.tsx | 3 +--
public/app/plugins/panel/geomap/editor/layerEditor.tsx | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx
index d1c51c96de8..123ede77f89 100644
--- a/public/app/core/components/OptionsUI/registry.tsx
+++ b/public/app/core/components/OptionsUI/registry.tsx
@@ -87,8 +87,7 @@ export const getAllOptionEditors = () => {
name: 'Boolean',
description: 'Allows boolean values input',
editor(props) {
- const { id, ...rest } = props; // Remove id from properties passed into switch
- return props.onChange(e.currentTarget.checked)} />;
+ return props.onChange(e.currentTarget.checked)} />;
},
};
diff --git a/public/app/plugins/panel/geomap/editor/layerEditor.tsx b/public/app/plugins/panel/geomap/editor/layerEditor.tsx
index 064e6189cf7..865b9f94654 100644
--- a/public/app/plugins/panel/geomap/editor/layerEditor.tsx
+++ b/public/app/plugins/panel/geomap/editor/layerEditor.tsx
@@ -122,7 +122,7 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions