mirror of
https://github.com/grafana/grafana.git
synced 2026-01-15 05:35:41 +00:00
Compare commits
10 Commits
ash/fix-fl
...
gabor/no-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a89e155e7 | ||
|
|
5dbbe8164b | ||
|
|
d1064da4cd | ||
|
|
b57b8d4359 | ||
|
|
5219ccddb6 | ||
|
|
c95e3da2d5 | ||
|
|
43d9fbc056 | ||
|
|
7b80c44ac7 | ||
|
|
98f271f345 | ||
|
|
60c4fab063 |
7
Makefile
7
Makefile
@@ -135,7 +135,7 @@ i18n-extract-enterprise:
|
||||
@echo "Skipping i18n extract for Enterprise: not enabled"
|
||||
else
|
||||
i18n-extract-enterprise:
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
@echo "Extracting i18n strings for Enterprise"
|
||||
cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary
|
||||
endif
|
||||
|
||||
@@ -227,6 +227,10 @@ fix-cue:
|
||||
gen-jsonnet:
|
||||
go generate ./devenv/jsonnet
|
||||
|
||||
.PHONY: gen-themes
|
||||
gen-themes:
|
||||
go generate ./pkg/services/preference
|
||||
|
||||
.PHONY: update-workspace
|
||||
update-workspace: gen-go
|
||||
@echo "updating workspace"
|
||||
@@ -244,6 +248,7 @@ build-go-fast: ## Build all Go binaries without updating workspace.
|
||||
.PHONY: build-backend
|
||||
build-backend: ## Build Grafana backend.
|
||||
@echo "build backend"
|
||||
$(MAKE) gen-themes
|
||||
$(GO) run build.go $(GO_BUILD_FLAGS) build-backend
|
||||
|
||||
.PHONY: build-air
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
|
||||
github.com/grafana/grafana-app-sdk v0.48.7
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7
|
||||
|
||||
@@ -243,8 +243,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
|
||||
|
||||
@@ -97,7 +97,7 @@ require (
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
|
||||
@@ -215,8 +215,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
|
||||
@@ -66,17 +66,18 @@ Please refer to plugin documentation to see what RBAC permissions the plugin has
|
||||
|
||||
The following list contains app plugins that have fine-grained RBAC support.
|
||||
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| App plugin | App plugin ID | App plugin permission documentation |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Access policies](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) | `grafana-auth-app` | [RBAC actions for Access Policies](ref:cloud-access-policies-action-definitions) |
|
||||
| [Adaptive Metrics](https://grafana.com/docs/grafana-cloud/cost-management-and-billing/reduce-costs/metrics-costs/control-metrics-usage-via-adaptive-metrics/adaptive-metrics-plugin/) | `grafana-adaptive-metrics-app` | [RBAC actions for Adaptive Metrics](ref:adaptive-metrics-permissions) |
|
||||
| [Cloud Provider](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/) | `grafana-csp-app` | [Cloud Provider Observability role-based access control](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/rbac/) |
|
||||
| [Incident](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/incident/) | `grafana-incident-app` | n/a |
|
||||
| [Kubernetes Monitoring](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/) | `grafana-k8s-app` | [Kubernetes Monitoring role-based access control](/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/configuration/control-access/#precision-access-with-rbac-custom-plugin-roles) |
|
||||
| [OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/) | `grafana-oncall-app` | [Configure RBAC for OnCall](https://grafana.com/docs/grafana-cloud/alerting-and-irm/irm/oncall/manage/user-and-team-management/#manage-users-and-teams-for-grafana-oncall) |
|
||||
| [Performance Testing (K6)](https://grafana.com/docs/grafana-cloud/testing/k6/) | `k6-app` | [Configure RBAC for K6](https://grafana.com/docs/grafana-cloud/testing/k6/projects-and-users/configure-rbac/) |
|
||||
| [Private data source connect (PDC)](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/) | `grafana-pdc-app` | n/a |
|
||||
| [Service Level Objective (SLO)](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/) | `grafana-slo-app` | [Configure RBAC for SLO](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/rbac/) |
|
||||
| [Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/) | `grafana-synthetic-monitoring-app` | [Configure RBAC for Synthetic Monitoring](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/user-and-team-management/) |
|
||||
|
||||
### Revoke fine-grained access from app plugins
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setScopes } from '../utils/scope-helpers';
|
||||
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
import {
|
||||
getAdHocFilterOptionValues,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
} from './cuj-selectors';
|
||||
import { prepareAPIMocks } from './utils';
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
|
||||
|
||||
test.use({
|
||||
@@ -34,6 +36,11 @@ test.describe(
|
||||
const adHocFilterPills = getAdHocFilterPills(page);
|
||||
const scopesSelectorInput = getScopesSelectorInput(page);
|
||||
|
||||
// Set up routes before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, testScopes());
|
||||
}
|
||||
|
||||
await test.step('1.Apply filtering to a whole dashboard', async () => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
|
||||
@@ -66,6 +66,17 @@ export function getScopesDashboards(page: Page) {
|
||||
return page.locator('[data-testid^="scopes-dashboards-"][role="treeitem"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the first available dashboard in the scopes dashboard list.
|
||||
*/
|
||||
export async function clickFirstScopesDashboard(page: Page) {
|
||||
const dashboards = getScopesDashboards(page);
|
||||
// Wait for at least one dashboard to be visible
|
||||
await expect(dashboards.first()).toBeVisible({ timeout: 10000 });
|
||||
// Click - Playwright will automatically wait for the element to be actionable
|
||||
await dashboards.first().click();
|
||||
}
|
||||
|
||||
export function getScopesDashboardsSearchInput(page: Page) {
|
||||
return page.getByTestId('scopes-dashboards-search');
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setScopes } from '../utils/scope-helpers';
|
||||
import { setScopes, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
import {
|
||||
clickFirstScopesDashboard,
|
||||
getAdHocFilterPills,
|
||||
getGroupByInput,
|
||||
getGroupByValues,
|
||||
@@ -21,6 +23,7 @@ test.use({
|
||||
},
|
||||
});
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
|
||||
const DASHBOARD_UNDER_TEST_2 = 'cuj-dashboard-2';
|
||||
const NAVIGATE_TO = 'cuj-dashboard-3';
|
||||
@@ -38,6 +41,11 @@ test.describe(
|
||||
const adhocFilterPills = getAdHocFilterPills(page);
|
||||
const groupByValues = getGroupByValues(page);
|
||||
|
||||
// Set up routes before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, testScopes());
|
||||
}
|
||||
|
||||
await test.step('1.Search dashboard', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
@@ -74,7 +82,7 @@ test.describe(
|
||||
|
||||
await expect(markdownContent).toContainText(`now-12h`);
|
||||
|
||||
await scopesDashboards.first().click();
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
|
||||
await expect(markdownContent).toBeVisible();
|
||||
@@ -117,10 +125,10 @@ test.describe(
|
||||
await groupByVariable.press('Enter');
|
||||
await groupByVariable.press('Escape');
|
||||
|
||||
await expect(scopesDashboards.first()).toBeVisible();
|
||||
|
||||
const { getRequests, waitForExpectedRequests } = await trackDashboardReloadRequests(page);
|
||||
await scopesDashboards.first().click();
|
||||
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
await waitForExpectedRequests();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
@@ -158,8 +166,7 @@ test.describe(
|
||||
const oldFilters = `GroupByVar: ${selectedValues}\n\nAdHocVar: ${processedPills}`;
|
||||
await expect(markdownContent).toContainText(oldFilters);
|
||||
|
||||
await expect(scopesDashboards.first()).toBeVisible();
|
||||
await scopesDashboards.first().click();
|
||||
await clickFirstScopesDashboard(page);
|
||||
await page.waitForURL('**/d/**');
|
||||
|
||||
const newPillCount = await adhocFilterPills.count();
|
||||
|
||||
@@ -165,9 +165,8 @@ test.describe(
|
||||
|
||||
await refreshBtn.click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
expect(await panelContent.textContent()).not.toBe(panelContents);
|
||||
// Wait for the panel content to change (not just for network to complete)
|
||||
await expect(panelContent).not.toHaveText(panelContents!, { timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('6.Turn off refresh', async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
openScopesSelector,
|
||||
searchScopes,
|
||||
selectScope,
|
||||
setupScopeRoutes,
|
||||
} from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
@@ -36,32 +37,37 @@ test.describe(
|
||||
const scopesSelector = getScopesSelectorInput(page);
|
||||
const recentScopesSelector = getRecentScopesSelector(page);
|
||||
const scopeTreeCheckboxes = getScopeTreeCheckboxes(page);
|
||||
const scopes = testScopes();
|
||||
|
||||
// Set up routes once before any navigation (only for mocked mode)
|
||||
if (!USE_LIVE_DATA) {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
}
|
||||
|
||||
await test.step('1.View and select any scope', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
|
||||
const selectedScopes = [secondLevelScopes[0]];
|
||||
|
||||
scopeName = await getScopeLeafName(page, 0);
|
||||
let scopeTitle = await getScopeLeafTitle(page, 0);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitle);
|
||||
});
|
||||
@@ -70,28 +76,27 @@ test.describe(
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const scopeTitles: string[] = [];
|
||||
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]]; //used only in mocked scopes version
|
||||
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]];
|
||||
for (let i = 0; i < selectedScopes.length; i++) {
|
||||
scopeName = await getScopeLeafName(page, i);
|
||||
scopeTitles.push(await getScopeLeafTitle(page, i));
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]); //used only in mocked scopes version
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]);
|
||||
}
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes);
|
||||
|
||||
await expect.soft(scopesSelector).toHaveAttribute('data-value', scopeTitles.join(' + '));
|
||||
});
|
||||
@@ -102,8 +107,7 @@ test.describe(
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
await recentScopesSelector.click();
|
||||
|
||||
@@ -121,26 +125,25 @@ test.describe(
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', '');
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const firstLevelScopes = scopes[2].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[2].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
|
||||
const selectedScopes = [secondLevelScopes[0]];
|
||||
|
||||
scopeName = await getScopeLeafName(page, 0);
|
||||
let scopeTitle = await getScopeLeafTitle(page, 0);
|
||||
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
|
||||
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : []); //used only in mocked scopes version
|
||||
await applyScopes(page, USE_LIVE_DATA ? undefined : []);
|
||||
|
||||
expect.soft(scopesSelector).toHaveAttribute('data-value', new RegExp(`^${scopeTitle}`));
|
||||
});
|
||||
@@ -148,17 +151,16 @@ test.describe(
|
||||
await test.step('5.View pre-completed production entity values as I type', async () => {
|
||||
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
|
||||
|
||||
const scopes = testScopes();
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
|
||||
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
|
||||
|
||||
let scopeName = await getScopeTreeName(page, 0);
|
||||
|
||||
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
|
||||
const firstLevelScopes = scopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
|
||||
|
||||
scopeName = await getScopeTreeName(page, 1);
|
||||
|
||||
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
|
||||
const secondLevelScopes = firstLevelScopes[0].children!;
|
||||
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
|
||||
|
||||
const scopeSearchOne = await getScopeLeafTitle(page, 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { applyScopes, openScopesSelector, selectScope } from '../utils/scope-helpers';
|
||||
import { applyScopes, openScopesSelector, selectScope, setupScopeRoutes } from '../utils/scope-helpers';
|
||||
import { testScopesWithRedirect } from '../utils/scopes';
|
||||
|
||||
test.use({
|
||||
@@ -16,8 +16,13 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should redirect to custom URL when scope has redirectUrl', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and open scopes selector', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
// Set up routes BEFORE navigation to ensure all requests are mocked
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Open scopes selector', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
});
|
||||
|
||||
@@ -40,8 +45,12 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should prioritize redirectUrl over scope navigation fallback', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and open scopes selector', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Open scopes selector', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
});
|
||||
|
||||
@@ -68,8 +77,12 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
}) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and select scope', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Select and apply scope', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
|
||||
await applyScopes(page, [scopes[1]]);
|
||||
@@ -112,8 +125,12 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
}) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Navigate to dashboard and select scope', async () => {
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Select and apply scope', async () => {
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-fallback', scopes[1]);
|
||||
await applyScopes(page, [scopes[1]]);
|
||||
@@ -151,9 +168,13 @@ test.describe('Scope Redirect Functionality', () => {
|
||||
test('should not redirect to redirectPath when on active scope navigation', async ({ page, gotoDashboardPage }) => {
|
||||
const scopes = testScopesWithRedirect();
|
||||
|
||||
await test.step('Set up routes and navigate to dashboard', async () => {
|
||||
await setupScopeRoutes(page, scopes);
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
});
|
||||
|
||||
await test.step('Set up scope navigation to dashboard-1', async () => {
|
||||
// First, apply a scope that creates scope navigation to dashboard-1 (without redirectPath)
|
||||
await gotoDashboardPage({ uid: 'cuj-dashboard-1' });
|
||||
await openScopesSelector(page, scopes);
|
||||
await selectScope(page, 'sn-redirect-setup', scopes[2]);
|
||||
await applyScopes(page, [scopes[2]]);
|
||||
|
||||
@@ -6,7 +6,150 @@ import { Resource } from '../../public/app/features/apiserver/types';
|
||||
|
||||
import { testScopes } from './scopes';
|
||||
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CALLS_CONFIG_PATH);
|
||||
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
|
||||
|
||||
/**
|
||||
* Sets up all scope-related API routes before navigation.
|
||||
* This ensures that ALL scope API requests (including those made during initial page load)
|
||||
* are intercepted by the mocks, preventing RTK Query from caching real API responses.
|
||||
*
|
||||
* Call this BEFORE navigating to a page (e.g., before gotoDashboardPage).
|
||||
*/
|
||||
export async function setupScopeRoutes(page: Page, scopes: TestScope[]): Promise<void> {
|
||||
// Route for scope node children (tree structure)
|
||||
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_node_children*`, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const parentParam = url.searchParams.get('parent');
|
||||
const queryParam = url.searchParams.get('query');
|
||||
|
||||
// Find the appropriate scopes based on parent
|
||||
let scopesToReturn = scopes;
|
||||
if (parentParam) {
|
||||
// Find nested scopes based on parent name
|
||||
const findChildren = (items: TestScope[]): TestScope[] => {
|
||||
for (const item of items) {
|
||||
if (item.name === parentParam && item.children) {
|
||||
return item.children;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findChildren(item.children);
|
||||
if (found.length > 0) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
scopesToReturn = findChildren(scopes);
|
||||
if (scopesToReturn.length === 0) {
|
||||
scopesToReturn = scopes; // Fallback to root scopes
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search query if provided
|
||||
if (queryParam) {
|
||||
const query = queryParam.toLowerCase();
|
||||
const filterByQuery = (items: TestScope[]): TestScope[] => {
|
||||
const results: TestScope[] = [];
|
||||
for (const item of items) {
|
||||
// Exact match on name or title containing the query
|
||||
if (item.name.toLowerCase() === query || item.title.toLowerCase() === query) {
|
||||
results.push(item);
|
||||
} else if (item.name.toLowerCase().includes(query) || item.title.toLowerCase().includes(query)) {
|
||||
results.push(item);
|
||||
}
|
||||
// Also search in children
|
||||
if (item.children) {
|
||||
results.push(...filterByQuery(item.children));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
scopesToReturn = filterByQuery(scopesToReturn);
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
kind: 'FindScopeNodeChildrenResults',
|
||||
metadata: {},
|
||||
items: scopesToReturn.map((scope) => ({
|
||||
kind: 'ScopeNode',
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
metadata: {
|
||||
name: scope.name,
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
title: scope.title,
|
||||
description: scope.title,
|
||||
disableMultiSelect: scope.disableMultiSelect ?? false,
|
||||
nodeType: scope.children ? 'container' : 'leaf',
|
||||
...(parentParam && { parentName: parentParam }),
|
||||
...((scope.addLinks || scope.children) && {
|
||||
linkType: 'scope',
|
||||
linkId: `scope-${scope.name}`,
|
||||
}),
|
||||
...(scope.redirectPath && { redirectPath: scope.redirectPath }),
|
||||
},
|
||||
})),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Route for individual scope fetching
|
||||
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/*`, async (route) => {
|
||||
const url = route.request().url();
|
||||
const scopeName = url.split('/scopes/')[1]?.split('?')[0];
|
||||
|
||||
// Find the scope in the test data
|
||||
const findScope = (items: TestScope[]): TestScope | undefined => {
|
||||
for (const item of items) {
|
||||
if (`scope-${item.name}` === scopeName) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findScope(item.children);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const scope = findScope(scopes);
|
||||
|
||||
if (scope) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
kind: 'Scope',
|
||||
apiVersion: 'scope.grafana.app/v0alpha1',
|
||||
metadata: {
|
||||
name: `scope-${scope.name}`,
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
title: scope.title,
|
||||
description: '',
|
||||
filters: scope.filters,
|
||||
category: scope.category,
|
||||
type: scope.type,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({ status: 404 });
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Dashboard bindings and navigations routes are set up dynamically in applyScopes()
|
||||
// with scope-specific URL patterns to avoid cache issues. They are not set up here.
|
||||
}
|
||||
|
||||
export type TestScope = {
|
||||
name: string;
|
||||
@@ -24,6 +167,9 @@ export type TestScope = {
|
||||
|
||||
type ScopeDashboardBinding = Resource<ScopeDashboardBindingSpec, ScopeDashboardBindingStatus, 'ScopeDashboardBinding'>;
|
||||
|
||||
/**
|
||||
* Sets up a route for scope node children requests and waits for the response.
|
||||
*/
|
||||
export async function scopeNodeChildrenRequest(
|
||||
page: Page,
|
||||
scopes: TestScope[],
|
||||
@@ -68,10 +214,13 @@ export async function scopeNodeChildrenRequest(
|
||||
return page.waitForResponse((response) => response.url().includes(`/find/scope_node_children`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the scopes selector dropdown and waits for the tree to load.
|
||||
*/
|
||||
export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId('scopes-selector-input').click();
|
||||
|
||||
if (!scopes) {
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -82,10 +231,13 @@ export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a scope tree node and waits for children to load.
|
||||
*/
|
||||
export async function expandScopesSelection(page: Page, parentScope: string, scopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId(`scopes-tree-${parentScope}-expand`).click();
|
||||
|
||||
if (!scopes) {
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -96,6 +248,9 @@ export async function expandScopesSelection(page: Page, parentScope: string, sco
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a route for individual scope requests and waits for the response.
|
||||
*/
|
||||
export async function scopeSelectRequest(page: Page, selectedScope: TestScope): Promise<Response> {
|
||||
await page.route(
|
||||
`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/scope-${selectedScope.name}`,
|
||||
@@ -125,6 +280,9 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope):
|
||||
return page.waitForResponse((response) => response.url().includes(`/scopes/scope-${selectedScope.name}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a scope in the tree.
|
||||
*/
|
||||
export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) {
|
||||
const click = async () => {
|
||||
const element = page.locator(
|
||||
@@ -134,7 +292,7 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
|
||||
await element.click({ force: true });
|
||||
};
|
||||
|
||||
if (!selectedScope) {
|
||||
if (!selectedScope || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
@@ -145,14 +303,22 @@ export async function selectScope(page: Page, scopeName: string, selectedScope?:
|
||||
await responsePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected scopes and waits for the selector to close and page to settle.
|
||||
* Sets up routes dynamically with scope-specific URL patterns to avoid cache issues.
|
||||
*/
|
||||
export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
const click = async () => {
|
||||
await page.getByTestId('scopes-selector-apply').scrollIntoViewIfNeeded();
|
||||
await page.getByTestId('scopes-selector-apply').click({ force: true });
|
||||
};
|
||||
|
||||
if (!scopes) {
|
||||
if (!scopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
// Wait for the apply button to disappear (selector closed)
|
||||
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
|
||||
// Wait for any resulting API calls (dashboard bindings, etc.) to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,7 +332,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
|
||||
const groups: string[] = ['Most relevant', 'Dashboards', 'Something else', ''];
|
||||
|
||||
// Mock scope_dashboard_bindings endpoint
|
||||
// Mock scope_dashboard_bindings endpoint with scope-specific URL pattern
|
||||
await page.route(dashboardBindingsUrl, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -220,7 +386,7 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
});
|
||||
});
|
||||
|
||||
// Mock scope_navigations endpoint
|
||||
// Mock scope_navigations endpoint with scope-specific URL pattern
|
||||
await page.route(scopeNavigationsUrl, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -266,21 +432,23 @@ export async function applyScopes(page: Page, scopes?: TestScope[]) {
|
||||
(response) =>
|
||||
response.url().includes(`/find/scope_dashboard_bindings`) || response.url().includes(`/find/scope_navigations`)
|
||||
);
|
||||
const scopeRequestPromises: Array<Promise<Response>> = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
scopeRequestPromises.push(scopeSelectRequest(page, scope));
|
||||
}
|
||||
|
||||
await click();
|
||||
await responsePromise;
|
||||
await Promise.all(scopeRequestPromises);
|
||||
// Wait for the apply button to disappear (selector closed)
|
||||
await page.waitForSelector('[data-testid="scopes-selector-apply"]', { state: 'hidden', timeout: 5000 });
|
||||
// Wait for any resulting API calls to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
export async function searchScopes(page: Page, value: string, resultScopes: TestScope[]) {
|
||||
/**
|
||||
* Searches for scopes in the tree and waits for results.
|
||||
* Sets up a route dynamically with filtered results to return only matching scopes.
|
||||
*/
|
||||
export async function searchScopes(page: Page, value: string, resultScopes?: TestScope[]) {
|
||||
const click = async () => await page.getByTestId('scopes-tree-search').fill(value);
|
||||
|
||||
if (!resultScopes) {
|
||||
if (!resultScopes || USE_LIVE_DATA) {
|
||||
await click();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = false
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[feature_toggles]
|
||||
unifiedStorageSearchUI = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
|
||||
23
go.mod
23
go.mod
@@ -32,14 +32,14 @@ require (
|
||||
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect; @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect; @grafana/grafana-operator-experience-squad
|
||||
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||
@@ -82,14 +82,14 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group
|
||||
github.com/golang/snappy v1.0.0 // @grafana/alerting-backend
|
||||
github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group
|
||||
github.com/google/go-github/v70 v70.0.0 // indirect; @grafana/grafana-git-ui-sync-team
|
||||
github.com/google/go-github/v70 v70.0.0 // @grafana/grafana-git-ui-sync-team
|
||||
github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent
|
||||
github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group
|
||||
github.com/google/wire v0.7.0 // @grafana/grafana-backend-group
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
@@ -113,6 +113,7 @@ require (
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // @grafana/grafana-backend-group
|
||||
@@ -260,12 +261,13 @@ require (
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0 // @grafana/plugins-platform-backend
|
||||
|
||||
// This needs to be here for other projects that import grafana/grafana
|
||||
// For local development grafana/grafana will always use the local files
|
||||
// Check go.work file for details
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0 // @grafana/grafana-app-platform-squad
|
||||
)
|
||||
|
||||
// Replace the workspace versions
|
||||
@@ -294,6 +296,8 @@ replace (
|
||||
github.com/grafana/grafana/pkg/aggregator => ./pkg/aggregator
|
||||
github.com/grafana/grafana/pkg/apimachinery => ./pkg/apimachinery
|
||||
github.com/grafana/grafana/pkg/apiserver => ./pkg/apiserver
|
||||
github.com/grafana/grafana/pkg/plugins => ./pkg/plugins
|
||||
github.com/grafana/grafana/pkg/semconv => ./pkg/semconv
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -652,11 +656,12 @@ require (
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 // indirect
|
||||
github.com/Machiel/slugify v1.0.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
@@ -676,6 +681,8 @@ require (
|
||||
github.com/google/gnostic v0.7.1 // indirect
|
||||
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 // indirect
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
@@ -697,7 +704,7 @@ require (
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
|
||||
|
||||
// Use our fork of the upstream Alertmanager.
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
|
||||
|
||||
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
|
||||
|
||||
23
go.sum
23
go.sum
@@ -680,6 +680,7 @@ github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7Og
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
@@ -737,6 +738,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U=
|
||||
github.com/IBM/pgxpoolprometheus v1.1.2/go.mod h1:+vWzISN6S9ssgurhUNmm6AlXL9XLah3TdWJktquKTR8=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
|
||||
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
@@ -759,6 +762,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
|
||||
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@@ -1026,6 +1031,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -1620,8 +1627,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
|
||||
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
@@ -1664,8 +1671,6 @@ github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDP
|
||||
github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 h1:NznuPwItog+rwdVg8hAuGKP29ndRSzJAwhxKldkP8oQ=
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32/go.mod h1:796sq+UcONnSlzA3RtlBZ+b/hrerkZXiEmO8oMjyRwY=
|
||||
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
|
||||
@@ -1676,8 +1681,8 @@ github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ
|
||||
github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20251118081820-ace37f973a0f h1:fTlIj5n4x5dU63XHItug7GLjtnaeJdPqBlqg4zlABq0=
|
||||
@@ -1753,6 +1758,8 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
@@ -1877,6 +1884,10 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
|
||||
2
go.work
2
go.work
@@ -38,6 +38,6 @@ use (
|
||||
./pkg/semconv
|
||||
)
|
||||
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f
|
||||
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
|
||||
|
||||
@@ -280,7 +280,6 @@ github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw
|
||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo=
|
||||
@@ -906,6 +905,8 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
|
||||
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
|
||||
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3 h1:KVncUdAc5YwY/OQmw6HgzJmbRKn6IwrhvtcBAd1yDHo=
|
||||
github.com/grafana/alerting v0.0.0-20260112110054-6c6f13659ad3/go.mod h1:Oy4MthJqfErlieO14ryZXdukDrUACy8Lg56P3zP7S1k=
|
||||
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
|
||||
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
@@ -996,6 +997,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975/go.mod h1:FGdGvhI40Dq+CTQaSzK9evuve774cgOUdGfVO04OXkw=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36 h1:AjZ58JRw1ZieFH/SdsddF5BXtsDKt5kSrKNPWrzYz3Y=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f h1:9tRhudagkQO2s61SLFLSziIdCm7XlkfypVKDxpcHokg=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20260112162805-d29cc9cf7f0f/go.mod h1:AsVdCBeDFN9QbgpJg+8voDAcgsW0RmNvBd70ecMMdC0=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
@@ -1911,6 +1914,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
|
||||
|
||||
@@ -62,8 +62,7 @@
|
||||
"stats": "webpack --mode production --config scripts/webpack/webpack.prod.js --profile --json > compilation-stats.json",
|
||||
"storybook": "yarn workspace @grafana/ui storybook --ci",
|
||||
"storybook:build": "yarn workspace @grafana/ui storybook:build",
|
||||
"themes-schema": "typescript-json-schema ./tsconfig.json NewThemeOptions --include 'packages/grafana-data/src/themes/createTheme.ts' --out public/app/features/theme-playground/schema.generated.json",
|
||||
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes-generate": "yarn workspace @grafana/data themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --conditions=@grafana-app/source --platform=node --tsconfig=./scripts/cli/tsconfig.json | node",
|
||||
"themes:usage": "eslint . --ignore-pattern '*.test.ts*' --ignore-pattern '*.spec.ts*' --cache --plugin '@grafana' --rule '{ @grafana/theme-token-usage: \"error\" }'",
|
||||
"typecheck": "tsc --noEmit && yarn run packages:typecheck",
|
||||
"plugins:build-bundled": "echo 'bundled plugins are no longer supported'",
|
||||
@@ -254,7 +253,6 @@
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-json-schema": "^0.65.1",
|
||||
"webpack": "5.101.0",
|
||||
"webpack-assets-manifest": "^5.1.0",
|
||||
"webpack-cli": "6.0.1",
|
||||
@@ -265,7 +263,7 @@
|
||||
"webpackbar": "^7.0.0",
|
||||
"yaml": "^2.0.0",
|
||||
"yargs": "^18.0.0",
|
||||
"zod": "^4.0.0"
|
||||
"zod": "^4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bsull/augurs": "^0.10.0",
|
||||
|
||||
@@ -34,6 +34,8 @@ export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryF
|
||||
getBackendSrv().fetch({
|
||||
...requestOptions,
|
||||
url: baseURL + requestOptions.url,
|
||||
// Default to GET so backend_srv correctly skips success alerts for queries
|
||||
method: requestOptions.method ?? 'GET',
|
||||
showErrorAlert: requestOptions.showErrorAlert ?? false,
|
||||
data: requestOptions.body,
|
||||
headers,
|
||||
|
||||
@@ -47,11 +47,12 @@
|
||||
"LICENSE_APACHE2"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"build": "yarn themes-schema && tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
|
||||
"clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"themes-schema": "tsx ./src/themes/scripts/generateSchema.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
@@ -81,10 +82,12 @@
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.8.1",
|
||||
"uplot": "1.6.32",
|
||||
"xss": "^1.0.14"
|
||||
"xss": "^1.0.14",
|
||||
"zod": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/scenes": "6.38.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@types/history": "4.7.11",
|
||||
@@ -101,6 +104,7 @@
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json from '@rollup/plugin-json';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { entryPoint, plugins, esmOutput, cjsOutput } from '../rollup.config.parts';
|
||||
@@ -8,13 +9,13 @@ const pkg = rq('./package.json');
|
||||
export default [
|
||||
{
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
plugins: [...plugins, json()],
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins,
|
||||
plugins: [...plugins, json()],
|
||||
output: [cjsOutput(pkg, 'grafana-data'), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
|
||||
@@ -106,3 +106,4 @@ export { findNumericFieldMinMax } from '../field/fieldOverrides';
|
||||
export { type PanelOptionsSupplier } from '../panel/PanelPlugin';
|
||||
export { sanitize, sanitizeUrl } from '../text/sanitize';
|
||||
export { type NestedValueAccess, type NestedPanelOptions, isNestedPanelOptions } from '../utils/OptionsUIBuilders';
|
||||
export { NewThemeOptionsSchema } from '../themes/createTheme';
|
||||
|
||||
@@ -1,83 +1,103 @@
|
||||
import { merge } from 'lodash';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { alpha, darken, emphasize, getContrastRatio, lighten } from './colorManipulator';
|
||||
import { palette } from './palette';
|
||||
import { DeepPartial, ThemeRichColor } from './types';
|
||||
import { DeepRequired, ThemeRichColor, ThemeRichColorInputSchema } from './types';
|
||||
|
||||
const ThemeColorsModeSchema = z.enum(['light', 'dark']);
|
||||
/** @internal */
|
||||
export type ThemeColorsMode = 'light' | 'dark';
|
||||
export type ThemeColorsMode = z.infer<typeof ThemeColorsModeSchema>;
|
||||
|
||||
const createThemeColorsBaseSchema = <TColor>(color: TColor) =>
|
||||
z
|
||||
.object({
|
||||
mode: ThemeColorsModeSchema,
|
||||
|
||||
primary: color,
|
||||
secondary: color,
|
||||
info: color,
|
||||
error: color,
|
||||
success: color,
|
||||
warning: color,
|
||||
|
||||
text: z.object({
|
||||
primary: z.string().optional(),
|
||||
secondary: z.string().optional(),
|
||||
disabled: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
/** Used for auto white or dark text on colored backgrounds */
|
||||
maxContrast: z.string().optional(),
|
||||
}),
|
||||
|
||||
background: z.object({
|
||||
/** Dashboard and body background */
|
||||
canvas: z.string().optional(),
|
||||
/** Primary content pane background (panels etc) */
|
||||
primary: z.string().optional(),
|
||||
/** Cards and elements that need to stand out on the primary background */
|
||||
secondary: z.string().optional(),
|
||||
/**
|
||||
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
|
||||
* themes it has a brighter shade to help give it contrast against the primary background.
|
||||
**/
|
||||
elevated: z.string().optional(),
|
||||
}),
|
||||
|
||||
border: z.object({
|
||||
weak: z.string().optional(),
|
||||
medium: z.string().optional(),
|
||||
strong: z.string().optional(),
|
||||
}),
|
||||
|
||||
gradients: z.object({
|
||||
brandVertical: z.string().optional(),
|
||||
brandHorizontal: z.string().optional(),
|
||||
}),
|
||||
|
||||
action: z.object({
|
||||
/** Used for selected menu item / select option */
|
||||
selected: z.string().optional(),
|
||||
/**
|
||||
* @alpha (Do not use from plugins)
|
||||
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
|
||||
**/
|
||||
selectedBorder: z.string().optional(),
|
||||
/** Used for hovered menu item / select option */
|
||||
hover: z.string().optional(),
|
||||
/** Used for button/colored background hover opacity */
|
||||
hoverOpacity: z.number().optional(),
|
||||
/** Used focused menu item / select option */
|
||||
focus: z.string().optional(),
|
||||
/** Used for disabled buttons and inputs */
|
||||
disabledBackground: z.string().optional(),
|
||||
/** Disabled text */
|
||||
disabledText: z.string().optional(),
|
||||
/** Disablerd opacity */
|
||||
disabledOpacity: z.number().optional(),
|
||||
}),
|
||||
|
||||
hoverFactor: z.number(),
|
||||
contrastThreshold: z.number(),
|
||||
tonalOffset: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
// Need to override the zod type to include the generic properly
|
||||
/** @internal */
|
||||
export interface ThemeColorsBase<TColor> {
|
||||
mode: ThemeColorsMode;
|
||||
|
||||
export type ThemeColorsBase<TColor> = DeepRequired<
|
||||
Omit<
|
||||
z.infer<ReturnType<typeof createThemeColorsBaseSchema>>,
|
||||
'primary' | 'secondary' | 'info' | 'error' | 'success' | 'warning'
|
||||
>
|
||||
> & {
|
||||
primary: TColor;
|
||||
secondary: TColor;
|
||||
info: TColor;
|
||||
error: TColor;
|
||||
success: TColor;
|
||||
warning: TColor;
|
||||
|
||||
text: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
disabled: string;
|
||||
link: string;
|
||||
/** Used for auto white or dark text on colored backgrounds */
|
||||
maxContrast: string;
|
||||
};
|
||||
|
||||
background: {
|
||||
/** Dashboard and body background */
|
||||
canvas: string;
|
||||
/** Primary content pane background (panels etc) */
|
||||
primary: string;
|
||||
/** Cards and elements that need to stand out on the primary background */
|
||||
secondary: string;
|
||||
/**
|
||||
* For popovers and menu backgrounds. This is the same color as primary in most light themes but in dark
|
||||
* themes it has a brighter shade to help give it contrast against the primary background.
|
||||
**/
|
||||
elevated: string;
|
||||
};
|
||||
|
||||
border: {
|
||||
weak: string;
|
||||
medium: string;
|
||||
strong: string;
|
||||
};
|
||||
|
||||
gradients: {
|
||||
brandVertical: string;
|
||||
brandHorizontal: string;
|
||||
};
|
||||
|
||||
action: {
|
||||
/** Used for selected menu item / select option */
|
||||
selected: string;
|
||||
/**
|
||||
* @alpha (Do not use from plugins)
|
||||
* Used for selected items when background only change is not enough (Currently only used for FilterPill)
|
||||
**/
|
||||
selectedBorder: string;
|
||||
/** Used for hovered menu item / select option */
|
||||
hover: string;
|
||||
/** Used for button/colored background hover opacity */
|
||||
hoverOpacity: number;
|
||||
/** Used focused menu item / select option */
|
||||
focus: string;
|
||||
/** Used for disabled buttons and inputs */
|
||||
disabledBackground: string;
|
||||
/** Disabled text */
|
||||
disabledText: string;
|
||||
/** Disablerd opacity */
|
||||
disabledOpacity: number;
|
||||
};
|
||||
|
||||
hoverFactor: number;
|
||||
contrastThreshold: number;
|
||||
tonalOffset: number;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ThemeHoverStrengh {}
|
||||
|
||||
@@ -89,8 +109,10 @@ export interface ThemeColors extends ThemeColorsBase<ThemeRichColor> {
|
||||
emphasize(color: string, amount?: number): string;
|
||||
}
|
||||
|
||||
export const ThemeColorsInputSchema = createThemeColorsBaseSchema(ThemeRichColorInputSchema);
|
||||
|
||||
/** @internal */
|
||||
export type ThemeColorsInput = DeepPartial<ThemeColorsBase<ThemeRichColor>>;
|
||||
export type ThemeColorsInput = z.infer<typeof ThemeColorsInputSchema>;
|
||||
|
||||
class DarkColors implements ThemeColorsBase<Partial<ThemeRichColor>> {
|
||||
mode: ThemeColorsMode = 'dark';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @beta */
|
||||
export interface ThemeShape {
|
||||
/**
|
||||
@@ -34,9 +36,12 @@ export interface Radii {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ThemeShapeInput {
|
||||
borderRadius?: number;
|
||||
}
|
||||
export const ThemeShapeInputSchema = z.object({
|
||||
borderRadius: z.int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export type ThemeShapeInput = z.infer<typeof ThemeShapeInputSchema>;
|
||||
|
||||
export function createShape(options: ThemeShapeInput): ThemeShape {
|
||||
const baseBorderRadius = options.borderRadius ?? 6;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// Code based on Material UI
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2014 Call-Em-All
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingOptions = {
|
||||
gridSize?: number;
|
||||
};
|
||||
export const ThemeSpacingOptionsSchema = z.object({
|
||||
gridSize: z.int().positive().optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingOptions = z.infer<typeof ThemeSpacingOptionsSchema>;
|
||||
|
||||
/** @internal */
|
||||
export type ThemeSpacingArgument = number | string;
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { createBreakpoints } from './breakpoints';
|
||||
import { createColors, ThemeColorsInput } from './createColors';
|
||||
import { createColors, ThemeColorsInputSchema } from './createColors';
|
||||
import { createComponents } from './createComponents';
|
||||
import { createShadows } from './createShadows';
|
||||
import { createShape, ThemeShapeInput } from './createShape';
|
||||
import { createSpacing, ThemeSpacingOptions } from './createSpacing';
|
||||
import { createShape, ThemeShapeInputSchema } from './createShape';
|
||||
import { createSpacing, ThemeSpacingOptionsSchema } from './createSpacing';
|
||||
import { createTransitions } from './createTransitions';
|
||||
import { createTypography, ThemeTypographyInput } from './createTypography';
|
||||
import { createTypography, ThemeTypographyInputSchema } from './createTypography';
|
||||
import { createV1Theme } from './createV1Theme';
|
||||
import { createVisualizationColors, ThemeVisualizationColorsInput } from './createVisualizationColors';
|
||||
import { createVisualizationColors, ThemeVisualizationColorsInputSchema } from './createVisualizationColors';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
import { zIndex } from './zIndex';
|
||||
|
||||
/** @internal */
|
||||
export interface NewThemeOptions {
|
||||
name?: string;
|
||||
colors?: ThemeColorsInput;
|
||||
spacing?: ThemeSpacingOptions;
|
||||
shape?: ThemeShapeInput;
|
||||
typography?: ThemeTypographyInput;
|
||||
visualization?: ThemeVisualizationColorsInput;
|
||||
}
|
||||
export const NewThemeOptionsSchema = z.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
colors: ThemeColorsInputSchema.optional(),
|
||||
spacing: ThemeSpacingOptionsSchema.optional(),
|
||||
shape: ThemeShapeInputSchema.optional(),
|
||||
typography: ThemeTypographyInputSchema.optional(),
|
||||
visualization: ThemeVisualizationColorsInputSchema.optional(),
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 {
|
||||
export type NewThemeOptions = z.infer<typeof NewThemeOptionsSchema>;
|
||||
|
||||
/** @internal */
|
||||
export function createTheme(
|
||||
options: Omit<NewThemeOptions, 'id' | 'name'> & {
|
||||
name?: NewThemeOptions['name'];
|
||||
} = {}
|
||||
): GrafanaTheme2 {
|
||||
const {
|
||||
name,
|
||||
colors: colorsInput = {},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Code based on Material UI
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2014 Call-Em-All
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ThemeColors } from './createColors';
|
||||
|
||||
@@ -40,18 +41,20 @@ export interface ThemeTypographyVariant {
|
||||
letterSpacing?: string;
|
||||
}
|
||||
|
||||
export interface ThemeTypographyInput {
|
||||
fontFamily?: string;
|
||||
fontFamilyMonospace?: string;
|
||||
fontSize?: number;
|
||||
fontWeightLight?: number;
|
||||
fontWeightRegular?: number;
|
||||
fontWeightMedium?: number;
|
||||
fontWeightBold?: number;
|
||||
// hat's the font-size on the html element.
|
||||
export const ThemeTypographyInputSchema = z.object({
|
||||
fontFamily: z.string().optional(),
|
||||
fontFamilyMonospace: z.string().optional(),
|
||||
fontSize: z.number().positive().optional(),
|
||||
fontWeightLight: z.number().positive().optional(),
|
||||
fontWeightRegular: z.number().positive().optional(),
|
||||
fontWeightMedium: z.number().positive().optional(),
|
||||
fontWeightBold: z.number().positive().optional(),
|
||||
// what's the font-size on the html element.
|
||||
// 16px is the default font-size used by browsers.
|
||||
htmlFontSize?: number;
|
||||
}
|
||||
htmlFontSize: z.number().positive().optional(),
|
||||
});
|
||||
|
||||
export type ThemeTypographyInput = z.infer<typeof ThemeTypographyInputSchema>;
|
||||
|
||||
const defaultFontFamily = "'Inter', 'Helvetica', 'Arial', sans-serif";
|
||||
const defaultFontFamilyMonospace = "'Roboto Mono', monospace";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FALLBACK_COLOR } from '../types/fieldColor';
|
||||
|
||||
import { ThemeColors } from './createColors';
|
||||
@@ -26,29 +28,44 @@ export interface ThemeVizColor<T extends ThemeVizColorName> {
|
||||
|
||||
type ThemeVizColorName = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple';
|
||||
|
||||
type ThemeVizColorShadeName<T extends ThemeVizColorName> =
|
||||
| `super-light-${T}`
|
||||
| `light-${T}`
|
||||
| T
|
||||
| `semi-dark-${T}`
|
||||
| `dark-${T}`;
|
||||
const createShadeSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
|
||||
z.enum([`super-light-${color}`, `light-${color}`, color, `semi-dark-${color}`, `dark-${color}`]);
|
||||
|
||||
type ThemeVizHueGeneric<T> = T extends ThemeVizColorName
|
||||
? {
|
||||
name: T;
|
||||
shades: Array<ThemeVizColor<T>>;
|
||||
}
|
||||
: never;
|
||||
type ThemeVizColorShadeName<T extends ThemeVizColorName> = z.infer<ReturnType<typeof createShadeSchema<T>>>;
|
||||
|
||||
const createHueSchema = <T>(color: T extends ThemeVizColorName ? T : never) =>
|
||||
z.object({
|
||||
name: z.literal(color),
|
||||
shades: z.array(
|
||||
z.object({
|
||||
color: z.string(),
|
||||
name: createShadeSchema(color),
|
||||
aliases: z.array(z.string()).optional(),
|
||||
primary: z.boolean().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const ThemeVizHueSchema = z.union([
|
||||
createHueSchema('red'),
|
||||
createHueSchema('orange'),
|
||||
createHueSchema('yellow'),
|
||||
createHueSchema('green'),
|
||||
createHueSchema('blue'),
|
||||
createHueSchema('purple'),
|
||||
]);
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type ThemeVizHue = ThemeVizHueGeneric<ThemeVizColorName>;
|
||||
export type ThemeVizHue = z.infer<typeof ThemeVizHueSchema>;
|
||||
|
||||
export type ThemeVisualizationColorsInput = {
|
||||
hues?: ThemeVizHue[];
|
||||
palette?: string[];
|
||||
};
|
||||
export const ThemeVisualizationColorsInputSchema = z.object({
|
||||
hues: z.array(ThemeVizHueSchema).optional(),
|
||||
palette: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type ThemeVisualizationColorsInput = z.infer<typeof ThemeVisualizationColorsInputSchema>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Registry, RegistryItem } from '../utils/Registry';
|
||||
|
||||
import { createTheme } from './createTheme';
|
||||
import { createTheme, NewThemeOptionsSchema } from './createTheme';
|
||||
import * as extraThemes from './themeDefinitions';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
|
||||
@@ -42,9 +42,6 @@ export function getBuiltInThemes(allowedExtras: string[]) {
|
||||
return sortedThemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is also a backend list at pkg/services/preference/themes.go
|
||||
*/
|
||||
const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
return [
|
||||
{ id: 'system', name: 'System preference', build: getSystemPreferenceTheme },
|
||||
@@ -53,13 +50,19 @@ const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
for (const [id, theme] of Object.entries(extraThemes)) {
|
||||
themeRegistry.register({
|
||||
id,
|
||||
name: theme.name ?? '',
|
||||
build: () => createTheme(theme),
|
||||
isExtra: true,
|
||||
});
|
||||
for (const [name, json] of Object.entries(extraThemes)) {
|
||||
const result = NewThemeOptionsSchema.safeParse(json);
|
||||
if (!result.success) {
|
||||
console.error(`Invalid theme definition for theme ${name}: ${result.error.message}`);
|
||||
} else {
|
||||
const theme = result.data;
|
||||
themeRegistry.register({
|
||||
id: theme.id,
|
||||
name: theme.name,
|
||||
build: () => createTheme(theme),
|
||||
isExtra: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemPreferenceTheme() {
|
||||
|
||||
608
packages/grafana-data/src/themes/schema.generated.json
Normal file
608
packages/grafana-data/src/themes/schema.generated.json
Normal file
@@ -0,0 +1,608 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"colors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["light", "dark"]
|
||||
},
|
||||
"primary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"secondary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"success": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"warning": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"shade": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"border": {
|
||||
"type": "string"
|
||||
},
|
||||
"transparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"borderTransparent": {
|
||||
"type": "string"
|
||||
},
|
||||
"contrastText": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"text": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxContrast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"background": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canvas": {
|
||||
"type": "string"
|
||||
},
|
||||
"primary": {
|
||||
"type": "string"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string"
|
||||
},
|
||||
"elevated": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"border": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"weak": {
|
||||
"type": "string"
|
||||
},
|
||||
"medium": {
|
||||
"type": "string"
|
||||
},
|
||||
"strong": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"gradients": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"brandVertical": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandHorizontal": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"selected": {
|
||||
"type": "string"
|
||||
},
|
||||
"selectedBorder": {
|
||||
"type": "string"
|
||||
},
|
||||
"hover": {
|
||||
"type": "string"
|
||||
},
|
||||
"hoverOpacity": {
|
||||
"type": "number"
|
||||
},
|
||||
"focus": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledBackground": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledText": {
|
||||
"type": "string"
|
||||
},
|
||||
"disabledOpacity": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"hoverFactor": {
|
||||
"type": "number"
|
||||
},
|
||||
"contrastThreshold": {
|
||||
"type": "number"
|
||||
},
|
||||
"tonalOffset": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"spacing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gridSize": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"shape": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"borderRadius": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"typography": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fontFamily": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontFamilyMonospace": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontSize": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightLight": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightRegular": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightMedium": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fontWeightBold": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"htmlFontSize": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"visualization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hues": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "red"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-red", "light-red", "red", "semi-dark-red", "dark-red"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "orange"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-orange", "light-orange", "orange", "semi-dark-orange", "dark-orange"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "yellow"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-yellow", "light-yellow", "yellow", "semi-dark-yellow", "dark-yellow"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "green"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-green", "light-green", "green", "semi-dark-green", "dark-green"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "blue"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-blue", "light-blue", "blue", "semi-dark-blue", "dark-blue"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"const": "purple"
|
||||
},
|
||||
"shades": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["super-light-purple", "light-purple", "purple", "semi-dark-purple", "dark-purple"]
|
||||
},
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["color", "name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "shades"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"palette": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "id"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
19
packages/grafana-data/src/themes/scripts/generateSchema.ts
Normal file
19
packages/grafana-data/src/themes/scripts/generateSchema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { NewThemeOptionsSchema } from '../createTheme';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '../schema.generated.json'),
|
||||
JSON.stringify(
|
||||
NewThemeOptionsSchema.toJSONSchema({
|
||||
target: 'draft-07',
|
||||
}),
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Aubergine",
|
||||
"id": "aubergine",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#4F2A3D",
|
||||
"medium": "#6A3C4B",
|
||||
"strong": "#8C5A69"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E5D0D6",
|
||||
"secondary": "#D1A8C4",
|
||||
"disabled": "#B7A0A6",
|
||||
"link": "#A56BB6",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#8C5A69"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#6A3C4B",
|
||||
"text": "#D1A8C4",
|
||||
"border": "#8C5A69"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#2E1F2D",
|
||||
"primary": "#3C2136",
|
||||
"secondary": "#4A2D47",
|
||||
"elevated": "#4A2D47"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#6A3C4B",
|
||||
"selected": "#8C5A69",
|
||||
"selectedBorder": "#FFB300",
|
||||
"focus": "#A56BB6",
|
||||
"hoverOpacity": 0.1,
|
||||
"disabledText": "#B7A0A6",
|
||||
"disabledBackground": "#4A2D47",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)"
|
||||
},
|
||||
"contrastThreshold": 4,
|
||||
"hoverFactor": 0.07,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const aubergineTheme: NewThemeOptions = {
|
||||
name: 'Aubergine',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#4F2A3D',
|
||||
medium: '#6A3C4B',
|
||||
strong: '#8C5A69',
|
||||
},
|
||||
text: {
|
||||
primary: '#E5D0D6',
|
||||
secondary: '#D1A8C4',
|
||||
disabled: '#B7A0A6',
|
||||
link: '#A56BB6',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#8C5A69',
|
||||
},
|
||||
secondary: {
|
||||
main: '#6A3C4B',
|
||||
text: '#D1A8C4',
|
||||
border: '#8C5A69',
|
||||
},
|
||||
background: {
|
||||
canvas: '#2E1F2D',
|
||||
primary: '#3C2136',
|
||||
secondary: '#4A2D47',
|
||||
elevated: '#4A2D47',
|
||||
},
|
||||
action: {
|
||||
hover: '#6A3C4B',
|
||||
selected: '#8C5A69',
|
||||
selectedBorder: '#FFB300',
|
||||
focus: '#A56BB6',
|
||||
hoverOpacity: 0.1,
|
||||
disabledText: '#B7A0A6',
|
||||
disabledBackground: '#4A2D47',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #6A3C4B 0%, #A56BB6 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, #6A3C4B 0%, #A56BB6 100%)',
|
||||
},
|
||||
contrastThreshold: 4,
|
||||
hoverFactor: 0.07,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default aubergineTheme;
|
||||
60
packages/grafana-data/src/themes/themeDefinitions/debug.json
Normal file
60
packages/grafana-data/src/themes/themeDefinitions/debug.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "Debug",
|
||||
"id": "debug",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"background": {
|
||||
"canvas": "#000033",
|
||||
"primary": "#000044",
|
||||
"secondary": "#000055",
|
||||
"elevated": "#000055"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#bbbb00",
|
||||
"secondary": "#888800",
|
||||
"disabled": "#444400",
|
||||
"link": "#dddd00",
|
||||
"maxContrast": "#ffff00"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#ff000044",
|
||||
"medium": "#ff000088",
|
||||
"strong": "#ff0000ff"
|
||||
},
|
||||
"primary": {
|
||||
"border": "#ff000088",
|
||||
"text": "#cccc00",
|
||||
"contrastText": "#ffff00",
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"secondary": {
|
||||
"border": "#ff000088",
|
||||
"text": "#cccc00",
|
||||
"contrastText": "#ffff00",
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"info": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"warning": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"success": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"error": {
|
||||
"shade": "#9900dd"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#9900dd",
|
||||
"focus": "#6600aa",
|
||||
"selected": "#440088"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"borderRadius": 8
|
||||
},
|
||||
"spacing": {
|
||||
"gridSize": 10
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
/**
|
||||
* a very ugly theme that is useful for debugging and checking if the theme is applied correctly
|
||||
* borders are red,
|
||||
* backgrounds are blue,
|
||||
* text is yellow,
|
||||
* and grafana loves you <3
|
||||
* (also corners are rounded, action states (hover, focus, selected) are purple)
|
||||
*/
|
||||
const debugTheme: NewThemeOptions = {
|
||||
name: 'Debug',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
background: {
|
||||
canvas: '#000033',
|
||||
primary: '#000044',
|
||||
secondary: '#000055',
|
||||
elevated: '#000055',
|
||||
},
|
||||
text: {
|
||||
primary: '#bbbb00',
|
||||
secondary: '#888800',
|
||||
disabled: '#444400',
|
||||
link: '#dddd00',
|
||||
maxContrast: '#ffff00',
|
||||
},
|
||||
border: {
|
||||
weak: '#ff000044',
|
||||
medium: '#ff000088',
|
||||
strong: '#ff0000ff',
|
||||
},
|
||||
primary: {
|
||||
border: '#ff000088',
|
||||
text: '#cccc00',
|
||||
contrastText: '#ffff00',
|
||||
shade: '#9900dd',
|
||||
},
|
||||
secondary: {
|
||||
border: '#ff000088',
|
||||
text: '#cccc00',
|
||||
contrastText: '#ffff00',
|
||||
shade: '#9900dd',
|
||||
},
|
||||
info: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
warning: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
success: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
error: {
|
||||
shade: '#9900dd',
|
||||
},
|
||||
action: {
|
||||
hover: '#9900dd',
|
||||
focus: '#6600aa',
|
||||
selected: '#440088',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
spacing: {
|
||||
gridSize: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export default debugTheme;
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "Desert bloom",
|
||||
"id": "desertbloom",
|
||||
"colors": {
|
||||
"mode": "light",
|
||||
"border": {
|
||||
"weak": "rgba(0, 0, 0, 0.12)",
|
||||
"medium": "rgba(0, 0, 0, 0.20)",
|
||||
"strong": "rgba(0, 0, 0, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#333333",
|
||||
"secondary": "#555555",
|
||||
"disabled": "rgba(0, 0, 0, 0.5)",
|
||||
"link": "#1A82E2",
|
||||
"maxContrast": "#000000"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FF6F61",
|
||||
"text": "#FE6F61",
|
||||
"border": "#E55B4D",
|
||||
"name": "primary",
|
||||
"shade": "#E55B4D",
|
||||
"transparent": "#FF6F6126",
|
||||
"contrastText": "#FFFFFF",
|
||||
"borderTransparent": "#FF6F6140"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#FFFFFF",
|
||||
"text": "#695f53",
|
||||
"border": "#d9cec0",
|
||||
"name": "secondary",
|
||||
"shade": "#d9cec0",
|
||||
"transparent": "#FFFFFF26",
|
||||
"contrastText": "#4c4339",
|
||||
"borderTransparent": "#FFFFFF40"
|
||||
},
|
||||
"info": {
|
||||
"main": "#1A82E2"
|
||||
},
|
||||
"success": {
|
||||
"main": "#4CAF50"
|
||||
},
|
||||
"warning": {
|
||||
"main": "#FFC107"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#FFF8F0",
|
||||
"primary": "#FFFFFF",
|
||||
"secondary": "#f9f3e8",
|
||||
"elevated": "#FFFFFF"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(168, 156, 134, 0.12)",
|
||||
"selected": "rgba(168, 156, 134, 0.36)",
|
||||
"selectedBorder": "#FF6F61",
|
||||
"focus": "rgba(168, 156, 134, 0.50)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(168, 156, 134, 0.5)",
|
||||
"disabledBackground": "rgba(168, 156, 134, 0.06)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg,rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const desertBloomTheme: NewThemeOptions = {
|
||||
name: 'Desert bloom',
|
||||
colors: {
|
||||
mode: 'light',
|
||||
border: {
|
||||
weak: 'rgba(0, 0, 0, 0.12)',
|
||||
medium: 'rgba(0, 0, 0, 0.20)',
|
||||
strong: 'rgba(0, 0, 0, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: '#333333',
|
||||
secondary: '#555555',
|
||||
disabled: 'rgba(0, 0, 0, 0.5)',
|
||||
link: '#1A82E2',
|
||||
maxContrast: '#000000',
|
||||
},
|
||||
primary: {
|
||||
main: '#FF6F61',
|
||||
text: '#FE6F61',
|
||||
border: '#E55B4D',
|
||||
name: 'primary',
|
||||
shade: '#E55B4D',
|
||||
transparent: '#FF6F6126',
|
||||
contrastText: '#FFFFFF',
|
||||
borderTransparent: '#FF6F6140',
|
||||
},
|
||||
secondary: {
|
||||
main: '#FFFFFF',
|
||||
text: '#695f53',
|
||||
border: '#d9cec0',
|
||||
name: 'secondary',
|
||||
shade: '#d9cec0',
|
||||
transparent: '#FFFFFF26',
|
||||
contrastText: '#4c4339',
|
||||
borderTransparent: '#FFFFFF40',
|
||||
},
|
||||
info: {
|
||||
main: '#1A82E2',
|
||||
},
|
||||
success: {
|
||||
main: '#4CAF50',
|
||||
},
|
||||
warning: {
|
||||
main: '#FFC107',
|
||||
},
|
||||
background: {
|
||||
canvas: '#FFF8F0',
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#f9f3e8',
|
||||
elevated: '#FFFFFF',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(168, 156, 134, 0.12)',
|
||||
selected: 'rgba(168, 156, 134, 0.36)',
|
||||
selectedBorder: '#FF6F61',
|
||||
focus: 'rgba(168, 156, 134, 0.50)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(168, 156, 134, 0.5)',
|
||||
disabledBackground: 'rgba(168, 156, 134, 0.06)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg,rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, rgba(255, 111, 97, 1) 0%, rgba(255, 167, 58, 1) 100%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default desertBloomTheme;
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "Gilded grove",
|
||||
"id": "gildedgrove",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(200, 200, 180, 0.12)",
|
||||
"medium": "rgba(200, 200, 180, 0.20)",
|
||||
"strong": "rgba(200, 200, 180, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "rgb(250, 250, 239)",
|
||||
"secondary": "rgba(200, 200, 180, 0.85)",
|
||||
"disabled": "rgba(200, 200, 180, 0.6)",
|
||||
"link": "#FEAC34",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FEAC34",
|
||||
"text": "#FFD783",
|
||||
"border": "#FFD783",
|
||||
"name": "primary",
|
||||
"shade": "rgb(255, 173, 80)",
|
||||
"transparent": "#FEAC3426",
|
||||
"contrastText": "#111614",
|
||||
"borderTransparent": "#FFD78340"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "rgba(200, 200, 180, 0.10)",
|
||||
"shade": "rgba(200, 200, 180, 0.14)",
|
||||
"transparent": "rgba(200, 200, 180, 0.08)",
|
||||
"text": "rgb(200, 200, 180)",
|
||||
"contrastText": "rgb(200, 200, 180)",
|
||||
"border": "rgba(200, 200, 180, 0.08)",
|
||||
"name": "secondary",
|
||||
"borderTransparent": "rgba(200, 200, 180, 0.25)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#111614",
|
||||
"primary": "#1d2220",
|
||||
"secondary": "#27312E",
|
||||
"elevated": "#27312E"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(200, 200, 180, 0.16)",
|
||||
"selected": "rgba(200, 200, 180, 0.12)",
|
||||
"selectedBorder": "#FEAC34",
|
||||
"focus": "rgba(200, 200, 180, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(200, 200, 180, 0.6)",
|
||||
"disabledBackground": "rgba(200, 200, 180, 0.04)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const gildedGroveTheme: NewThemeOptions = {
|
||||
name: 'Gilded grove',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(200, 200, 180, 0.12)',
|
||||
medium: 'rgba(200, 200, 180, 0.20)',
|
||||
strong: 'rgba(200, 200, 180, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: 'rgb(250, 250, 239)',
|
||||
secondary: 'rgba(200, 200, 180, 0.85)',
|
||||
disabled: 'rgba(200, 200, 180, 0.6)',
|
||||
link: '#FEAC34',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#FEAC34',
|
||||
text: '#FFD783',
|
||||
border: '#FFD783',
|
||||
name: 'primary',
|
||||
shade: 'rgb(255, 173, 80)',
|
||||
transparent: '#FEAC3426',
|
||||
contrastText: '#111614',
|
||||
borderTransparent: '#FFD78340',
|
||||
},
|
||||
secondary: {
|
||||
main: 'rgba(200, 200, 180, 0.10)',
|
||||
shade: 'rgba(200, 200, 180, 0.14)',
|
||||
transparent: 'rgba(200, 200, 180, 0.08)',
|
||||
text: 'rgb(200, 200, 180)',
|
||||
contrastText: 'rgb(200, 200, 180)',
|
||||
border: 'rgba(200, 200, 180, 0.08)',
|
||||
name: 'secondary',
|
||||
borderTransparent: 'rgba(200, 200, 180, 0.25)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#111614',
|
||||
primary: '#1d2220',
|
||||
secondary: '#27312E',
|
||||
elevated: '#27312E',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(200, 200, 180, 0.16)',
|
||||
selected: 'rgba(200, 200, 180, 0.12)',
|
||||
selectedBorder: '#FEAC34',
|
||||
focus: 'rgba(200, 200, 180, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(200, 200, 180, 0.6)',
|
||||
disabledBackground: 'rgba(200, 200, 180, 0.04)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #FEAC34 0%, #FFD783 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #FEAC34 0.01%, #FFD783 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default gildedGroveTheme;
|
||||
52
packages/grafana-data/src/themes/themeDefinitions/gloom.json
Normal file
52
packages/grafana-data/src/themes/themeDefinitions/gloom.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "Gloom",
|
||||
"id": "gloom",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(210, 210, 220, 0.12)",
|
||||
"medium": "rgba(210, 210, 220, 0.20)",
|
||||
"strong": "rgba(210, 210, 220, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "rgb(210, 210, 220)",
|
||||
"secondary": "rgba(210, 210, 220, 0.65)",
|
||||
"disabled": "rgba(210, 210, 220, 0.48)",
|
||||
"link": "#f99a5c",
|
||||
"maxContrast": "#FFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#ff934d",
|
||||
"text": "#f99a5c",
|
||||
"border": "#ff934d",
|
||||
"name": "primary"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "rgba(195, 195, 245, 0.10)",
|
||||
"shade": "rgba(195, 195, 245, 0.14)",
|
||||
"transparent": "rgba(195, 195, 245, 0.08)",
|
||||
"text": "rgba(195, 195, 245)",
|
||||
"contrastText": "rgb(195, 195, 245)",
|
||||
"border": "rgba(195, 195, 245, 0.08)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#000",
|
||||
"primary": "#121118",
|
||||
"secondary": "#211e28",
|
||||
"elevated": "#211e28"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(195, 195, 245, 0.07)",
|
||||
"selected": "rgba(195, 195, 245, 0.11)",
|
||||
"selectedBorder": "#ff934d",
|
||||
"focus": "rgba(195, 195, 245, 0.07)",
|
||||
"hoverOpacity": 0.05,
|
||||
"disabledText": "rgba(210, 210, 220, 0.48)",
|
||||
"disabledBackground": "rgba(210, 210, 220, 0.04)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
/**
|
||||
* Torkel's GrafanaCon theme
|
||||
* very WIP state
|
||||
*/
|
||||
|
||||
const whiteBase = `210, 210, 220`;
|
||||
const secondaryBase = `195, 195, 245`;
|
||||
|
||||
//const brandMain = '#3d71d9';
|
||||
//const brandText = '#6e9fff';
|
||||
const brandMain = '#ff934d';
|
||||
const brandText = '#f99a5c';
|
||||
const disabledText = `rgba(${whiteBase}, 0.48)`;
|
||||
|
||||
const gloomTheme: NewThemeOptions = {
|
||||
name: 'Gloom',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: `rgba(${whiteBase}, 0.12)`,
|
||||
medium: `rgba(${whiteBase}, 0.20)`,
|
||||
strong: `rgba(${whiteBase}, 0.30)`,
|
||||
},
|
||||
|
||||
text: {
|
||||
primary: `rgb(${whiteBase})`,
|
||||
secondary: `rgba(${whiteBase}, 0.65)`,
|
||||
disabled: disabledText,
|
||||
link: brandText,
|
||||
maxContrast: '#FFF',
|
||||
},
|
||||
|
||||
primary: {
|
||||
main: brandMain,
|
||||
text: brandText,
|
||||
border: brandMain,
|
||||
name: 'primary',
|
||||
},
|
||||
|
||||
secondary: {
|
||||
main: `rgba(${secondaryBase}, 0.10)`,
|
||||
shade: `rgba(${secondaryBase}, 0.14)`,
|
||||
transparent: `rgba(${secondaryBase}, 0.08)`,
|
||||
text: `rgba(${secondaryBase})`,
|
||||
contrastText: `rgb(${secondaryBase})`,
|
||||
border: `rgba(${secondaryBase}, 0.08)`,
|
||||
},
|
||||
|
||||
background: {
|
||||
canvas: '#000',
|
||||
primary: '#121118',
|
||||
secondary: '#211e28',
|
||||
elevated: '#211e28',
|
||||
},
|
||||
|
||||
action: {
|
||||
hover: `rgba(${secondaryBase}, 0.07)`,
|
||||
selected: `rgba(${secondaryBase}, 0.11)`,
|
||||
selectedBorder: brandMain,
|
||||
focus: `rgba(${secondaryBase}, 0.07)`,
|
||||
hoverOpacity: 0.05,
|
||||
disabledText: disabledText,
|
||||
disabledBackground: `rgba(${whiteBase}, 0.04)`,
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
|
||||
// gradients: {
|
||||
// brandHorizontal: 'linear-gradient(270deg, #ff934d 0%, #FEAC34 100%)',
|
||||
// brandVertical: 'linear-gradient(0.01deg, #ff934d 0.01%, #FEAC34 99.99%)',
|
||||
// },
|
||||
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default gloomTheme;
|
||||
@@ -1,12 +1,12 @@
|
||||
export { default as aubergine } from './aubergine';
|
||||
export { default as debug } from './debug';
|
||||
export { default as desertbloom } from './desertbloom';
|
||||
export { default as gildedgrove } from './gildedgrove';
|
||||
export { default as mars } from './mars';
|
||||
export { default as matrix } from './matrix';
|
||||
export { default as sapphiredusk } from './sapphiredusk';
|
||||
export { default as synthwave } from './synthwave';
|
||||
export { default as tron } from './tron';
|
||||
export { default as victorian } from './victorian';
|
||||
export { default as zen } from './zen';
|
||||
export { default as gloom } from './gloom';
|
||||
export { default as aubergine } from './aubergine.json';
|
||||
export { default as debug } from './debug.json';
|
||||
export { default as desertbloom } from './desertbloom.json';
|
||||
export { default as gildedgrove } from './gildedgrove.json';
|
||||
export { default as mars } from './mars.json';
|
||||
export { default as matrix } from './matrix.json';
|
||||
export { default as sapphiredusk } from './sapphiredusk.json';
|
||||
export { default as synthwave } from './synthwave.json';
|
||||
export { default as tron } from './tron.json';
|
||||
export { default as victorian } from './victorian.json';
|
||||
export { default as zen } from './zen.json';
|
||||
export { default as gloom } from './gloom.json';
|
||||
|
||||
50
packages/grafana-data/src/themes/themeDefinitions/mars.json
Normal file
50
packages/grafana-data/src/themes/themeDefinitions/mars.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Mars",
|
||||
"id": "mars",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(210, 90, 60, 0.2)",
|
||||
"medium": "rgba(210, 90, 60, 0.35)",
|
||||
"strong": "rgba(210, 90, 60, 0.5)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#DDDDDD",
|
||||
"secondary": "#BBBBBB",
|
||||
"disabled": "rgba(221, 221, 221, 0.5)",
|
||||
"link": "#FF6F61",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FF6F61"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#6a2f2f",
|
||||
"text": "#BBBBBB",
|
||||
"border": "rgba(210, 90, 60, 0.2)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#3C1E1E",
|
||||
"primary": "#522626",
|
||||
"secondary": "#6A2F2F",
|
||||
"elevated": "#6A2F2F"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(210, 90, 60, 0.16)",
|
||||
"selected": "rgba(210, 90, 60, 0.12)",
|
||||
"selectedBorder": "#FF6F61",
|
||||
"focus": "rgba(210, 90, 60, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(221, 221, 221, 0.5)",
|
||||
"disabledBackground": "rgba(210, 90, 60, 0.08)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.05,
|
||||
"tonalOffset": 0.2
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const marsTheme: NewThemeOptions = {
|
||||
name: 'Mars',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(210, 90, 60, 0.2)',
|
||||
medium: 'rgba(210, 90, 60, 0.35)',
|
||||
strong: 'rgba(210, 90, 60, 0.5)',
|
||||
},
|
||||
text: {
|
||||
primary: '#DDDDDD',
|
||||
secondary: '#BBBBBB',
|
||||
disabled: 'rgba(221, 221, 221, 0.5)',
|
||||
link: '#FF6F61',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#FF6F61',
|
||||
},
|
||||
secondary: {
|
||||
main: '#6a2f2f',
|
||||
text: '#BBBBBB',
|
||||
border: 'rgba(210, 90, 60, 0.2)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#3C1E1E',
|
||||
primary: '#522626',
|
||||
secondary: '#6A2F2F',
|
||||
elevated: '#6A2F2F',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(210, 90, 60, 0.16)',
|
||||
selected: 'rgba(210, 90, 60, 0.12)',
|
||||
selectedBorder: '#FF6F61',
|
||||
focus: 'rgba(210, 90, 60, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(221, 221, 221, 0.5)',
|
||||
disabledBackground: 'rgba(210, 90, 60, 0.08)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #FF6F61 0%, #D25A3C 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #FF6F61 0.01%, #D25A3C 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.05,
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
export default marsTheme;
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "Matrix",
|
||||
"id": "matrix",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"background": {
|
||||
"canvas": "#000000",
|
||||
"primary": "#020202",
|
||||
"secondary": "#080808",
|
||||
"elevated": "#080808"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#00c017",
|
||||
"secondary": "#008910",
|
||||
"disabled": "#006a0c",
|
||||
"link": "#00ff41",
|
||||
"maxContrast": "#00ff41"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#008f1144",
|
||||
"medium": "#008f1188",
|
||||
"strong": "#008910"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#008910"
|
||||
},
|
||||
"secondary": {
|
||||
"text": "#008910"
|
||||
},
|
||||
"gradients": {
|
||||
"brandVertical": "linear-gradient(0deg, #008910 0%, #00ff41 100%)",
|
||||
"brandHorizontal": "linear-gradient(90deg, #008910 0%, #00ff41 100%)"
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"borderRadius": 0
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "monospace"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const matrixTheme: NewThemeOptions = {
|
||||
name: 'Matrix',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
background: {
|
||||
canvas: '#000000',
|
||||
primary: '#020202',
|
||||
secondary: '#080808',
|
||||
elevated: '#080808',
|
||||
},
|
||||
text: {
|
||||
primary: '#00c017',
|
||||
secondary: '#008910',
|
||||
disabled: '#006a0c',
|
||||
link: '#00ff41',
|
||||
maxContrast: '#00ff41',
|
||||
},
|
||||
border: {
|
||||
weak: '#008f1144',
|
||||
medium: '#008f1188',
|
||||
strong: '#008910',
|
||||
},
|
||||
primary: {
|
||||
main: '#008910',
|
||||
},
|
||||
secondary: {
|
||||
text: '#008910',
|
||||
},
|
||||
gradients: {
|
||||
brandVertical: 'linear-gradient(0deg, #008910 0%, #00ff41 100%)',
|
||||
brandHorizontal: 'linear-gradient(90deg, #008910 0%, #00ff41 100%)',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
};
|
||||
|
||||
export default matrixTheme;
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "Sapphire dusk",
|
||||
"id": "sapphiredusk",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#232e47",
|
||||
"medium": "#2c3853",
|
||||
"strong": "#404d6b"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#FFFFFF",
|
||||
"secondary": "#bcccdd",
|
||||
"disabled": "#838da5",
|
||||
"link": "#93EBF0",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#93EBF0",
|
||||
"text": "#a8e9ed",
|
||||
"border": "#93ebf0",
|
||||
"name": "primary",
|
||||
"shade": "#c0f5d9",
|
||||
"transparent": "#93EBF029",
|
||||
"contrastText": "#111614",
|
||||
"borderTransparent": "#93ebf040"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#2c364f",
|
||||
"shade": "#36415e",
|
||||
"transparent": "rgba(200, 200, 180, 0.08)",
|
||||
"text": "#d1dfff",
|
||||
"contrastText": "#acfeff",
|
||||
"border": "rgba(200, 200, 180, 0.08)",
|
||||
"name": "secondary",
|
||||
"borderTransparent": "rgba(200, 200, 180, 0.25)"
|
||||
},
|
||||
"info": {
|
||||
"main": "#4d4593",
|
||||
"text": "#a8e9ed",
|
||||
"border": "#5d54a7"
|
||||
},
|
||||
"error": {
|
||||
"main": "#c63370"
|
||||
},
|
||||
"success": {
|
||||
"main": "#1A7F4B"
|
||||
},
|
||||
"warning": {
|
||||
"main": "#D448EA"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#1e273d",
|
||||
"primary": "#12192e",
|
||||
"secondary": "#212c47",
|
||||
"elevated": "#212c47"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#364057",
|
||||
"selected": "#364260",
|
||||
"selectedBorder": "#D448EA",
|
||||
"focus": "#364057",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "#838da5",
|
||||
"disabledBackground": "rgba(54, 64, 87, 0.2)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)",
|
||||
"brandVertical": "linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const sapphireDuskTheme: NewThemeOptions = {
|
||||
name: 'Sapphire dusk',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#232e47',
|
||||
medium: '#2c3853',
|
||||
strong: '#404d6b',
|
||||
},
|
||||
text: {
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#bcccdd',
|
||||
disabled: '#838da5',
|
||||
link: '#93EBF0',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#93EBF0',
|
||||
text: '#a8e9ed',
|
||||
border: '#93ebf0',
|
||||
name: 'primary',
|
||||
shade: '#c0f5d9',
|
||||
transparent: '#93EBF029',
|
||||
contrastText: '#111614',
|
||||
borderTransparent: '#93ebf040',
|
||||
},
|
||||
secondary: {
|
||||
main: '#2c364f',
|
||||
shade: '#36415e',
|
||||
transparent: 'rgba(200, 200, 180, 0.08)',
|
||||
text: '#d1dfff',
|
||||
contrastText: '#acfeff',
|
||||
border: 'rgba(200, 200, 180, 0.08)',
|
||||
name: 'secondary',
|
||||
borderTransparent: 'rgba(200, 200, 180, 0.25)',
|
||||
},
|
||||
info: {
|
||||
main: '#4d4593',
|
||||
text: '#a8e9ed',
|
||||
border: '#5d54a7',
|
||||
},
|
||||
error: {
|
||||
main: '#c63370',
|
||||
},
|
||||
success: {
|
||||
main: '#1A7F4B',
|
||||
},
|
||||
warning: {
|
||||
main: '#D448EA',
|
||||
},
|
||||
background: {
|
||||
canvas: '#1e273d',
|
||||
primary: '#12192e',
|
||||
secondary: '#212c47',
|
||||
elevated: '#212c47',
|
||||
},
|
||||
action: {
|
||||
hover: '#364057',
|
||||
selected: '#364260',
|
||||
selectedBorder: '#D448EA',
|
||||
focus: '#364057',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: '#838da5',
|
||||
disabledBackground: 'rgba(54, 64, 87, 0.2)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #D346EF 0%, #2C83FE 100%)',
|
||||
brandVertical: 'linear-gradient(0deg, #D346EF 0%, #2C83FE 100%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default sapphireDuskTheme;
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Synthwave",
|
||||
"id": "synthwave",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(255, 20, 147, 0.12)",
|
||||
"medium": "rgba(255, 20, 147, 0.20)",
|
||||
"strong": "rgba(255, 20, 147, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E0E0E0",
|
||||
"secondary": "rgba(224, 224, 224, 0.75)",
|
||||
"disabled": "rgba(224, 224, 224, 0.5)",
|
||||
"link": "#FF69B4",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#FF1493"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#37183a",
|
||||
"text": "rgba(224, 224, 224, 0.75)",
|
||||
"border": "rgba(255, 20, 147, 0.10)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#1A1A2E",
|
||||
"primary": "#16213E",
|
||||
"secondary": "#0F3460",
|
||||
"elevated": "#0F3460"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(255, 20, 147, 0.16)",
|
||||
"selected": "rgba(255, 20, 147, 0.12)",
|
||||
"selectedBorder": "#FF1493",
|
||||
"focus": "rgba(255, 20, 147, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(224, 224, 224, 0.5)",
|
||||
"disabledBackground": "rgba(255, 20, 147, 0.08)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.15
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const synthwaveTheme: NewThemeOptions = {
|
||||
name: 'Synthwave',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(255, 20, 147, 0.12)',
|
||||
medium: 'rgba(255, 20, 147, 0.20)',
|
||||
strong: 'rgba(255, 20, 147, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: '#E0E0E0',
|
||||
secondary: 'rgba(224, 224, 224, 0.75)',
|
||||
disabled: 'rgba(224, 224, 224, 0.5)',
|
||||
link: '#FF69B4',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#FF1493',
|
||||
},
|
||||
secondary: {
|
||||
main: '#37183a',
|
||||
text: 'rgba(224, 224, 224, 0.75)',
|
||||
border: 'rgba(255, 20, 147, 0.10)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#1A1A2E',
|
||||
primary: '#16213E',
|
||||
secondary: '#0F3460',
|
||||
elevated: '#0F3460',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(255, 20, 147, 0.16)',
|
||||
selected: 'rgba(255, 20, 147, 0.12)',
|
||||
selectedBorder: '#FF1493',
|
||||
focus: 'rgba(255, 20, 147, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(224, 224, 224, 0.5)',
|
||||
disabledBackground: 'rgba(255, 20, 147, 0.08)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #FF1493 0%, #1E90FF 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #FF1493 0.01%, #1E90FF 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
};
|
||||
|
||||
export default synthwaveTheme;
|
||||
50
packages/grafana-data/src/themes/themeDefinitions/tron.json
Normal file
50
packages/grafana-data/src/themes/themeDefinitions/tron.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Tron",
|
||||
"id": "tron",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "rgba(0, 255, 255, 0.12)",
|
||||
"medium": "rgba(0, 255, 255, 0.20)",
|
||||
"strong": "rgba(0, 255, 255, 0.30)"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#E0E0E0",
|
||||
"secondary": "rgba(224, 224, 224, 0.75)",
|
||||
"disabled": "rgba(224, 224, 224, 0.5)",
|
||||
"link": "#00FFFF",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#00FFFF"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#0b2e36",
|
||||
"text": "rgba(224, 224, 224, 0.75)",
|
||||
"border": "rgba(0, 255, 255, 0.10)"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#0A0F18",
|
||||
"primary": "#0F1B2A",
|
||||
"secondary": "#152234",
|
||||
"elevated": "#152234"
|
||||
},
|
||||
"action": {
|
||||
"hover": "rgba(0, 255, 255, 0.16)",
|
||||
"selected": "rgba(0, 255, 255, 0.12)",
|
||||
"selectedBorder": "#00FFFF",
|
||||
"focus": "rgba(0, 255, 255, 0.16)",
|
||||
"hoverOpacity": 0.08,
|
||||
"disabledText": "rgba(224, 224, 224, 0.5)",
|
||||
"disabledBackground": "rgba(0, 255, 255, 0.08)",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.05,
|
||||
"tonalOffset": 0.2
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const tronTheme: NewThemeOptions = {
|
||||
name: 'Tron',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: 'rgba(0, 255, 255, 0.12)',
|
||||
medium: 'rgba(0, 255, 255, 0.20)',
|
||||
strong: 'rgba(0, 255, 255, 0.30)',
|
||||
},
|
||||
text: {
|
||||
primary: '#E0E0E0',
|
||||
secondary: 'rgba(224, 224, 224, 0.75)',
|
||||
disabled: 'rgba(224, 224, 224, 0.5)',
|
||||
link: '#00FFFF',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#00FFFF',
|
||||
},
|
||||
secondary: {
|
||||
main: '#0b2e36',
|
||||
text: 'rgba(224, 224, 224, 0.75)',
|
||||
border: 'rgba(0, 255, 255, 0.10)',
|
||||
},
|
||||
background: {
|
||||
canvas: '#0A0F18',
|
||||
primary: '#0F1B2A',
|
||||
secondary: '#152234',
|
||||
elevated: '#152234',
|
||||
},
|
||||
action: {
|
||||
hover: 'rgba(0, 255, 255, 0.16)',
|
||||
selected: 'rgba(0, 255, 255, 0.12)',
|
||||
selectedBorder: '#00FFFF',
|
||||
focus: 'rgba(0, 255, 255, 0.16)',
|
||||
hoverOpacity: 0.08,
|
||||
disabledText: 'rgba(224, 224, 224, 0.5)',
|
||||
disabledBackground: 'rgba(0, 255, 255, 0.08)',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #00FFFF 0%, #29ABE2 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #00FFFF 0.01%, #29ABE2 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.05,
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
export default tronTheme;
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "Victorian",
|
||||
"id": "victorian",
|
||||
"colors": {
|
||||
"mode": "dark",
|
||||
"border": {
|
||||
"weak": "#3A2C22",
|
||||
"medium": "#3A2C22",
|
||||
"strong": "#4B3D32"
|
||||
},
|
||||
"text": {
|
||||
"primary": "#D9D0A2",
|
||||
"secondary": "#C4B89B",
|
||||
"disabled": "#A89F91",
|
||||
"link": "#C28A4D",
|
||||
"maxContrast": "#FFFFFF"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#C28A4D"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#3A2C22",
|
||||
"text": "#C4B89B",
|
||||
"border": "#4B3D32"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#1F1510",
|
||||
"primary": "#2C1A13",
|
||||
"secondary": "#402A21",
|
||||
"elevated": "#402A21"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#3A2C22",
|
||||
"selected": "#4B3D32",
|
||||
"selectedBorder": "#C28A4D",
|
||||
"focus": "#C28A4D",
|
||||
"hoverOpacity": 0.1,
|
||||
"disabledText": "#A89F91",
|
||||
"disabledBackground": "#402A21",
|
||||
"disabledOpacity": 0.38
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 4,
|
||||
"hoverFactor": 0.07,
|
||||
"tonalOffset": 0.15
|
||||
},
|
||||
"typography": {
|
||||
"fontFamily": "\"Georgia\", \"Times New Roman\", serif",
|
||||
"fontFamilyMonospace": "'Courier New', monospace"
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const victorianTheme: NewThemeOptions = {
|
||||
name: 'Victorian',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
border: {
|
||||
weak: '#3A2C22',
|
||||
medium: '#3A2C22',
|
||||
strong: '#4B3D32',
|
||||
},
|
||||
text: {
|
||||
primary: '#D9D0A2',
|
||||
secondary: '#C4B89B',
|
||||
disabled: '#A89F91',
|
||||
link: '#C28A4D',
|
||||
maxContrast: '#FFFFFF',
|
||||
},
|
||||
primary: {
|
||||
main: '#C28A4D',
|
||||
},
|
||||
secondary: {
|
||||
main: '#3A2C22',
|
||||
text: '#C4B89B',
|
||||
border: '#4B3D32',
|
||||
},
|
||||
background: {
|
||||
canvas: '#1F1510',
|
||||
primary: '#2C1A13',
|
||||
secondary: '#402A21',
|
||||
elevated: '#402A21',
|
||||
},
|
||||
action: {
|
||||
hover: '#3A2C22',
|
||||
selected: '#4B3D32',
|
||||
selectedBorder: '#C28A4D',
|
||||
focus: '#C28A4D',
|
||||
hoverOpacity: 0.1,
|
||||
disabledText: '#A89F91',
|
||||
disabledBackground: '#402A21',
|
||||
disabledOpacity: 0.38,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #D9D0a1 0%, #C28A4D 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #D9D0a1 0.01%, #C28A4D 99.99%)',
|
||||
},
|
||||
contrastThreshold: 4,
|
||||
hoverFactor: 0.07,
|
||||
tonalOffset: 0.15,
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Georgia", "Times New Roman", serif',
|
||||
fontFamilyMonospace: "'Courier New', monospace",
|
||||
},
|
||||
};
|
||||
|
||||
export default victorianTheme;
|
||||
50
packages/grafana-data/src/themes/themeDefinitions/zen.json
Normal file
50
packages/grafana-data/src/themes/themeDefinitions/zen.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Zen",
|
||||
"id": "zen",
|
||||
"colors": {
|
||||
"mode": "light",
|
||||
"text": {
|
||||
"primary": "#333333",
|
||||
"secondary": "#666666",
|
||||
"disabled": "#B8B8B8",
|
||||
"link": "#4F9F6E",
|
||||
"maxContrast": "#000000"
|
||||
},
|
||||
"border": {
|
||||
"weak": "#B1B7B3",
|
||||
"medium": "#A2A8A2",
|
||||
"strong": "#7C7F7A"
|
||||
},
|
||||
"primary": {
|
||||
"main": "#6D8E6D"
|
||||
},
|
||||
"secondary": {
|
||||
"main": "#E0E0E0",
|
||||
"text": "#666666",
|
||||
"border": "#A2A8A2"
|
||||
},
|
||||
"background": {
|
||||
"canvas": "#F4F4F4",
|
||||
"primary": "#E9E9E9",
|
||||
"secondary": "#D8D8D8",
|
||||
"elevated": "#E9E9E9"
|
||||
},
|
||||
"action": {
|
||||
"hover": "#D1D1D1",
|
||||
"selected": "#B8B8B8",
|
||||
"selectedBorder": "#88B88B",
|
||||
"hoverOpacity": 0.1,
|
||||
"focus": "#D1D1D1",
|
||||
"disabledBackground": "#E0E0E0",
|
||||
"disabledText": "#B8B8B8",
|
||||
"disabledOpacity": 0.5
|
||||
},
|
||||
"gradients": {
|
||||
"brandHorizontal": "linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)",
|
||||
"brandVertical": "linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)"
|
||||
},
|
||||
"contrastThreshold": 3,
|
||||
"hoverFactor": 0.03,
|
||||
"tonalOffset": 0.2
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NewThemeOptions } from '../createTheme';
|
||||
|
||||
const zenTheme: NewThemeOptions = {
|
||||
name: 'Zen',
|
||||
colors: {
|
||||
mode: 'light',
|
||||
text: {
|
||||
primary: '#333333',
|
||||
secondary: '#666666',
|
||||
disabled: '#B8B8B8',
|
||||
link: '#4F9F6E',
|
||||
maxContrast: '#000000',
|
||||
},
|
||||
border: {
|
||||
weak: '#B1B7B3',
|
||||
medium: '#A2A8A2',
|
||||
strong: '#7C7F7A',
|
||||
},
|
||||
primary: {
|
||||
main: '#6D8E6D',
|
||||
},
|
||||
secondary: {
|
||||
main: '#E0E0E0',
|
||||
text: '#666666',
|
||||
border: '#A2A8A2',
|
||||
},
|
||||
background: {
|
||||
canvas: '#F4F4F4',
|
||||
primary: '#E9E9E9',
|
||||
secondary: '#D8D8D8',
|
||||
elevated: '#E9E9E9',
|
||||
},
|
||||
action: {
|
||||
hover: '#D1D1D1',
|
||||
selected: '#B8B8B8',
|
||||
selectedBorder: '#88B88B',
|
||||
hoverOpacity: 0.1,
|
||||
focus: '#D1D1D1',
|
||||
disabledBackground: '#E0E0E0',
|
||||
disabledText: '#B8B8B8',
|
||||
disabledOpacity: 0.5,
|
||||
},
|
||||
gradients: {
|
||||
brandHorizontal: 'linear-gradient(270deg, #88B88B 0%, #6D8E6D 100%)',
|
||||
brandVertical: 'linear-gradient(0.01deg, #88B88B 0.01%, #6D8E6D 99.99%)',
|
||||
},
|
||||
contrastThreshold: 3,
|
||||
hoverFactor: 0.03,
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
};
|
||||
|
||||
export default zenTheme;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { GrafanaTheme } from '../types/theme';
|
||||
|
||||
import { ThemeBreakpoints } from './breakpoints';
|
||||
@@ -35,27 +37,36 @@ export interface GrafanaTheme2 {
|
||||
flags: {};
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export interface ThemeRichColor {
|
||||
export const ThemeRichColorInputSchema = z.object({
|
||||
/** color intent (primary, secondary, info, error, etc) */
|
||||
name: string;
|
||||
name: z.string().optional(),
|
||||
/** Main color */
|
||||
main: string;
|
||||
main: z.string().optional(),
|
||||
/** Used for hover */
|
||||
shade: string;
|
||||
shade: z.string().optional(),
|
||||
/** Used for text */
|
||||
text: string;
|
||||
text: z.string().optional(),
|
||||
/** Used for borders */
|
||||
border: string;
|
||||
border: z.string().optional(),
|
||||
/** Used subtly colored backgrounds */
|
||||
transparent: string;
|
||||
transparent: z.string().optional(),
|
||||
/** Used for weak colored borders like larger alert/banner boxes and smaller badges and tags */
|
||||
borderTransparent: string;
|
||||
borderTransparent: z.string().optional(),
|
||||
/** Text color for text ontop of main */
|
||||
contrastText: string;
|
||||
}
|
||||
contrastText: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ThemeRichColorSchema = ThemeRichColorInputSchema.required();
|
||||
|
||||
/** @alpha */
|
||||
export type ThemeRichColor = z.infer<typeof ThemeRichColorSchema>;
|
||||
|
||||
/** @internal */
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type DeepRequired<T> = Required<{
|
||||
[P in keyof T]: T[P] extends Required<T[P]> ? T[P] : DeepRequired<T[P]>;
|
||||
}>;
|
||||
|
||||
@@ -649,10 +649,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
rolePickerDrawer?: boolean;
|
||||
/**
|
||||
* Enable sprinkles on unified storage search
|
||||
*/
|
||||
unifiedStorageSearchSprinkles?: boolean;
|
||||
/**
|
||||
* Pick the dual write mode from database configs
|
||||
*/
|
||||
managedDualWriter?: boolean;
|
||||
|
||||
@@ -9,5 +9,4 @@
|
||||
* and be subject to the standard policies
|
||||
*/
|
||||
|
||||
// This is a dummy export so typescript doesn't error importing an "empty module"
|
||||
export const unstable = {};
|
||||
export { default as themeJsonSchema } from './themes/schema.generated.json';
|
||||
|
||||
500
packages/grafana-test-utils/src/fixtures/scopes.ts
Normal file
500
packages/grafana-test-utils/src/fixtures/scopes.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* Types for Scopes API - matching @grafana/data types
|
||||
*/
|
||||
|
||||
export interface ScopeFilter {
|
||||
key: string;
|
||||
value: string;
|
||||
operator: 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match';
|
||||
}
|
||||
|
||||
export interface ScopeSpec {
|
||||
title: string;
|
||||
filters: ScopeFilter[];
|
||||
}
|
||||
|
||||
export interface Scope {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: ScopeSpec;
|
||||
}
|
||||
|
||||
export interface ScopeNodeSpec {
|
||||
nodeType: 'container' | 'leaf';
|
||||
title: string;
|
||||
description?: string;
|
||||
disableMultiSelect?: boolean;
|
||||
linkType?: 'scope';
|
||||
linkId?: string;
|
||||
parentName: string;
|
||||
}
|
||||
|
||||
export interface ScopeNode {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: ScopeNodeSpec;
|
||||
}
|
||||
|
||||
export interface ScopeDashboardBindingSpec {
|
||||
dashboard: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface ScopeDashboardBindingStatus {
|
||||
dashboardTitle: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
export interface ScopeDashboardBinding {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: ScopeDashboardBindingSpec;
|
||||
status: ScopeDashboardBindingStatus;
|
||||
}
|
||||
|
||||
export interface ScopeNavigation {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: {
|
||||
url: string;
|
||||
scope: string;
|
||||
subScope?: string;
|
||||
preLoadSubScopeChildren?: boolean;
|
||||
expandOnLoad?: boolean;
|
||||
disableSubScopeSelection?: boolean;
|
||||
};
|
||||
status: {
|
||||
title: string;
|
||||
groups?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export const MOCK_SCOPES: Scope[] = [
|
||||
{
|
||||
metadata: { name: 'cloud' },
|
||||
spec: {
|
||||
title: 'Cloud',
|
||||
filters: [{ key: 'cloud', value: '.*', operator: 'regex-match' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'dev' },
|
||||
spec: {
|
||||
title: 'Dev',
|
||||
filters: [{ key: 'cloud', value: 'dev', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'ops' },
|
||||
spec: {
|
||||
title: 'Ops',
|
||||
filters: [{ key: 'cloud', value: 'ops', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'prod' },
|
||||
spec: {
|
||||
title: 'Prod',
|
||||
filters: [{ key: 'cloud', value: 'prod', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'grafana' },
|
||||
spec: {
|
||||
title: 'Grafana',
|
||||
filters: [{ key: 'app', value: 'grafana', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'mimir' },
|
||||
spec: {
|
||||
title: 'Mimir',
|
||||
filters: [{ key: 'app', value: 'mimir', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'loki' },
|
||||
spec: {
|
||||
title: 'Loki',
|
||||
filters: [{ key: 'app', value: 'loki', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'tempo' },
|
||||
spec: {
|
||||
title: 'Tempo',
|
||||
filters: [{ key: 'app', value: 'tempo', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'dev-env' },
|
||||
spec: {
|
||||
title: 'Development',
|
||||
filters: [{ key: 'environment', value: 'dev', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'prod-env' },
|
||||
spec: {
|
||||
title: 'Production',
|
||||
filters: [{ key: 'environment', value: 'prod', operator: 'equals' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const dashboardBindingsGenerator = (
|
||||
scopes: string[],
|
||||
dashboards: Array<{ dashboardTitle: string; dashboardKey?: string; groups?: string[] }>
|
||||
) =>
|
||||
scopes.reduce<ScopeDashboardBinding[]>((scopeAcc, scopeTitle) => {
|
||||
const scope = scopeTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
|
||||
|
||||
return [
|
||||
...scopeAcc,
|
||||
...dashboards.reduce<ScopeDashboardBinding[]>((acc, { dashboardTitle, groups, dashboardKey }, idx) => {
|
||||
dashboardKey = dashboardKey ?? dashboardTitle.toLowerCase().replaceAll(' ', '-').replaceAll('/', '-');
|
||||
const group = !groups
|
||||
? ''
|
||||
: groups.length === 1
|
||||
? groups[0] === ''
|
||||
? ''
|
||||
: `${groups[0].toLowerCase().replaceAll(' ', '-').replaceAll('/', '-')}-`
|
||||
: `multiple${idx}-`;
|
||||
const dashboard = `${group}${dashboardKey}`;
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
metadata: { name: `${scope}-${dashboard}` },
|
||||
spec: {
|
||||
dashboard,
|
||||
scope,
|
||||
},
|
||||
status: {
|
||||
dashboardTitle,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []),
|
||||
];
|
||||
}, []);
|
||||
|
||||
export const MOCK_SCOPE_DASHBOARD_BINDINGS: ScopeDashboardBinding[] = [
|
||||
...dashboardBindingsGenerator(
|
||||
['Grafana'],
|
||||
[
|
||||
{ dashboardTitle: 'Data Sources', groups: ['General'] },
|
||||
{ dashboardTitle: 'Usage', groups: ['General'] },
|
||||
{ dashboardTitle: 'Frontend Errors', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Frontend Logs', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Backend Errors', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Backend Logs', groups: ['Observability'] },
|
||||
{ dashboardTitle: 'Usage Overview', groups: ['Usage'] },
|
||||
{ dashboardTitle: 'Data Sources', groups: ['Usage'] },
|
||||
{ dashboardTitle: 'Stats', groups: ['Usage'] },
|
||||
{ dashboardTitle: 'Overview', groups: [''] },
|
||||
{ dashboardTitle: 'Frontend' },
|
||||
{ dashboardTitle: 'Stats' },
|
||||
]
|
||||
),
|
||||
...dashboardBindingsGenerator(
|
||||
['Loki', 'Tempo', 'Mimir'],
|
||||
[
|
||||
{ dashboardTitle: 'Ingester', groups: ['Components', 'Investigations'] },
|
||||
{ dashboardTitle: 'Distributor', groups: ['Components', 'Investigations'] },
|
||||
{ dashboardTitle: 'Compacter', groups: ['Components', 'Investigations'] },
|
||||
{ dashboardTitle: 'Datasource Errors', groups: ['Observability', 'Investigations'] },
|
||||
{ dashboardTitle: 'Datasource Logs', groups: ['Observability', 'Investigations'] },
|
||||
{ dashboardTitle: 'Overview' },
|
||||
{ dashboardTitle: 'Stats', dashboardKey: 'another-stats' },
|
||||
]
|
||||
),
|
||||
...dashboardBindingsGenerator(
|
||||
['Dev', 'Ops', 'Prod'],
|
||||
[
|
||||
{ dashboardTitle: 'Overview', groups: ['Cardinality Management'] },
|
||||
{ dashboardTitle: 'Metrics', groups: ['Cardinality Management'] },
|
||||
{ dashboardTitle: 'Labels', groups: ['Cardinality Management'] },
|
||||
{ dashboardTitle: 'Overview', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Data Sources', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Query Errors', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Alertmanager', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Metrics Ingestion', groups: ['Usage Insights'] },
|
||||
{ dashboardTitle: 'Billing/Usage' },
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
export const MOCK_NODES: ScopeNode[] = [
|
||||
{
|
||||
metadata: { name: 'applications' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Applications',
|
||||
description: 'Application Scopes',
|
||||
parentName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Cloud',
|
||||
description: 'Cloud Scopes',
|
||||
disableMultiSelect: true,
|
||||
linkType: 'scope',
|
||||
linkId: 'cloud',
|
||||
parentName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-grafana' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Grafana',
|
||||
description: 'Grafana',
|
||||
linkType: 'scope',
|
||||
linkId: 'grafana',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-mimir' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Mimir',
|
||||
description: 'Mimir',
|
||||
linkType: 'scope',
|
||||
linkId: 'mimir',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-loki' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Loki',
|
||||
description: 'Loki',
|
||||
linkType: 'scope',
|
||||
linkId: 'loki',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-tempo' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Tempo',
|
||||
description: 'Tempo',
|
||||
linkType: 'scope',
|
||||
linkId: 'tempo',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Cloud',
|
||||
description: 'Application/Cloud Scopes',
|
||||
linkType: 'scope',
|
||||
linkId: 'cloud',
|
||||
parentName: 'applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud-dev' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Dev',
|
||||
description: 'Dev',
|
||||
linkType: 'scope',
|
||||
linkId: 'dev',
|
||||
parentName: 'applications-cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud-ops' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Ops',
|
||||
description: 'Ops',
|
||||
linkType: 'scope',
|
||||
linkId: 'ops',
|
||||
parentName: 'applications-cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'applications-cloud-prod' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Prod',
|
||||
description: 'Prod',
|
||||
linkType: 'scope',
|
||||
linkId: 'prod',
|
||||
parentName: 'applications-cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-dev' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Dev',
|
||||
description: 'Dev',
|
||||
linkType: 'scope',
|
||||
linkId: 'dev',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-ops' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Ops',
|
||||
description: 'Ops',
|
||||
linkType: 'scope',
|
||||
linkId: 'ops',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-prod' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Prod',
|
||||
description: 'Prod',
|
||||
linkType: 'scope',
|
||||
linkId: 'prod',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Applications',
|
||||
description: 'Cloud/Application Scopes',
|
||||
parentName: 'cloud',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-grafana' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Grafana',
|
||||
description: 'Grafana',
|
||||
linkType: 'scope',
|
||||
linkId: 'grafana',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-mimir' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Mimir',
|
||||
description: 'Mimir',
|
||||
linkType: 'scope',
|
||||
linkId: 'mimir',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-loki' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Loki',
|
||||
description: 'Loki',
|
||||
linkType: 'scope',
|
||||
linkId: 'loki',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'cloud-applications-tempo' },
|
||||
spec: {
|
||||
nodeType: 'leaf',
|
||||
title: 'Tempo',
|
||||
description: 'Tempo',
|
||||
linkType: 'scope',
|
||||
linkId: 'tempo',
|
||||
parentName: 'cloud-applications',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'environments' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Environments',
|
||||
description: 'Environment Scopes',
|
||||
disableMultiSelect: true,
|
||||
parentName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'environments-dev' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Development',
|
||||
description: 'Development Environment',
|
||||
linkType: 'scope',
|
||||
linkId: 'dev-env',
|
||||
parentName: 'environments',
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'environments-prod' },
|
||||
spec: {
|
||||
nodeType: 'container',
|
||||
title: 'Production',
|
||||
description: 'Production Environment',
|
||||
linkType: 'scope',
|
||||
linkId: 'prod-env',
|
||||
parentName: 'environments',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_SUB_SCOPE_MIMIR_ITEMS: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'mimir-item-1' },
|
||||
spec: {
|
||||
scope: 'mimir',
|
||||
url: '/d/mimir-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboard 1',
|
||||
groups: ['General'],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { name: 'mimir-item-2' },
|
||||
spec: {
|
||||
scope: 'mimir',
|
||||
url: '/d/mimir-dashboard-2',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboard 2',
|
||||
groups: ['Observability'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_SUB_SCOPE_LOKI_ITEMS: ScopeNavigation[] = [
|
||||
{
|
||||
metadata: { name: 'loki-item-1' },
|
||||
spec: {
|
||||
scope: 'loki',
|
||||
url: '/d/loki-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Loki Dashboard 1',
|
||||
groups: ['General'],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -12,6 +12,7 @@ import appPlatformDashboardv0alpha1Handlers from './apis/dashboard.grafana.app/v
|
||||
import appPlatformDashboardv1beta1Handlers from './apis/dashboard.grafana.app/v1beta1/handlers';
|
||||
import appPlatformFolderv1beta1Handlers from './apis/folder.grafana.app/v1beta1/handlers';
|
||||
import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/handlers';
|
||||
import appPlatformScopev0alpha1Handlers from './apis/scope.grafana.app/v0alpha1/handlers';
|
||||
|
||||
const allHandlers: HttpHandler[] = [
|
||||
// Legacy handlers
|
||||
@@ -29,6 +30,7 @@ const allHandlers: HttpHandler[] = [
|
||||
...appPlatformFolderv1beta1Handlers,
|
||||
...appPlatformIamv0alpha1Handlers,
|
||||
...appPlatformCollectionsv1alpha1Handlers,
|
||||
...appPlatformScopev0alpha1Handlers,
|
||||
];
|
||||
|
||||
export default allHandlers;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import {
|
||||
MOCK_NODES,
|
||||
MOCK_SCOPES,
|
||||
MOCK_SCOPE_DASHBOARD_BINDINGS,
|
||||
MOCK_SUB_SCOPE_LOKI_ITEMS,
|
||||
MOCK_SUB_SCOPE_MIMIR_ITEMS,
|
||||
ScopeNavigation,
|
||||
} from '../../../../fixtures/scopes';
|
||||
import { getErrorResponse } from '../../../helpers';
|
||||
|
||||
const API_BASE = '/apis/scope.grafana.app/v0alpha1/namespaces/:namespace';
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/scopes/:name
|
||||
*
|
||||
* Fetches a single scope by name.
|
||||
*/
|
||||
const getScopeHandler = () =>
|
||||
http.get<{ namespace: string; name: string }>(`${API_BASE}/scopes/:name`, ({ params }) => {
|
||||
const { name } = params;
|
||||
const scope = MOCK_SCOPES.find((s) => s.metadata.name === name);
|
||||
|
||||
if (!scope) {
|
||||
return HttpResponse.json(getErrorResponse(`scopes.scope.grafana.app "${name}" not found`, 404), {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return HttpResponse.json(scope);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/scopenodes/:name
|
||||
*
|
||||
* Fetches a single scope node by name.
|
||||
*/
|
||||
const getScopeNodeHandler = () =>
|
||||
http.get<{ namespace: string; name: string }>(`${API_BASE}/scopenodes/:name`, ({ params }) => {
|
||||
const { name } = params;
|
||||
const node = MOCK_NODES.find((n) => n.metadata.name === name);
|
||||
|
||||
if (!node) {
|
||||
return HttpResponse.json(getErrorResponse(`scopenodes.scope.grafana.app "${name}" not found`, 404), {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return HttpResponse.json(node);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_node_children
|
||||
*
|
||||
* Finds scope node children based on parent and query filters.
|
||||
*/
|
||||
const findScopeNodeChildrenHandler = () =>
|
||||
http.get(`${API_BASE}/find/scope_node_children`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const parent = url.searchParams.get('parent') ?? '';
|
||||
const query = url.searchParams.get('query') ?? '';
|
||||
const limitParam = url.searchParams.get('limit');
|
||||
const names = url.searchParams.getAll('names');
|
||||
|
||||
let filtered = MOCK_NODES.filter(
|
||||
(node) => node.spec.parentName === parent && node.spec.title.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
if (names.length > 0) {
|
||||
filtered = MOCK_NODES.filter((node) => names.includes(node.metadata.name));
|
||||
}
|
||||
|
||||
if (limitParam) {
|
||||
const limit = parseInt(limitParam, 10);
|
||||
filtered = filtered.slice(0, limit);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
items: filtered,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_dashboard_bindings
|
||||
*
|
||||
* Finds scope dashboard bindings for the given scope names.
|
||||
*/
|
||||
const findScopeDashboardBindingsHandler = () =>
|
||||
http.get(`${API_BASE}/find/scope_dashboard_bindings`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const scopeNames = url.searchParams.getAll('scope');
|
||||
|
||||
const bindings = MOCK_SCOPE_DASHBOARD_BINDINGS.filter((b) => scopeNames.includes(b.spec.scope));
|
||||
|
||||
return HttpResponse.json({
|
||||
items: bindings,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /apis/scope.grafana.app/v0alpha1/namespaces/:namespace/find/scope_navigations
|
||||
*
|
||||
* Finds scope navigations for the given scope names.
|
||||
*/
|
||||
const findScopeNavigationsHandler = () =>
|
||||
http.get(`${API_BASE}/find/scope_navigations`, ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const scopeNames = url.searchParams.getAll('scope');
|
||||
|
||||
let items: ScopeNavigation[] = [];
|
||||
|
||||
if (scopeNames.includes('mimir')) {
|
||||
items = [...items, ...MOCK_SUB_SCOPE_MIMIR_ITEMS];
|
||||
}
|
||||
if (scopeNames.includes('loki')) {
|
||||
items = [...items, ...MOCK_SUB_SCOPE_LOKI_ITEMS];
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
items,
|
||||
});
|
||||
});
|
||||
|
||||
export default [
|
||||
getScopeHandler(),
|
||||
getScopeNodeHandler(),
|
||||
findScopeNodeChildrenHandler(),
|
||||
findScopeDashboardBindingsHandler(),
|
||||
findScopeNavigationsHandler(),
|
||||
];
|
||||
@@ -2,3 +2,12 @@ import { wellFormedTree } from './fixtures/folders';
|
||||
|
||||
export const getFolderFixtures = wellFormedTree;
|
||||
export { MOCK_TEAMS, MOCK_TEAM_GROUPS } from './fixtures/teams';
|
||||
export {
|
||||
MOCK_SCOPES,
|
||||
MOCK_NODES,
|
||||
MOCK_SCOPE_DASHBOARD_BINDINGS,
|
||||
MOCK_SUB_SCOPE_MIMIR_ITEMS,
|
||||
MOCK_SUB_SCOPE_LOKI_ITEMS,
|
||||
} from './fixtures/scopes';
|
||||
export { default as allHandlers } from './handlers/all-handlers';
|
||||
export { default as scopeHandlers } from './handlers/apis/scope.grafana.app/v0alpha1/handlers';
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||
_ "github.com/Azure/go-autorest/autorest"
|
||||
_ "github.com/Azure/go-autorest/autorest/adal"
|
||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
_ "github.com/beevik/etree"
|
||||
_ "github.com/blugelabs/bluge"
|
||||
_ "github.com/blugelabs/bluge_segment_api"
|
||||
|
||||
@@ -156,7 +156,7 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O
|
||||
}
|
||||
}
|
||||
}
|
||||
connectLogger.Debug("responder sending status code", "statusCode", statusCode, "caller", getCaller(ctx))
|
||||
connectLogger.Debug("responder sending status code", "statusCode", *statusCode, "caller", getCaller(ctx))
|
||||
},
|
||||
|
||||
func(err error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/dskit/flagext"
|
||||
@@ -15,11 +16,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
@@ -111,14 +116,25 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
|
||||
Help: "Time spent executing requests to resource server.",
|
||||
Buckets: prometheus.ExponentialBuckets(0.008, 4, 7),
|
||||
}, []string{"operation", "status_code"})
|
||||
factoryRequestRetries := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "resource_server_client_request_retries_total",
|
||||
Help: "Total number of retries for requests to the resource server.",
|
||||
}, []string{"operation"})
|
||||
|
||||
factory := ringclient.PoolInstFunc(func(inst ring.InstanceDesc) (ringclient.PoolClient, error) {
|
||||
unaryInterceptors, streamInterceptors := grpcclient.Instrument(factoryRequestDuration)
|
||||
|
||||
// Add retry interceptors for transient connection issues
|
||||
unaryInterceptors = append(unaryInterceptors, ringClientRetryInterceptor())
|
||||
unaryInterceptors = append(unaryInterceptors, ringClientRetryInstrument(factoryRequestRetries))
|
||||
|
||||
opts, err := clientCfg.DialOption(unaryInterceptors, streamInterceptors, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, connectionBackoffOptions())
|
||||
|
||||
conn, err := grpc.NewClient(inst.Addr, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial resource server %s %s: %s", inst.Id, inst.Addr, err)
|
||||
@@ -135,3 +151,40 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
|
||||
|
||||
return ringclient.NewPool(resource.RingName, poolCfg, nil, factory, clientsCount, log)
|
||||
}
|
||||
|
||||
// ringClientRetryInterceptor creates an interceptor to perform retries for unary methods.
|
||||
// It retries on ResourceExhausted and Unavailable codes, which are typical for
|
||||
// transient connection issues and rate limiting.
|
||||
func ringClientRetryInterceptor() grpc.UnaryClientInterceptor {
|
||||
return grpc_retry.UnaryClientInterceptor(
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.1)),
|
||||
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
|
||||
)
|
||||
}
|
||||
|
||||
// ringClientRetryInstrument creates an interceptor to count retry attempts for metrics.
|
||||
func ringClientRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, resp interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
// We can tell if a call is a retry by checking the retry attempt metadata.
|
||||
attempt, err := strconv.Atoi(metautils.ExtractOutgoing(ctx).Get(grpc_retry.AttemptMetadataKey))
|
||||
if err == nil && attempt > 0 {
|
||||
metric.WithLabelValues(method).Inc()
|
||||
}
|
||||
return invoker(ctx, method, req, resp, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// connectionBackoffOptions configures connection backoff parameters for faster recovery from
|
||||
// transient connection failures (e.g., during pod restarts).
|
||||
func connectionBackoffOptions() grpc.DialOption {
|
||||
return grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
|
||||
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
secret "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
@@ -232,19 +237,16 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
|
||||
if o.StorageType != StorageTypeUnifiedGrpc {
|
||||
return nil
|
||||
}
|
||||
conn, err := grpc.NewClient(o.Address,
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
|
||||
grpcOpts := o.buildGrpcDialOptions()
|
||||
|
||||
conn, err := grpc.NewClient(o.Address, grpcOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var indexConn *grpc.ClientConn
|
||||
if o.SearchServerAddress != "" {
|
||||
indexConn, err = grpc.NewClient(o.SearchServerAddress,
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
indexConn, err = grpc.NewClient(o.SearchServerAddress, grpcOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -293,3 +295,42 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
|
||||
serverConfig.RESTOptionsGetter = getter
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildGrpcDialOptions creates gRPC dial options with resilience mechanisms:
|
||||
// - Round-robin load balancing with client-side health checking
|
||||
// - Retry interceptor for transient connection issues
|
||||
// - Keepalive for long-lived connections
|
||||
func (o *StorageOptions) buildGrpcDialOptions() []grpc.DialOption {
|
||||
// Retry interceptor for transient connection issues (codes.Unavailable includes connection refused)
|
||||
retryInterceptor := grpc_retry.UnaryClientInterceptor(
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.5)),
|
||||
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
|
||||
)
|
||||
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithChainUnaryInterceptor(retryInterceptor),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
}),
|
||||
}
|
||||
|
||||
if o.GrpcClientKeepaliveTime > 0 {
|
||||
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: o.GrpcClientKeepaliveTime,
|
||||
Timeout: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}))
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -1073,13 +1073,6 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
},
|
||||
{
|
||||
Name: "unifiedStorageSearchSprinkles",
|
||||
Description: "Enable sprinkles on unified storage search",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "managedDualWriter",
|
||||
Description: "Pick the dual write mode from database configs",
|
||||
|
||||
@@ -409,7 +409,6 @@ lokiLabelNamesQueryApi,2024-12-13T14:31:41Z,,5ac7443fcec0db412d3333044a82c2c26b5
|
||||
kubernetesCliDashboards,2024-12-13T22:55:43Z,2025-02-18T23:11:26Z,8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4,Stephanie Hingtgen
|
||||
useV2DashboardsAPI,2024-12-17T21:17:09Z,2025-03-12T17:43:32Z,070f0e4457c5967102ef157197073dc2662f6fb8,Dominik Prokop
|
||||
investigationsBackend,2024-12-18T08:31:03Z,,f46c07aba7b6faccd2ecafc83051d1410cacc867,Jackson Coelho
|
||||
unifiedStorageSearchSprinkles,2024-12-18T17:00:54Z,,4837585cab0fd84184a8c6f5d6891f442a2b95f1,owensmallwood
|
||||
prometheusSpecialCharsInLabelValues,2024-12-18T21:31:08Z,,721c50a304588ebd7cea76e301ec0f68a5a55d68,Nick Richmond
|
||||
unifiedStorageSearchUI,2024-12-19T18:21:48Z,,a8f347144ddc16f2033fdeb4f3474e49239ba7ab,Scott Lepper
|
||||
playlistsReconciler,2024-12-20T03:09:31Z,,24bf337c562dc9b9d8684cc9acb7ea171ea83414,Charandas
|
||||
|
||||
|
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -148,7 +148,6 @@ alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true
|
||||
improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false
|
||||
useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false
|
||||
rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
|
||||
unifiedStorageSearchSprinkles,experimental,@grafana/search-and-storage,false,false,false
|
||||
managedDualWriter,experimental,@grafana/search-and-storage,false,false,false
|
||||
pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -455,10 +455,6 @@ const (
|
||||
// Enables the new role picker drawer design
|
||||
FlagRolePickerDrawer = "rolePickerDrawer"
|
||||
|
||||
// FlagUnifiedStorageSearchSprinkles
|
||||
// Enable sprinkles on unified storage search
|
||||
FlagUnifiedStorageSearchSprinkles = "unifiedStorageSearchSprinkles"
|
||||
|
||||
// FlagManagedDualWriter
|
||||
// Pick the dual write mode from database configs
|
||||
FlagManagedDualWriter = "managedDualWriter"
|
||||
|
||||
13
pkg/services/featuremgmt/toggles_gen.json
generated
13
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -3723,19 +3723,6 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "unifiedStorageSearchSprinkles",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-12-18T17:00:54Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable sprinkles on unified storage search",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage",
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "unifiedStorageSearchUI",
|
||||
|
||||
90
pkg/services/preference/generate_themes.go
Normal file
90
pkg/services/preference/generate_themes.go
Normal file
@@ -0,0 +1,90 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Colors struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type ThemeDefinition struct {
|
||||
Colors Colors `json:"colors"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
themesPath := filepath.Join("..", "..", "..", "packages", "grafana-data", "src", "themes", "themeDefinitions")
|
||||
|
||||
// Check if the themes directory exists
|
||||
if _, err := os.Stat(themesPath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Themes directory not found: %s\n", themesPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
output := `// Code generated by go generate; DO NOT EDIT.
|
||||
|
||||
package pref
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
`
|
||||
|
||||
err := filepath.WalkDir(themesPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only process json files
|
||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".json") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileBytes, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", path, readErr)
|
||||
return nil // Continue processing other files
|
||||
}
|
||||
|
||||
var themeDef ThemeDefinition
|
||||
jsonErr := json.Unmarshal(fileBytes, &themeDef)
|
||||
if jsonErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing JSON from %s: %v\n", path, jsonErr)
|
||||
return nil // Continue processing other files
|
||||
}
|
||||
|
||||
themeId := themeDef.Id
|
||||
themeType := "dark" // default fallback
|
||||
if themeDef.Colors.Mode != "" {
|
||||
themeType = themeDef.Colors.Mode
|
||||
}
|
||||
|
||||
output += fmt.Sprintf("\t{ID: %q, Type: %q, IsExtra: true},\n", themeId, themeType)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error walking themes directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
output += "}\n"
|
||||
|
||||
// Write the generated file
|
||||
outputPath := filepath.Join("themes_generated.go")
|
||||
if err := os.WriteFile(outputPath, []byte(output), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing output file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully generated themes_generated.go\n")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:generate go run generate_themes.go
|
||||
|
||||
package pref
|
||||
|
||||
type ThemeDTO struct {
|
||||
@@ -6,24 +8,6 @@ type ThemeDTO struct {
|
||||
IsExtra bool `json:"isExtra"`
|
||||
}
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
{ID: "debug", Type: "dark", IsExtra: true},
|
||||
{ID: "aubergine", Type: "dark", IsExtra: true},
|
||||
{ID: "desertbloom", Type: "light", IsExtra: true},
|
||||
{ID: "gildedgrove", Type: "dark", IsExtra: true},
|
||||
{ID: "mars", Type: "dark", IsExtra: true},
|
||||
{ID: "matrix", Type: "dark", IsExtra: true},
|
||||
{ID: "sapphiredusk", Type: "dark", IsExtra: true},
|
||||
{ID: "synthwave", Type: "dark", IsExtra: true},
|
||||
{ID: "tron", Type: "dark", IsExtra: true},
|
||||
{ID: "victorian", Type: "dark", IsExtra: true},
|
||||
{ID: "zen", Type: "light", IsExtra: true},
|
||||
{ID: "gloom", Type: "dark", IsExtra: true},
|
||||
}
|
||||
|
||||
func GetThemeByID(id string) *ThemeDTO {
|
||||
for _, theme := range themes {
|
||||
if theme.ID == id {
|
||||
|
||||
21
pkg/services/preference/themes_generated.go
Normal file
21
pkg/services/preference/themes_generated.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
|
||||
package pref
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
{ID: "aubergine", Type: "dark", IsExtra: true},
|
||||
{ID: "debug", Type: "dark", IsExtra: true},
|
||||
{ID: "desertbloom", Type: "light", IsExtra: true},
|
||||
{ID: "gildedgrove", Type: "dark", IsExtra: true},
|
||||
{ID: "gloom", Type: "dark", IsExtra: true},
|
||||
{ID: "mars", Type: "dark", IsExtra: true},
|
||||
{ID: "matrix", Type: "dark", IsExtra: true},
|
||||
{ID: "sapphiredusk", Type: "dark", IsExtra: true},
|
||||
{ID: "synthwave", Type: "dark", IsExtra: true},
|
||||
{ID: "tron", Type: "dark", IsExtra: true},
|
||||
{ID: "victorian", Type: "dark", IsExtra: true},
|
||||
{ID: "zen", Type: "light", IsExtra: true},
|
||||
}
|
||||
@@ -237,7 +237,6 @@ kubernetesFolders = true
|
||||
unifiedStorage = true
|
||||
unifiedStorageHistoryPruner = true
|
||||
unifiedStorageSearchPermissionFiltering = false
|
||||
unifiedStorageSearchSprinkles = false
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
@@ -315,9 +314,6 @@ To enable it, add the following to your `custom.ini` under the `[feature_toggles
|
||||
; Used by the Grafana instance
|
||||
unifiedStorageSearchUI = true
|
||||
|
||||
; (optional) Allows you to sort dashboards by usage insights fields when using enterprise
|
||||
; unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
; Used by unified storage server
|
||||
enable_search = true
|
||||
@@ -934,7 +930,6 @@ Unified Search requires several feature flags to be enabled depending on the des
|
||||
| Feature Flag | Purpose | Stage | Required For |
|
||||
|--------------|---------|-------|--------------|
|
||||
| `unifiedStorageSearchUI` | Frontend search interface | Experimental | Grafana UI search |
|
||||
| `unifiedStorageSearchSprinkles` | Usage insights integration | Experimental | Dashboard usage sorting (Enterprise) |
|
||||
| `unifiedStorageSearchDualReaderEnabled` | Shadow traffic to unified search | Experimental | Shadow traffic during migration |
|
||||
|
||||
#### Unified Search Specific Configuration
|
||||
@@ -955,9 +950,6 @@ unifiedStorageSearchUI = true
|
||||
; Enable shadow traffic during migration (optional)
|
||||
unifiedStorageSearchDualReaderEnabled = true
|
||||
|
||||
; Enable usage insights sorting (Enterprise only)
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
; Enable core search functionality (required)
|
||||
enable_search = true
|
||||
|
||||
@@ -271,7 +271,7 @@ func grpcConn(address string, metrics *clientMetrics, clientKeepaliveTime time.D
|
||||
retryCfg := retryConfig{
|
||||
Max: 3,
|
||||
Backoff: time.Second,
|
||||
BackoffJitter: 0.5,
|
||||
BackoffJitter: 0.1,
|
||||
}
|
||||
unary = append(unary, unaryRetryInterceptor(retryCfg))
|
||||
unary = append(unary, unaryRetryInstrument(metrics.requestRetries))
|
||||
@@ -288,13 +288,15 @@ func grpcConn(address string, metrics *clientMetrics, clientKeepaliveTime time.D
|
||||
opts = append(opts, grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// Use round_robin to balances requests more evenly over the available Storage server.
|
||||
// Use round_robin to balance requests more evenly over the available Storage server.
|
||||
opts = append(opts, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`))
|
||||
|
||||
// Disable looking up service config from TXT DNS records.
|
||||
// This reduces the number of requests made to the DNS servers.
|
||||
opts = append(opts, grpc.WithDisableServiceConfig())
|
||||
|
||||
opts = append(opts, connectionBackoffOptions())
|
||||
|
||||
if clientKeepaliveTime > 0 {
|
||||
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: clientKeepaliveTime,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
@@ -44,3 +45,17 @@ func unaryRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterce
|
||||
return invoker(ctx, method, req, resp, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// connectionBackoffOptions configures connection backoff parameters for faster recovery from
|
||||
// transient connection failures (e.g., during pod restarts).
|
||||
func connectionBackoffOptions() grpc.DialOption {
|
||||
return grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
16
public/app/api/clients/scope/v0alpha1/baseAPI.ts
Normal file
16
public/app/api/clients/scope/v0alpha1/baseAPI.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { getAPIBaseURL } from '@grafana/api-clients';
|
||||
import { createBaseQuery } from '@grafana/api-clients/rtkq';
|
||||
|
||||
export const API_GROUP = 'scope.grafana.app' as const;
|
||||
export const API_VERSION = 'v0alpha1' as const;
|
||||
export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION);
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'scopeAPIv0alpha1',
|
||||
baseQuery: createBaseQuery({
|
||||
baseURL: BASE_URL,
|
||||
}),
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
1727
public/app/api/clients/scope/v0alpha1/endpoints.gen.ts
generated
Normal file
1727
public/app/api/clients/scope/v0alpha1/endpoints.gen.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
public/app/api/clients/scope/v0alpha1/index.ts
Normal file
3
public/app/api/clients/scope/v0alpha1/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { generatedAPI } from './endpoints.gen';
|
||||
|
||||
export const scopeAPIv0alpha1 = generatedAPI;
|
||||
43
public/app/api/clients/scope/v0alpha1/sync-from-enterprise.sh
Executable file
43
public/app/api/clients/scope/v0alpha1/sync-from-enterprise.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# Syncs the scope API client from Enterprise to OSS.
|
||||
#
|
||||
# This script:
|
||||
# 1. Regenerates the Enterprise API client from the OpenAPI spec
|
||||
# 2. Copies the generated endpoints.gen.ts to OSS
|
||||
#
|
||||
# Prerequisites:
|
||||
# - The OpenAPI spec must exist at data/openapi/scope.grafana.app-v0alpha1.json
|
||||
# (generated by running TestIntegrationOpenAPIs in pkg/extensions/apiserver/tests/)
|
||||
#
|
||||
# Usage: ./sync-from-enterprise.sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GRAFANA_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)"
|
||||
|
||||
# Source and destination directories for the generated API client
|
||||
ENTERPRISE_SCOPE_API_DIR="$GRAFANA_ROOT/public/app/extensions/api/clients/scope/v0alpha1"
|
||||
OSS_SCOPE_API_DIR="$SCRIPT_DIR"
|
||||
|
||||
cd "$GRAFANA_ROOT"
|
||||
|
||||
# Check if OpenAPI spec exists
|
||||
if [ ! -f "data/openapi/scope.grafana.app-v0alpha1.json" ]; then
|
||||
echo "Error: OpenAPI spec not found at data/openapi/scope.grafana.app-v0alpha1.json"
|
||||
echo "Run TestIntegrationOpenAPIs in pkg/extensions/apiserver/tests/ to generate it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Generating Enterprise API client from OpenAPI spec..."
|
||||
yarn workspace @grafana/api-clients process-specs && npx rtk-query-codegen-openapi ./local/generate-enterprise-apis.ts
|
||||
|
||||
if [ ! -f "$ENTERPRISE_SCOPE_API_DIR/endpoints.gen.ts" ]; then
|
||||
echo "Error: Enterprise endpoints.gen.ts not found after generation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 2: Copying endpoints.gen.ts from Enterprise to OSS..."
|
||||
cp "$ENTERPRISE_SCOPE_API_DIR/endpoints.gen.ts" "$OSS_SCOPE_API_DIR/endpoints.gen.ts"
|
||||
|
||||
echo "Done! Scope API client synced from Enterprise."
|
||||
@@ -3,6 +3,7 @@ import { AnyAction, combineReducers } from 'redux';
|
||||
|
||||
import { allReducers as allApiClientReducers } from '@grafana/api-clients/rtkq';
|
||||
import { generatedAPI as legacyAPI } from '@grafana/api-clients/rtkq/legacy';
|
||||
import { scopeAPIv0alpha1 } from 'app/api/clients/scope/v0alpha1';
|
||||
import sharedReducers from 'app/core/reducers';
|
||||
import ldapReducers from 'app/features/admin/state/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
@@ -52,6 +53,7 @@ const rootReducers = {
|
||||
[alertingApi.reducerPath]: alertingApi.reducer,
|
||||
[publicDashboardApi.reducerPath]: publicDashboardApi.reducer,
|
||||
[browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer,
|
||||
[scopeAPIv0alpha1.reducerPath]: scopeAPIv0alpha1.reducer,
|
||||
...allApiClientReducers,
|
||||
};
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export function useCombinedLabels(
|
||||
// This is called by Combobox when the dropdown menu opens
|
||||
const createAsyncValuesLoader = useCallback(
|
||||
(key: string): AsyncOptionsLoader => {
|
||||
return async (_inputValue: string): Promise<Array<ComboboxOption<string>>> => {
|
||||
return async (valueQuery: string): Promise<Array<ComboboxOption<string>>> => {
|
||||
if (!isKeyAllowed(key) || !key) {
|
||||
return [];
|
||||
}
|
||||
@@ -188,7 +188,10 @@ export function useCombinedLabels(
|
||||
// Combine: existing values first, then unique ops values (Set preserves first occurrence)
|
||||
const combinedValues = [...new Set([...existingValues, ...opsValues])];
|
||||
|
||||
return mapLabelsToOptions(combinedValues);
|
||||
const valueQueryLowerCase = valueQuery.toLowerCase();
|
||||
const filteredValues = combinedValues.filter((value) => value.toLowerCase().includes(valueQueryLowerCase));
|
||||
|
||||
return mapLabelsToOptions(filteredValues);
|
||||
};
|
||||
},
|
||||
[labelsByKeyFromExisingAlerts, labelsPluginInstalled, opsLabelKeysSet, fetchLabelValues]
|
||||
|
||||
@@ -6,24 +6,40 @@ import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { mockAlertRuleApi, setupMswServer } from '../../../mockApi';
|
||||
import { getGrafanaRule } from '../../../mocks';
|
||||
import {
|
||||
defaultLabelValues,
|
||||
getLabelValuesHandler,
|
||||
getMockOpsLabels,
|
||||
} from '../../../mocks/server/handlers/plugins/grafana-labels-app';
|
||||
import { getMockOpsLabels } from '../../../mocks/server/handlers/plugins/grafana-labels-app';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
|
||||
import { LabelsWithSuggestions } from './LabelsField';
|
||||
|
||||
// Mock getBoundingClientRect for @tanstack/react-virtual to calculate visible items
|
||||
// The global ResizeObserver mock in jest-setup.ts handles subsequent measurements
|
||||
Element.prototype.getBoundingClientRect = jest.fn(() => ({
|
||||
width: 200,
|
||||
height: 400,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 400,
|
||||
right: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
}));
|
||||
|
||||
// Existing labels in the form (simulating editing an existing alert rule with ops labels)
|
||||
const existingOpsLabels = getMockOpsLabels();
|
||||
|
||||
const SubFormProviderWrapper = ({
|
||||
// Wrapper that provides portal container for Combobox dropdowns
|
||||
const TestWrapper = ({
|
||||
children,
|
||||
labels,
|
||||
}: React.PropsWithChildren<{ labels: Array<{ key: string; value: string }> }>) => {
|
||||
const methods = useForm({ defaultValues: { labelsInSubform: labels } });
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>{children}</FormProvider>
|
||||
<div id="grafana-portal-container" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const grafanaRule = getGrafanaRule(undefined, {
|
||||
@@ -64,9 +80,9 @@ describe('LabelsField with ops labels', () => {
|
||||
|
||||
async function renderLabelsWithOpsLabels(labels = existingOpsLabels) {
|
||||
const view = render(
|
||||
<SubFormProviderWrapper labels={labels}>
|
||||
<TestWrapper labels={labels}>
|
||||
<LabelsWithSuggestions dataSourceName="grafana" />
|
||||
</SubFormProviderWrapper>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Wait for the dropdowns to be rendered
|
||||
@@ -221,41 +237,84 @@ describe('LabelsField with ops labels', () => {
|
||||
expect(combobox).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
// Test that opening the value dropdown requests values for the CORRECT label key
|
||||
// This verifies the async loader is called with the right key
|
||||
it('should request correct label values when opening value dropdown', async () => {
|
||||
const requestedKeys: string[] = [];
|
||||
|
||||
// Add a spy handler that tracks which keys are requested
|
||||
server.use(getLabelValuesHandler(defaultLabelValues, (key) => requestedKeys.push(key)));
|
||||
|
||||
// Test that opening the value dropdown shows values for the CORRECT label key
|
||||
// This verifies the async loader is called with the right key and renders the correct options
|
||||
it('should show correct label values when opening value dropdown', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Open the first label's value dropdown (sentMail)
|
||||
// Expected values: "true", "false"
|
||||
const firstValueDropdown = within(screen.getByTestId('labelsInSubform-value-0'));
|
||||
await user.click(firstValueDropdown.getByRole('combobox'));
|
||||
|
||||
// Wait for the API call to be made
|
||||
await waitFor(() => {
|
||||
expect(requestedKeys).toContain('sentMail');
|
||||
});
|
||||
// Wait for sentMail values to appear
|
||||
const trueOption = await screen.findByRole('option', { name: /true/i });
|
||||
expect(trueOption).toBeInTheDocument();
|
||||
|
||||
// Close dropdown
|
||||
await user.keyboard('{Escape}');
|
||||
// Verify we have exactly 2 options for sentMail (true, false)
|
||||
const firstDropdownOptions = screen.getAllByRole('option');
|
||||
expect(firstDropdownOptions).toHaveLength(2);
|
||||
expect(firstDropdownOptions[0]).toHaveTextContent('true');
|
||||
expect(firstDropdownOptions[1]).toHaveTextContent('false');
|
||||
|
||||
// Clear the tracked keys
|
||||
requestedKeys.length = 0;
|
||||
// Close dropdown by clicking outside (simulate real user behavior)
|
||||
await user.click(document.body);
|
||||
|
||||
// Open the second label's value dropdown (stage)
|
||||
// Expected values: "production", "staging", "development"
|
||||
const secondValueDropdown = within(screen.getByTestId('labelsInSubform-value-1'));
|
||||
await user.click(secondValueDropdown.getByRole('combobox'));
|
||||
|
||||
// Wait for the API call - should request 'stage', NOT 'sentMail'
|
||||
await waitFor(() => {
|
||||
expect(requestedKeys).toContain('stage');
|
||||
});
|
||||
// Wait for stage values to appear
|
||||
const productionOption = await screen.findByRole('option', { name: /production/i });
|
||||
expect(productionOption).toBeInTheDocument();
|
||||
|
||||
// Verify we didn't request the wrong key (the bug from escalation #19378)
|
||||
expect(requestedKeys).not.toContain('sentMail');
|
||||
// Verify we have exactly 3 options for stage (production, staging, development)
|
||||
// This ensures we're NOT showing sentMail values
|
||||
const secondDropdownOptions = screen.getAllByRole('option');
|
||||
expect(secondDropdownOptions).toHaveLength(3);
|
||||
expect(secondDropdownOptions[0]).toHaveTextContent('production');
|
||||
expect(secondDropdownOptions[1]).toHaveTextContent('staging');
|
||||
expect(secondDropdownOptions[2]).toHaveTextContent('development');
|
||||
});
|
||||
|
||||
// Test that typing in the value dropdown filters options (search functionality)
|
||||
it('should filter value options when typing in the combobox', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Add a new label with "stage" key which has multiple values: production, staging, development
|
||||
const addMoreButton = await screen.findByText('Add more');
|
||||
await user.click(addMoreButton);
|
||||
|
||||
// First, set the key to "stage"
|
||||
const keyDropdown = within(screen.getByTestId('labelsInSubform-key-2'));
|
||||
await user.type(keyDropdown.getByRole('combobox'), 'stage{enter}');
|
||||
|
||||
// Wait for the key to be set
|
||||
const keyInput = screen.getByTestId('labelsInSubform-key-2').querySelector('input');
|
||||
await waitFor(() => expect(keyInput).toHaveValue('stage'));
|
||||
|
||||
const valueDropdown = within(screen.getByTestId('labelsInSubform-value-2'));
|
||||
const combobox = valueDropdown.getByRole('combobox');
|
||||
|
||||
// Type "stag" which should filter to only "staging" (not "production" or "development")
|
||||
await user.type(combobox, 'stag');
|
||||
|
||||
// Wait for the staging option to appear (allows for debounce + async load)
|
||||
const stagingOption = await screen.findByRole('option', { name: /staging/i });
|
||||
expect(stagingOption).toBeInTheDocument();
|
||||
|
||||
// Verify we have exactly 2 options:
|
||||
// 1. "stag" - Use custom value (created because user typed custom text)
|
||||
// 2. "staging" - The filtered match from available values
|
||||
const allOptions = screen.getAllByRole('option');
|
||||
expect(allOptions).toHaveLength(2);
|
||||
expect(allOptions[0]).toHaveTextContent('stag');
|
||||
expect(allOptions[0]).toHaveTextContent('Use custom value');
|
||||
expect(allOptions[1]).toHaveTextContent('staging');
|
||||
|
||||
// Verify that "production" and "development" are NOT shown (they don't match "stag")
|
||||
expect(screen.queryByRole('option', { name: /^production$/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('option', { name: /^development$/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,74 +1,194 @@
|
||||
import { getBackendSrv, config } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { MOCK_NODES, MOCK_SCOPES } from '@grafana/test-utils/unstable';
|
||||
import { scopeAPIv0alpha1 } from 'app/api/clients/scope/v0alpha1';
|
||||
|
||||
import { ScopesApiClient } from './ScopesApiClient';
|
||||
|
||||
// Mock the runtime dependencies
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: jest.fn(),
|
||||
config: {
|
||||
featureToggles: {
|
||||
useMultipleScopeNodesEndpoint: true,
|
||||
useScopeSingleNodeEndpoint: true,
|
||||
// Helper to create a mock subscription with unsubscribe method
|
||||
const createMockSubscription = <T>(data: T): Promise<T> & { unsubscribe: jest.Mock } => {
|
||||
const subscription = Promise.resolve(data) as Promise<T> & { unsubscribe: jest.Mock };
|
||||
subscription.unsubscribe = jest.fn();
|
||||
return subscription;
|
||||
};
|
||||
|
||||
// Mock the RTK Query API and dispatch
|
||||
jest.mock('app/api/clients/scope/v0alpha1', () => ({
|
||||
scopeAPIv0alpha1: {
|
||||
endpoints: {
|
||||
getScope: {
|
||||
initiate: jest.fn(),
|
||||
},
|
||||
getScopeNode: {
|
||||
initiate: jest.fn(),
|
||||
},
|
||||
getFindScopeNodeChildrenResults: {
|
||||
initiate: jest.fn(),
|
||||
},
|
||||
getFindScopeDashboardBindingsResults: {
|
||||
initiate: jest.fn(),
|
||||
},
|
||||
getFindScopeNavigationsResults: {
|
||||
initiate: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/api-clients', () => ({
|
||||
getAPIBaseURL: jest.fn().mockReturnValue('/apis/scope.grafana.app/v0alpha1'),
|
||||
jest.mock('app/store/store', () => ({
|
||||
dispatch: jest.fn((action) => action),
|
||||
}));
|
||||
|
||||
describe('ScopesApiClient', () => {
|
||||
let apiClient: ScopesApiClient;
|
||||
let mockBackendSrv: jest.Mocked<{ get: jest.Mock }>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockBackendSrv = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
(getBackendSrv as jest.Mock).mockReturnValue(mockBackendSrv);
|
||||
apiClient = new ScopesApiClient();
|
||||
config.featureToggles.useMultipleScopeNodesEndpoint = true;
|
||||
config.featureToggles.useScopeSingleNodeEndpoint = true;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchScope', () => {
|
||||
it('should fetch a scope by name', async () => {
|
||||
// Expected: MOCK_SCOPES contains a scope with name 'grafana'
|
||||
const expectedScope = MOCK_SCOPES.find((s) => s.metadata.name === 'grafana');
|
||||
expect(expectedScope).toBeDefined();
|
||||
|
||||
const mockSubscription = createMockSubscription({ data: expectedScope });
|
||||
(scopeAPIv0alpha1.endpoints.getScope.initiate as jest.Mock).mockReturnValue(mockSubscription);
|
||||
|
||||
const result = await apiClient.fetchScope('grafana');
|
||||
|
||||
// Validate: result matches the expected scope from MOCK_SCOPES
|
||||
expect(result).toEqual(expectedScope);
|
||||
expect(scopeAPIv0alpha1.endpoints.getScope.initiate).toHaveBeenCalledWith(
|
||||
{ name: 'grafana' },
|
||||
{ subscribe: false }
|
||||
);
|
||||
});
|
||||
|
||||
it('should return undefined when scope is not found', async () => {
|
||||
// Expected: No scope with this name exists in MOCK_SCOPES
|
||||
const nonExistentScopeName = 'non-existent-scope';
|
||||
const errorResponse = {
|
||||
kind: 'Status',
|
||||
apiVersion: 'v1',
|
||||
status: 'Failure',
|
||||
message: `scopes.scope.grafana.app "${nonExistentScopeName}" not found`,
|
||||
code: 404,
|
||||
};
|
||||
const mockSubscription = createMockSubscription({ data: errorResponse });
|
||||
(scopeAPIv0alpha1.endpoints.getScope.initiate as jest.Mock).mockReturnValue(mockSubscription);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const result = await apiClient.fetchScope(nonExistentScopeName);
|
||||
|
||||
// Validate: returns undefined for non-existent scope
|
||||
expect(result).toBeUndefined();
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchMultipleScopes', () => {
|
||||
it('should fetch multiple scopes in parallel', async () => {
|
||||
// Expected: Both 'grafana' and 'mimir' exist in MOCK_SCOPES
|
||||
const scopeNames = ['grafana', 'mimir'];
|
||||
const expectedScopes = MOCK_SCOPES.filter((s) => scopeNames.includes(s.metadata.name));
|
||||
|
||||
const mockSubscriptions = expectedScopes.map((scope) => createMockSubscription({ data: scope }));
|
||||
(scopeAPIv0alpha1.endpoints.getScope.initiate as jest.Mock)
|
||||
.mockReturnValueOnce(mockSubscriptions[0])
|
||||
.mockReturnValueOnce(mockSubscriptions[1]);
|
||||
|
||||
const result = await apiClient.fetchMultipleScopes(scopeNames);
|
||||
|
||||
// Validate: returns both scopes from MOCK_SCOPES
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((s) => s.metadata.name)).toContain('grafana');
|
||||
expect(result.map((s) => s.metadata.name)).toContain('mimir');
|
||||
expect(result).toEqual(expect.arrayContaining(expectedScopes));
|
||||
});
|
||||
|
||||
it('should filter out undefined scopes when some fail', async () => {
|
||||
// Expected: 'grafana' exists in MOCK_SCOPES, 'non-existent' does not
|
||||
const scopeNames = ['grafana', 'non-existent'];
|
||||
const expectedScope = MOCK_SCOPES.find((s) => s.metadata.name === 'grafana');
|
||||
const errorResponse = {
|
||||
kind: 'Status',
|
||||
apiVersion: 'v1',
|
||||
status: 'Failure',
|
||||
message: 'scopes.scope.grafana.app "non-existent" not found',
|
||||
code: 404,
|
||||
};
|
||||
|
||||
const mockSubscriptions = [
|
||||
createMockSubscription({ data: expectedScope }),
|
||||
createMockSubscription({ data: errorResponse }),
|
||||
];
|
||||
(scopeAPIv0alpha1.endpoints.getScope.initiate as jest.Mock)
|
||||
.mockReturnValueOnce(mockSubscriptions[0])
|
||||
.mockReturnValueOnce(mockSubscriptions[1]);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
const result = await apiClient.fetchMultipleScopes(scopeNames);
|
||||
|
||||
// Validate: only returns the existing scope from MOCK_SCOPES, filters out the non-existent one
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual(expectedScope);
|
||||
expect(result[0].metadata.name).toBe('grafana');
|
||||
// Validate: console.warn is called when some scopes fail
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
consoleErrorSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return empty array when no scopes provided', async () => {
|
||||
const result = await apiClient.fetchMultipleScopes([]);
|
||||
|
||||
// Validate: empty input returns empty array
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchMultipleScopeNodes', () => {
|
||||
it('should fetch multiple nodes by names', async () => {
|
||||
const mockNodes = [
|
||||
{
|
||||
metadata: { name: 'node-1' },
|
||||
spec: { nodeType: 'container', title: 'Node 1', parentName: '' },
|
||||
},
|
||||
{
|
||||
metadata: { name: 'node-2' },
|
||||
spec: { nodeType: 'leaf', title: 'Node 2', parentName: 'node-1' },
|
||||
},
|
||||
];
|
||||
// Expected: Both nodes exist in MOCK_NODES
|
||||
const nodeNames = ['applications-grafana', 'applications-mimir'];
|
||||
const expectedNodes = MOCK_NODES.filter((n) => nodeNames.includes(n.metadata.name));
|
||||
|
||||
mockBackendSrv.get.mockResolvedValue({ items: mockNodes });
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(['node-1', 'node-2']);
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
names: ['node-1', 'node-2'],
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: expectedNodes },
|
||||
});
|
||||
expect(result).toEqual(mockNodes);
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(nodeNames);
|
||||
|
||||
// Validate: returns the expected nodes from MOCK_NODES
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((n) => n.metadata.name)).toContain('applications-grafana');
|
||||
expect(result.map((n) => n.metadata.name)).toContain('applications-mimir');
|
||||
expect(result).toEqual(expect.arrayContaining(expectedNodes));
|
||||
});
|
||||
|
||||
it('should return empty array when names array is empty', async () => {
|
||||
const result = await apiClient.fetchMultipleScopeNodes([]);
|
||||
|
||||
expect(mockBackendSrv.get).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when feature toggle is disabled', async () => {
|
||||
config.featureToggles.useMultipleScopeNodesEndpoint = false;
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(['node-1']);
|
||||
const result = await apiClient.fetchMultipleScopeNodes(['applications-grafana']);
|
||||
|
||||
expect(mockBackendSrv.get).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([]);
|
||||
|
||||
// Restore feature toggle
|
||||
@@ -76,79 +196,94 @@ describe('ScopesApiClient', () => {
|
||||
});
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
mockBackendSrv.get.mockRejectedValue(new Error('Network error'));
|
||||
// Expected: No node with this name exists in MOCK_NODES
|
||||
const nonExistentNodeName = 'non-existent-node';
|
||||
const mockSubscription = createMockSubscription({ data: { items: [] } });
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(['node-1']);
|
||||
const result = await apiClient.fetchMultipleScopeNodes([nonExistentNodeName]);
|
||||
|
||||
// Validate: returns empty array when no matches
|
||||
expect(result).toEqual([]);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle response with no items field', async () => {
|
||||
mockBackendSrv.get.mockResolvedValue({});
|
||||
// Expected: Node exists in MOCK_NODES
|
||||
const nodeName = 'applications-grafana';
|
||||
const mockSubscription = createMockSubscription({ data: {} });
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(['node-1']);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle response with null items', async () => {
|
||||
mockBackendSrv.get.mockResolvedValue({ items: null });
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(['node-1']);
|
||||
const result = await apiClient.fetchMultipleScopeNodes([nodeName]);
|
||||
|
||||
// Validate: returns empty array when items field is missing
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle large arrays of node names', async () => {
|
||||
const names = Array.from({ length: 100 }, (_, i) => `node-${i}`);
|
||||
const mockNodes = names.map((name) => ({
|
||||
metadata: { name },
|
||||
spec: { nodeType: 'leaf', title: name, parentName: '' },
|
||||
}));
|
||||
// Expected: None of these node names exist in MOCK_NODES
|
||||
const nonExistentNodeNames = Array.from({ length: 10 }, (_, i) => `node-${i}`);
|
||||
const mockSubscription = createMockSubscription({ data: { items: [] } });
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
mockBackendSrv.get.mockResolvedValue({ items: mockNodes });
|
||||
const result = await apiClient.fetchMultipleScopeNodes(nonExistentNodeNames);
|
||||
|
||||
const result = await apiClient.fetchMultipleScopeNodes(names);
|
||||
|
||||
expect(result).toEqual(mockNodes);
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
names,
|
||||
});
|
||||
// Validate: returns empty array when no matches
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should pass through node names exactly as provided', async () => {
|
||||
const names = ['node-with-special-chars_123', 'node.with.dots', 'node-with-dashes'];
|
||||
mockBackendSrv.get.mockResolvedValue({ items: [] });
|
||||
// Expected: Both nodes exist in MOCK_NODES
|
||||
const nodeNames = ['applications-grafana', 'applications-mimir'];
|
||||
const expectedNodes = MOCK_NODES.filter((n) => nodeNames.includes(n.metadata.name));
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: expectedNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
await apiClient.fetchMultipleScopeNodes(names);
|
||||
const result = await apiClient.fetchMultipleScopeNodes(nodeNames);
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
names,
|
||||
// Validate: returns nodes matching the provided names
|
||||
const resultNames = result.map((n) => n.metadata.name);
|
||||
expect(resultNames).toEqual(expect.arrayContaining(nodeNames));
|
||||
// Verify we got the expected nodes from MOCK_NODES
|
||||
expectedNodes.forEach((expectedNode) => {
|
||||
expect(result).toContainEqual(expectedNode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchScopeNode', () => {
|
||||
it('should fetch a single scope node by ID', async () => {
|
||||
const mockNode = {
|
||||
metadata: { name: 'test-node' },
|
||||
spec: { nodeType: 'leaf', title: 'Test Node', parentName: 'parent' },
|
||||
};
|
||||
// Expected: Node exists in MOCK_NODES
|
||||
const nodeName = 'applications-grafana';
|
||||
const expectedNode = MOCK_NODES.find((n) => n.metadata.name === nodeName);
|
||||
expect(expectedNode).toBeDefined();
|
||||
|
||||
mockBackendSrv.get.mockResolvedValue(mockNode);
|
||||
const mockSubscription = createMockSubscription({ data: expectedNode });
|
||||
(scopeAPIv0alpha1.endpoints.getScopeNode.initiate as jest.Mock).mockReturnValue(mockSubscription);
|
||||
|
||||
const result = await apiClient.fetchScopeNode('test-node');
|
||||
const result = await apiClient.fetchScopeNode(nodeName);
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/scopenodes/test-node');
|
||||
expect(result).toEqual(mockNode);
|
||||
// Validate: result matches the expected node from MOCK_NODES
|
||||
expect(result).toEqual(expectedNode);
|
||||
});
|
||||
|
||||
it('should return undefined when feature toggle is disabled', async () => {
|
||||
config.featureToggles.useScopeSingleNodeEndpoint = false;
|
||||
|
||||
const result = await apiClient.fetchScopeNode('test-node');
|
||||
const result = await apiClient.fetchScopeNode('applications-grafana');
|
||||
|
||||
expect(mockBackendSrv.get).not.toHaveBeenCalled();
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
// Restore feature toggle
|
||||
@@ -156,65 +291,95 @@ describe('ScopesApiClient', () => {
|
||||
});
|
||||
|
||||
it('should return undefined on API error', async () => {
|
||||
mockBackendSrv.get.mockRejectedValue(new Error('Not found'));
|
||||
// Expected: No node with this name exists in MOCK_NODES
|
||||
const nonExistentNodeName = 'non-existent-node';
|
||||
const errorResponse = {
|
||||
kind: 'Status',
|
||||
apiVersion: 'v1',
|
||||
status: 'Failure',
|
||||
message: `scopenodes.scope.grafana.app "${nonExistentNodeName}" not found`,
|
||||
code: 404,
|
||||
};
|
||||
const mockSubscription = createMockSubscription({ data: errorResponse });
|
||||
(scopeAPIv0alpha1.endpoints.getScopeNode.initiate as jest.Mock).mockReturnValue(mockSubscription);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const result = await apiClient.fetchScopeNode('non-existent');
|
||||
const result = await apiClient.fetchScopeNode(nonExistentNodeName);
|
||||
|
||||
// Validate: returns undefined for non-existent node
|
||||
expect(result).toBeUndefined();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchNodes', () => {
|
||||
it('should fetch nodes with parent filter', async () => {
|
||||
const mockNodes = [
|
||||
{
|
||||
metadata: { name: 'child-1' },
|
||||
spec: { nodeType: 'leaf', title: 'Child 1', parentName: 'parent' },
|
||||
},
|
||||
];
|
||||
// Expected: MOCK_NODES contains nodes with parentName 'applications'
|
||||
const parentName = 'applications';
|
||||
const expectedNodes = MOCK_NODES.filter((n) => n.spec.parentName === parentName);
|
||||
|
||||
mockBackendSrv.get.mockResolvedValue({ items: mockNodes });
|
||||
|
||||
const result = await apiClient.fetchNodes({ parent: 'parent' });
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
parent: 'parent',
|
||||
query: undefined,
|
||||
limit: 1000,
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: expectedNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchNodes({ parent: parentName });
|
||||
|
||||
// Validate: returns nodes with matching parentName from MOCK_NODES
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach((node) => {
|
||||
expect(node.spec.parentName).toBe(parentName);
|
||||
});
|
||||
// Verify all returned nodes are from the expected set
|
||||
result.forEach((node) => {
|
||||
expect(expectedNodes).toContainEqual(node);
|
||||
});
|
||||
expect(result).toEqual(mockNodes);
|
||||
});
|
||||
|
||||
it('should fetch nodes with query filter', async () => {
|
||||
const mockNodes = [
|
||||
{
|
||||
metadata: { name: 'matching-node' },
|
||||
spec: { nodeType: 'leaf', title: 'Matching Node', parentName: '' },
|
||||
},
|
||||
];
|
||||
// Expected: MOCK_NODES contains nodes with 'Grafana' in title (case-insensitive)
|
||||
// When query is provided without parent, the API returns nodes matching the query
|
||||
// In MOCK_NODES, nodes with 'Grafana' in title have parentName 'applications' or 'cloud-applications'
|
||||
const query = 'Grafana';
|
||||
const expectedNodes = MOCK_NODES.filter((n) => n.spec.title.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
mockBackendSrv.get.mockResolvedValue({ items: mockNodes });
|
||||
|
||||
const result = await apiClient.fetchNodes({ query: 'matching' });
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
parent: undefined,
|
||||
query: 'matching',
|
||||
limit: 1000,
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: expectedNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchNodes({ query });
|
||||
|
||||
// Validate: returns nodes matching the query from MOCK_NODES
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach((node) => {
|
||||
expect(node.spec.title.toLowerCase()).toContain('grafana');
|
||||
});
|
||||
// Verify all returned nodes are from the expected set
|
||||
result.forEach((node) => {
|
||||
expect(expectedNodes).toContainEqual(node);
|
||||
});
|
||||
expect(result).toEqual(mockNodes);
|
||||
});
|
||||
|
||||
it('should respect custom limit', async () => {
|
||||
mockBackendSrv.get.mockResolvedValue({ items: [] });
|
||||
|
||||
await apiClient.fetchNodes({ limit: 50 });
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
parent: undefined,
|
||||
query: undefined,
|
||||
limit: 50,
|
||||
const limit = 5;
|
||||
const mockNodes = MOCK_NODES.slice(0, limit);
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: mockNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchNodes({ limit });
|
||||
|
||||
expect(result.length).toBeLessThanOrEqual(limit);
|
||||
});
|
||||
|
||||
it('should throw error for invalid limit (too small)', async () => {
|
||||
@@ -226,137 +391,297 @@ describe('ScopesApiClient', () => {
|
||||
});
|
||||
|
||||
it('should use default limit of 1000 when not specified', async () => {
|
||||
mockBackendSrv.get.mockResolvedValue({ items: [] });
|
||||
|
||||
await apiClient.fetchNodes({});
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/find/scope_node_children', {
|
||||
parent: undefined,
|
||||
query: undefined,
|
||||
limit: 1000,
|
||||
const mockNodes = MOCK_NODES.slice(0, 1000);
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: mockNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchNodes({});
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// Default limit is 1000, so result should not exceed that
|
||||
expect(result.length).toBeLessThanOrEqual(1000);
|
||||
});
|
||||
|
||||
it('should return empty array on API error', async () => {
|
||||
mockBackendSrv.get.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const result = await apiClient.fetchNodes({ parent: 'test' });
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchScope', () => {
|
||||
it('should fetch a scope by name', async () => {
|
||||
const mockScope = {
|
||||
metadata: { name: 'test-scope' },
|
||||
spec: {
|
||||
title: 'Test Scope',
|
||||
filters: [],
|
||||
},
|
||||
};
|
||||
|
||||
mockBackendSrv.get.mockResolvedValue(mockScope);
|
||||
|
||||
const result = await apiClient.fetchScope('test-scope');
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledWith('/apis/scope.grafana.app/v0alpha1/scopes/test-scope');
|
||||
expect(result).toEqual(mockScope);
|
||||
});
|
||||
|
||||
it('should return undefined on error', async () => {
|
||||
const mockSubscription = createMockSubscription({ data: { items: [] } });
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
mockBackendSrv.get.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const result = await apiClient.fetchScope('non-existent');
|
||||
const result = await apiClient.fetchNodes({ parent: 'non-existent-parent' });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should log error to console', async () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const error = new Error('Not found');
|
||||
mockBackendSrv.get.mockRejectedValue(error);
|
||||
it('should combine parent and query filters', async () => {
|
||||
// Expected: MOCK_NODES contains nodes with parentName 'applications' and 'Grafana' in title
|
||||
const parentName = 'applications';
|
||||
const query = 'Grafana';
|
||||
const expectedNodes = MOCK_NODES.filter(
|
||||
(n) => n.spec.parentName === parentName && n.spec.title.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
await apiClient.fetchScope('non-existent');
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: expectedNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(error);
|
||||
consoleErrorSpy.mockRestore();
|
||||
const result = await apiClient.fetchNodes({ parent: parentName, query });
|
||||
|
||||
// Validate: returns nodes matching both filters from MOCK_NODES
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
result.forEach((node) => {
|
||||
expect(node.spec.parentName).toBe(parentName);
|
||||
expect(node.spec.title.toLowerCase()).toContain('grafana');
|
||||
});
|
||||
// Verify all returned nodes are from the expected set
|
||||
result.forEach((node) => {
|
||||
expect(expectedNodes).toContainEqual(node);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchMultipleScopes', () => {
|
||||
it('should fetch multiple scopes in parallel', async () => {
|
||||
const mockScopes = [
|
||||
describe('fetchDashboards', () => {
|
||||
it('should fetch dashboards for scopes', async () => {
|
||||
// Expected: MOCK_SCOPE_DASHBOARD_BINDINGS contains bindings for 'grafana' scope
|
||||
const scopeNames = ['grafana'];
|
||||
const mockBindings = [
|
||||
{
|
||||
metadata: { name: 'scope-1' },
|
||||
spec: { title: 'Scope 1', filters: [] },
|
||||
},
|
||||
{
|
||||
metadata: { name: 'scope-2' },
|
||||
spec: { title: 'Scope 2', filters: [] },
|
||||
metadata: { name: 'grafana-binding-1' },
|
||||
spec: { dashboard: 'dashboard-1', scope: 'grafana' },
|
||||
status: { dashboardTitle: 'Dashboard 1' },
|
||||
},
|
||||
];
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: mockBindings },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeDashboardBindingsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
mockBackendSrv.get.mockResolvedValueOnce(mockScopes[0]).mockResolvedValueOnce(mockScopes[1]);
|
||||
const result = await apiClient.fetchDashboards(scopeNames);
|
||||
|
||||
const result = await apiClient.fetchMultipleScopes(['scope-1', 'scope-2']);
|
||||
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual(mockScopes);
|
||||
// Validate: returns bindings for the requested scope
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
result.forEach((binding) => {
|
||||
expect(binding.spec.scope).toBe('grafana');
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out undefined scopes', async () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
const mockScope = {
|
||||
metadata: { name: 'scope-1' },
|
||||
spec: { title: 'Scope 1', filters: [] },
|
||||
};
|
||||
it('should fetch dashboards for multiple scopes', async () => {
|
||||
// Expected: MOCK_SCOPE_DASHBOARD_BINDINGS contains bindings for 'grafana' and 'mimir' scopes
|
||||
const scopeNames = ['grafana', 'mimir'];
|
||||
const mockBindings = [
|
||||
{
|
||||
metadata: { name: 'grafana-binding-1' },
|
||||
spec: { dashboard: 'dashboard-1', scope: 'grafana' },
|
||||
status: { dashboardTitle: 'Dashboard 1' },
|
||||
},
|
||||
{
|
||||
metadata: { name: 'mimir-binding-1' },
|
||||
spec: { dashboard: 'dashboard-2', scope: 'mimir' },
|
||||
status: { dashboardTitle: 'Dashboard 2' },
|
||||
},
|
||||
];
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: mockBindings },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeDashboardBindingsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
mockBackendSrv.get.mockResolvedValueOnce(mockScope).mockRejectedValueOnce(new Error('Not found'));
|
||||
const result = await apiClient.fetchDashboards(scopeNames);
|
||||
|
||||
const result = await apiClient.fetchMultipleScopes(['scope-1', 'non-existent']);
|
||||
|
||||
expect(result).toEqual([mockScope]);
|
||||
consoleErrorSpy.mockRestore();
|
||||
// Validate: returns bindings for either scope
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
result.forEach((binding) => {
|
||||
expect(scopeNames).toContain(binding.spec.scope);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when no scopes provided', async () => {
|
||||
const result = await apiClient.fetchMultipleScopes([]);
|
||||
it('should return empty array when no dashboards found', async () => {
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: [] },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeDashboardBindingsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchDashboards(['non-existent-scope']);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockBackendSrv.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: [] },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeDashboardBindingsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const result = await apiClient.fetchDashboards(['grafana']);
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchScopeNavigations', () => {
|
||||
it('should fetch navigations for scopes', async () => {
|
||||
// Expected: MSW handler returns MOCK_SUB_SCOPE_MIMIR_ITEMS for 'mimir' scope
|
||||
const scopeName = 'mimir';
|
||||
const mockNavigations = [
|
||||
{
|
||||
metadata: { name: 'mimir-item-1' },
|
||||
spec: { scope: 'mimir', url: '/d/mimir-dashboard-1' },
|
||||
status: { title: 'Mimir Dashboard 1' },
|
||||
},
|
||||
];
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: mockNavigations },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNavigationsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchScopeNavigations([scopeName]);
|
||||
|
||||
// Validate: returns navigations for the requested scope
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
result.forEach((nav) => {
|
||||
expect(nav.spec.scope).toBe('mimir');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch navigations for multiple scopes', async () => {
|
||||
// Expected: Returns navigations for both 'mimir' and 'loki'
|
||||
const scopeNames = ['mimir', 'loki'];
|
||||
const mockNavigations = [
|
||||
{
|
||||
metadata: { name: 'mimir-item-1' },
|
||||
spec: { scope: 'mimir', url: '/d/mimir-dashboard-1' },
|
||||
status: { title: 'Mimir Dashboard 1' },
|
||||
},
|
||||
{
|
||||
metadata: { name: 'loki-item-1' },
|
||||
spec: { scope: 'loki', url: '/d/loki-dashboard-1' },
|
||||
status: { title: 'Loki Dashboard 1' },
|
||||
},
|
||||
];
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: mockNavigations },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNavigationsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchScopeNavigations(scopeNames);
|
||||
|
||||
// Validate: returns navigations for both scopes
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
const resultScopeNames = result.map((nav) => nav.spec.scope);
|
||||
expect(resultScopeNames.length).toBeGreaterThan(0);
|
||||
result.forEach((nav) => {
|
||||
expect(scopeNames).toContain(nav.spec.scope);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when no navigations found', async () => {
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: [] },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNavigationsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
const result = await apiClient.fetchScopeNavigations(['grafana']);
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: [] },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNavigationsResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const result = await apiClient.fetchScopeNavigations(['mimir']);
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('performance considerations', () => {
|
||||
it('should make single batched request with fetchMultipleScopeNodes', async () => {
|
||||
mockBackendSrv.get.mockResolvedValue({ items: [] });
|
||||
// This test verifies that the method uses the batched endpoint
|
||||
const nodeNames = [
|
||||
'applications-grafana',
|
||||
'applications-mimir',
|
||||
'applications-loki',
|
||||
'applications-tempo',
|
||||
'applications-cloud',
|
||||
];
|
||||
const expectedNodes = MOCK_NODES.filter((n) => nodeNames.includes(n.metadata.name));
|
||||
const mockSubscription = createMockSubscription({
|
||||
data: { items: expectedNodes },
|
||||
});
|
||||
(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate as jest.Mock).mockReturnValue(
|
||||
mockSubscription
|
||||
);
|
||||
|
||||
await apiClient.fetchMultipleScopeNodes(['node-1', 'node-2', 'node-3', 'node-4', 'node-5']);
|
||||
const result = await apiClient.fetchMultipleScopeNodes(nodeNames);
|
||||
|
||||
// Should make exactly 1 API call
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledTimes(1);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
// Verify it was called once with all names
|
||||
expect(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate).toHaveBeenCalledTimes(1);
|
||||
expect(scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate).toHaveBeenCalledWith(
|
||||
{ names: nodeNames },
|
||||
{ subscribe: false }
|
||||
);
|
||||
});
|
||||
|
||||
it('should make N sequential requests with fetchScopeNode (old pattern)', async () => {
|
||||
mockBackendSrv.get.mockResolvedValue({
|
||||
metadata: { name: 'test' },
|
||||
spec: { nodeType: 'leaf', title: 'Test', parentName: '' },
|
||||
// This test demonstrates the old pattern of fetching nodes one by one
|
||||
// Each call makes a separate API request
|
||||
const nodeNames = [
|
||||
'applications-grafana',
|
||||
'applications-mimir',
|
||||
'applications-loki',
|
||||
'applications-tempo',
|
||||
'applications-cloud',
|
||||
];
|
||||
const mockNodes = nodeNames.map((name) => MOCK_NODES.find((n) => n.metadata.name === name)).filter(Boolean);
|
||||
const mockSubscriptions = mockNodes.map((node) => createMockSubscription({ data: node }));
|
||||
mockSubscriptions.forEach((sub) => {
|
||||
(scopeAPIv0alpha1.endpoints.getScopeNode.initiate as jest.Mock).mockReturnValueOnce(sub);
|
||||
});
|
||||
|
||||
// Simulate old pattern of fetching nodes one by one
|
||||
await Promise.all([
|
||||
apiClient.fetchScopeNode('node-1'),
|
||||
apiClient.fetchScopeNode('node-2'),
|
||||
apiClient.fetchScopeNode('node-3'),
|
||||
apiClient.fetchScopeNode('node-4'),
|
||||
apiClient.fetchScopeNode('node-5'),
|
||||
const results = await Promise.all([
|
||||
apiClient.fetchScopeNode('applications-grafana'),
|
||||
apiClient.fetchScopeNode('applications-mimir'),
|
||||
apiClient.fetchScopeNode('applications-loki'),
|
||||
apiClient.fetchScopeNode('applications-tempo'),
|
||||
apiClient.fetchScopeNode('applications-cloud'),
|
||||
]);
|
||||
|
||||
// Should make 5 separate API calls
|
||||
expect(mockBackendSrv.get).toHaveBeenCalledTimes(5);
|
||||
expect(results).toHaveLength(5);
|
||||
expect(results.every((r) => r !== undefined)).toBe(true);
|
||||
// Verify it was called 5 times (once per node)
|
||||
expect(scopeAPIv0alpha1.endpoints.getScopeNode.initiate).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,95 @@
|
||||
import { getAPIBaseURL } from '@grafana/api-clients';
|
||||
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
|
||||
import { getBackendSrv, config } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { scopeAPIv0alpha1 } from 'app/api/clients/scope/v0alpha1';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { ScopeNavigation } from './dashboards/types';
|
||||
|
||||
const apiUrl = getAPIBaseURL('scope.grafana.app', 'v0alpha1');
|
||||
|
||||
export class ScopesApiClient {
|
||||
/**
|
||||
* Checks if the data is a Kubernetes Status error response.
|
||||
* @param data The data to check
|
||||
* @returns true if the data is a Status error, false otherwise
|
||||
*/
|
||||
private isStatusError(data: unknown): data is { kind: 'Status'; status: 'Failure'; message?: string; code?: number } {
|
||||
return (
|
||||
data !== null &&
|
||||
typeof data === 'object' &&
|
||||
'kind' in data &&
|
||||
data.kind === 'Status' &&
|
||||
'status' in data &&
|
||||
data.status === 'Failure'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and validates data from an RTK Query result, checking for error responses.
|
||||
* @param result The RTK Query result
|
||||
* @param context Context for error logging (e.g., resource name)
|
||||
* @returns The data if valid, undefined if it's an error response
|
||||
*/
|
||||
private extractDataOrHandleError<T>(result: { data?: T; error?: unknown }, context: string): T | undefined {
|
||||
if ('data' in result && result.data) {
|
||||
// Check if the data is actually an error response (Kubernetes Status object)
|
||||
if (this.isStatusError(result.data)) {
|
||||
const errorMessage = getMessageFromError(result.data);
|
||||
console.error(`Failed to fetch %s:`, context, errorMessage);
|
||||
return undefined;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
if ('error' in result) {
|
||||
const errorMessage = getMessageFromError(result.error);
|
||||
console.error(`Failed to fetch %s:`, context, errorMessage);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
async fetchScope(name: string): Promise<Scope | undefined> {
|
||||
const subscription = dispatch(scopeAPIv0alpha1.endpoints.getScope.initiate({ name }, { subscribe: false }));
|
||||
try {
|
||||
return await getBackendSrv().get<Scope>(apiUrl + `/scopes/${name}`);
|
||||
const result = await subscription;
|
||||
return this.extractDataOrHandleError(result, `scope: ${name}`);
|
||||
} catch (err) {
|
||||
// TODO: maybe some better error handling
|
||||
console.error(err);
|
||||
const errorMessage = getMessageFromError(err);
|
||||
console.error('Failed to fetch scope:', name, errorMessage);
|
||||
return undefined;
|
||||
} finally {
|
||||
// Unsubscribe for extra safety, even though with subscribe: false and awaiting,
|
||||
// the request completes before return, so this is mostly a no-op
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMultipleScopes(scopesIds: string[]): Promise<Scope[]> {
|
||||
const scopes = await Promise.all(scopesIds.map((id) => this.fetchScope(id)));
|
||||
return scopes.filter((scope) => scope !== undefined);
|
||||
if (scopesIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const scopes = await Promise.all(scopesIds.map((id) => this.fetchScope(id)));
|
||||
const successfulScopes = scopes.filter((scope) => scope !== undefined);
|
||||
|
||||
if (successfulScopes.length < scopesIds.length) {
|
||||
const failedCount = scopesIds.length - successfulScopes.length;
|
||||
console.warn(
|
||||
'Failed to fetch',
|
||||
failedCount,
|
||||
'of',
|
||||
scopesIds.length,
|
||||
'scope(s). Requested IDs:',
|
||||
scopesIds.join(', ')
|
||||
);
|
||||
}
|
||||
|
||||
return successfulScopes;
|
||||
} catch (err) {
|
||||
const errorMessage = getMessageFromError(err);
|
||||
console.error('Failed to fetch multiple scopes:', scopesIds, errorMessage);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMultipleScopeNodes(names: string[]): Promise<ScopeNode[]> {
|
||||
@@ -27,13 +97,31 @@ export class ScopesApiClient {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const subscription = dispatch(
|
||||
scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate({ names }, { subscribe: false })
|
||||
);
|
||||
try {
|
||||
const res = await getBackendSrv().get<{ items: ScopeNode[] }>(apiUrl + `/find/scope_node_children`, {
|
||||
names: names,
|
||||
});
|
||||
return res?.items ?? [];
|
||||
} catch (err) {
|
||||
const result = await subscription;
|
||||
|
||||
if ('data' in result && result.data) {
|
||||
// The generated API returns items compatible with @grafana/data ScopeNode
|
||||
return result.data.items ?? [];
|
||||
}
|
||||
|
||||
if ('error' in result) {
|
||||
const errorMessage = getMessageFromError(result.error);
|
||||
console.error('Failed to fetch multiple scope nodes:', names, errorMessage);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (err) {
|
||||
const errorMessage = getMessageFromError(err);
|
||||
console.error('Failed to fetch multiple scope nodes:', names, errorMessage);
|
||||
return [];
|
||||
} finally {
|
||||
// Unsubscribe for extra safety, even though with subscribe: false and awaiting,
|
||||
// the request completes before return, so this is mostly a no-op
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,46 +141,128 @@ export class ScopesApiClient {
|
||||
throw new Error('Limit must be between 1 and 10000');
|
||||
}
|
||||
|
||||
const subscription = dispatch(
|
||||
scopeAPIv0alpha1.endpoints.getFindScopeNodeChildrenResults.initiate(
|
||||
{
|
||||
parent: options.parent,
|
||||
query: options.query,
|
||||
limit,
|
||||
},
|
||||
{ subscribe: false, forceRefetch: true } // Froce refetch for search. Revisit this when necessary
|
||||
)
|
||||
);
|
||||
try {
|
||||
const nodes =
|
||||
(
|
||||
await getBackendSrv().get<{ items: ScopeNode[] }>(apiUrl + `/find/scope_node_children`, {
|
||||
parent: options.parent,
|
||||
query: options.query,
|
||||
limit,
|
||||
})
|
||||
)?.items ?? [];
|
||||
const result = await subscription;
|
||||
|
||||
if ('data' in result && result.data) {
|
||||
// The generated API returns items compatible with @grafana/data ScopeNode
|
||||
return result.data.items ?? [];
|
||||
}
|
||||
|
||||
if ('error' in result) {
|
||||
const errorMessage = getMessageFromError(result.error);
|
||||
const contextParts: string[] = [];
|
||||
if (options.parent) {
|
||||
contextParts.push('parent="' + options.parent + '"');
|
||||
}
|
||||
if (options.query) {
|
||||
contextParts.push('query="' + options.query + '"');
|
||||
}
|
||||
contextParts.push('limit=' + limit);
|
||||
const context = contextParts.join(', ');
|
||||
console.error('Failed to fetch scope nodes:', context, errorMessage);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
} catch (err) {
|
||||
return [];
|
||||
} catch (err) {
|
||||
const errorMessage = getMessageFromError(err);
|
||||
const contextParts: string[] = [];
|
||||
if (options.parent) {
|
||||
contextParts.push('parent="' + options.parent + '"');
|
||||
}
|
||||
if (options.query) {
|
||||
contextParts.push('query="' + options.query + '"');
|
||||
}
|
||||
contextParts.push('limit=' + limit);
|
||||
const context = contextParts.join(', ');
|
||||
console.error('Failed to fetch scope nodes:', context, errorMessage);
|
||||
return [];
|
||||
} finally {
|
||||
// Unsubscribe for extra safety, even though with subscribe: false and awaiting,
|
||||
// the request completes before return, so this is mostly a no-op
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public fetchDashboards = async (scopeNames: string[]): Promise<ScopeDashboardBinding[]> => {
|
||||
try {
|
||||
const response = await getBackendSrv().get<{ items: ScopeDashboardBinding[] }>(
|
||||
apiUrl + `/find/scope_dashboard_bindings`,
|
||||
const subscription = dispatch(
|
||||
// Note: `name` is required by generated types but ignored by the query builder (codegen bug)
|
||||
scopeAPIv0alpha1.endpoints.getFindScopeDashboardBindingsResults.initiate(
|
||||
{
|
||||
name: '',
|
||||
scope: scopeNames,
|
||||
}
|
||||
);
|
||||
},
|
||||
{ subscribe: false }
|
||||
)
|
||||
);
|
||||
try {
|
||||
const result = await subscription;
|
||||
|
||||
if ('data' in result && result.data) {
|
||||
// The generated API returns items compatible with @grafana/data ScopeDashboardBinding
|
||||
return result.data.items ?? [];
|
||||
}
|
||||
|
||||
if ('error' in result) {
|
||||
const errorMessage = getMessageFromError(result.error);
|
||||
console.error('Failed to fetch dashboards for scopes:', scopeNames, errorMessage);
|
||||
}
|
||||
|
||||
return response?.items ?? [];
|
||||
} catch (err) {
|
||||
return [];
|
||||
} catch (err) {
|
||||
const errorMessage = getMessageFromError(err);
|
||||
console.error('Failed to fetch dashboards for scopes:', scopeNames, errorMessage);
|
||||
return [];
|
||||
} finally {
|
||||
// Unsubscribe for extra safety, even though with subscribe: false and awaiting,
|
||||
// the request completes before return, so this is mostly a no-op
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
|
||||
public fetchScopeNavigations = async (scopeNames: string[]): Promise<ScopeNavigation[]> => {
|
||||
const subscription = dispatch(
|
||||
// Note: `name` is required by generated types but ignored by the query builder (codegen bug)
|
||||
scopeAPIv0alpha1.endpoints.getFindScopeNavigationsResults.initiate(
|
||||
{
|
||||
name: '',
|
||||
scope: scopeNames,
|
||||
},
|
||||
{ subscribe: false }
|
||||
)
|
||||
);
|
||||
try {
|
||||
const response = await getBackendSrv().get<{ items: ScopeNavigation[] }>(apiUrl + `/find/scope_navigations`, {
|
||||
scope: scopeNames,
|
||||
});
|
||||
const result = await subscription;
|
||||
|
||||
if ('data' in result && result.data) {
|
||||
// The generated API returns items compatible with ScopeNavigation
|
||||
return result.data.items ?? [];
|
||||
}
|
||||
|
||||
if ('error' in result) {
|
||||
const errorMessage = getMessageFromError(result.error);
|
||||
console.error('Failed to fetch scope navigations for scopes:', scopeNames, errorMessage);
|
||||
}
|
||||
|
||||
return response?.items ?? [];
|
||||
} catch (err) {
|
||||
return [];
|
||||
} catch (err) {
|
||||
const errorMessage = getMessageFromError(err);
|
||||
console.error('Failed to fetch scope navigations for scopes:', scopeNames, errorMessage);
|
||||
return [];
|
||||
} finally {
|
||||
// Unsubscribe for extra safety, even though with subscribe: false and awaiting,
|
||||
// the request completes before return, so this is mostly a no-op
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,11 +270,21 @@ export class ScopesApiClient {
|
||||
if (!config.featureToggles.useScopeSingleNodeEndpoint) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const subscription = dispatch(
|
||||
scopeAPIv0alpha1.endpoints.getScopeNode.initiate({ name: scopeNodeId }, { subscribe: false })
|
||||
);
|
||||
try {
|
||||
const response = await getBackendSrv().get<ScopeNode>(apiUrl + `/scopenodes/${scopeNodeId}`);
|
||||
return response;
|
||||
const result = await subscription;
|
||||
return this.extractDataOrHandleError(result, `scope node: ${scopeNodeId}`);
|
||||
} catch (err) {
|
||||
const errorMessage = getMessageFromError(err);
|
||||
console.error('Failed to fetch scope node:', scopeNodeId, errorMessage);
|
||||
return undefined;
|
||||
} finally {
|
||||
// Unsubscribe for extra safety, even though with subscribe: false and awaiting,
|
||||
// the request completes before return, so this is mostly a no-op
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ import { config, locationService } from '@grafana/runtime';
|
||||
|
||||
import { ScopesApiClient } from '../ScopesApiClient';
|
||||
// Import mock data for subScope tests
|
||||
import { navigationWithSubScope, navigationWithSubScope2, navigationWithSubScopeAndGroups } from '../tests/utils/mocks';
|
||||
import {
|
||||
navigationWithSubScope,
|
||||
navigationWithSubScope2,
|
||||
navigationWithSubScopeAndGroups,
|
||||
} from '../tests/utils/mockData';
|
||||
|
||||
import { ScopesDashboardsService, filterItemsWithSubScopesInPath } from './ScopesDashboardsService';
|
||||
import { ScopeNavigation } from './types';
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
|
||||
import { enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
|
||||
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
|
||||
import { getDatasource, getInstanceSettings } from './utils/mocks';
|
||||
import { renderDashboard, resetScenes } from './utils/render';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
getBackendSrv: () => ({ get: getMock }),
|
||||
getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }),
|
||||
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
|
||||
}));
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
describe('Dashboard reload', () => {
|
||||
let dashboardReloadSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { config, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { MOCK_SUB_SCOPE_MIMIR_ITEMS } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { ScopesApiClient } from '../ScopesApiClient';
|
||||
import { ScopesService } from '../ScopesService';
|
||||
@@ -35,26 +38,25 @@ import {
|
||||
dashboardWithRootFolder,
|
||||
dashboardWithRootFolderAndOtherFolder,
|
||||
dashboardWithTwoFolders,
|
||||
getDatasource,
|
||||
getInstanceSettings,
|
||||
getMock,
|
||||
navigationWithSubScope,
|
||||
navigationWithSubScope2,
|
||||
navigationWithSubScopeDifferent,
|
||||
navigationWithSubScopeAndGroups,
|
||||
subScopeMimirItems,
|
||||
} from './utils/mocks';
|
||||
} from './utils/mockData';
|
||||
import { getDatasource, getInstanceSettings } from './utils/mocks';
|
||||
import { renderDashboard, resetScenes } from './utils/render';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
getBackendSrv: () => ({ get: getMock }),
|
||||
getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }),
|
||||
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
|
||||
}));
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
describe('Dashboards list', () => {
|
||||
let fetchDashboardsSpy: jest.SpyInstance;
|
||||
let fetchScopeNavigationsSpy: jest.SpyInstance;
|
||||
@@ -539,7 +541,7 @@ describe('Dashboards list', () => {
|
||||
|
||||
it('Loads subScope items when folder is expanded', async () => {
|
||||
const mockNavigations = [navigationWithSubScope];
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(subScopeMimirItems);
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(MOCK_SUB_SCOPE_MIMIR_ITEMS);
|
||||
|
||||
await toggleDashboards();
|
||||
await updateScopes(scopesService, ['grafana']);
|
||||
@@ -571,7 +573,7 @@ describe('Dashboards list', () => {
|
||||
|
||||
it('Shows loading state while fetching subScope items', async () => {
|
||||
const mockNavigations = [navigationWithSubScope];
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(subScopeMimirItems);
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(MOCK_SUB_SCOPE_MIMIR_ITEMS);
|
||||
|
||||
await toggleDashboards();
|
||||
await updateScopes(scopesService, ['grafana']);
|
||||
@@ -591,7 +593,7 @@ describe('Dashboards list', () => {
|
||||
|
||||
it('Multiple subScope folders with same subScope load same content', async () => {
|
||||
const mockNavigations = [navigationWithSubScope, navigationWithSubScope2];
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValue(subScopeMimirItems);
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValue(MOCK_SUB_SCOPE_MIMIR_ITEMS);
|
||||
|
||||
await toggleDashboards();
|
||||
await updateScopes(scopesService, ['grafana']);
|
||||
@@ -676,7 +678,7 @@ describe('Dashboards list', () => {
|
||||
|
||||
it('Filters search works with loaded subScope content', async () => {
|
||||
const mockNavigations = [navigationWithSubScope];
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(subScopeMimirItems);
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(MOCK_SUB_SCOPE_MIMIR_ITEMS);
|
||||
|
||||
await toggleDashboards();
|
||||
await updateScopes(scopesService, ['grafana']);
|
||||
@@ -715,7 +717,7 @@ describe('Dashboards list', () => {
|
||||
|
||||
it('Does not fetch subScope items if folder is already loaded', async () => {
|
||||
const mockNavigations = [navigationWithSubScope];
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(subScopeMimirItems);
|
||||
fetchScopeNavigationsSpy.mockResolvedValueOnce(mockNavigations).mockResolvedValueOnce(MOCK_SUB_SCOPE_MIMIR_ITEMS);
|
||||
|
||||
await toggleDashboards();
|
||||
await updateScopes(scopesService, ['grafana']);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { config, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { MOCK_SCOPES } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { ScopesService } from '../ScopesService';
|
||||
@@ -25,7 +28,7 @@ import {
|
||||
expectResultApplicationsGrafanaSelected,
|
||||
expectScopesSelectorValue,
|
||||
} from './utils/assertions';
|
||||
import { getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks';
|
||||
import { getDatasource, getInstanceSettings } from './utils/mocks';
|
||||
import { renderDashboard, resetScenes } from './utils/render';
|
||||
import { getListOfScopes } from './utils/selectors';
|
||||
|
||||
@@ -33,11 +36,13 @@ jest.mock('@grafana/runtime', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
getBackendSrv: () => ({ get: getMock }),
|
||||
getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }),
|
||||
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
|
||||
}));
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
describe('Selector', () => {
|
||||
let fetchSelectedScopesSpy: jest.SpyInstance;
|
||||
let dashboardReloadSpy: jest.SpyInstance;
|
||||
@@ -67,7 +72,7 @@ describe('Selector', () => {
|
||||
await selectResultCloud();
|
||||
await applyScopes();
|
||||
expect(fetchSelectedScopesSpy).toHaveBeenCalled();
|
||||
expect(getListOfScopes(scopesService)).toEqual(mocksScopes.filter(({ metadata: { name } }) => name === 'cloud'));
|
||||
expect(getListOfScopes(scopesService)).toEqual(MOCK_SCOPES.filter(({ metadata: { name } }) => name === 'cloud'));
|
||||
});
|
||||
|
||||
it('Does not save the scopes on close', async () => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { config, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { ScopesService } from '../ScopesService';
|
||||
|
||||
@@ -43,18 +45,20 @@ import {
|
||||
expectScopesHeadline,
|
||||
expectScopesSelectorValue,
|
||||
} from './utils/assertions';
|
||||
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
|
||||
import { getDatasource, getInstanceSettings } from './utils/mocks';
|
||||
import { renderDashboard, resetScenes } from './utils/render';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
getBackendSrv: () => ({ get: getMock }),
|
||||
getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }),
|
||||
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
|
||||
}));
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
describe('Tree', () => {
|
||||
let fetchNodesSpy: jest.SpyInstance;
|
||||
let fetchScopeSpy: jest.SpyInstance;
|
||||
|
||||
92
public/app/features/scopes/tests/utils/mockData.ts
Normal file
92
public/app/features/scopes/tests/utils/mockData.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { ScopeDashboardBinding } from '@grafana/data';
|
||||
|
||||
import { ScopeNavigation } from '../../dashboards/types';
|
||||
|
||||
// Mock subScope navigation items (specific to these tests)
|
||||
export const navigationWithSubScope: ScopeNavigation = {
|
||||
metadata: { name: 'subscope-nav-1' },
|
||||
spec: {
|
||||
scope: 'grafana',
|
||||
subScope: 'mimir',
|
||||
url: '/d/subscope-dashboard-1',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Dashboards',
|
||||
groups: [], // subScope items ignore groups
|
||||
},
|
||||
};
|
||||
|
||||
export const navigationWithSubScope2: ScopeNavigation = {
|
||||
metadata: { name: 'subscope-nav-2' },
|
||||
spec: {
|
||||
scope: 'grafana',
|
||||
subScope: 'mimir',
|
||||
url: '/d/subscope-dashboard-2',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir Overview',
|
||||
groups: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const navigationWithSubScopeDifferent: ScopeNavigation = {
|
||||
metadata: { name: 'subscope-nav-3' },
|
||||
spec: {
|
||||
scope: 'grafana',
|
||||
subScope: 'loki',
|
||||
url: '/d/subscope-dashboard-3',
|
||||
},
|
||||
status: {
|
||||
title: 'Loki Dashboards',
|
||||
groups: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const navigationWithSubScopeAndGroups: ScopeNavigation = {
|
||||
metadata: { name: 'subscope-nav-groups' },
|
||||
spec: {
|
||||
scope: 'grafana',
|
||||
subScope: 'mimir',
|
||||
url: '/d/subscope-dashboard-groups',
|
||||
},
|
||||
status: {
|
||||
title: 'Mimir with Groups',
|
||||
groups: ['Group1', 'Group2'], // Should be ignored for subScope items
|
||||
},
|
||||
};
|
||||
|
||||
const generateScopeDashboardBinding = (dashboardTitle: string, groups?: string[], dashboardId?: string) => ({
|
||||
metadata: { name: `${dashboardTitle}-name` },
|
||||
spec: {
|
||||
dashboard: `${dashboardId ?? dashboardTitle}-dashboard`,
|
||||
scope: `${dashboardTitle}-scope`,
|
||||
},
|
||||
status: {
|
||||
dashboardTitle,
|
||||
groups,
|
||||
},
|
||||
});
|
||||
|
||||
export const dashboardWithoutFolder: ScopeDashboardBinding = generateScopeDashboardBinding('Without Folder');
|
||||
export const dashboardWithOneFolder: ScopeDashboardBinding = generateScopeDashboardBinding('With one folder', [
|
||||
'Folder 1',
|
||||
]);
|
||||
export const dashboardWithTwoFolders: ScopeDashboardBinding = generateScopeDashboardBinding('With two folders', [
|
||||
'Folder 1',
|
||||
'Folder 2',
|
||||
]);
|
||||
export const alternativeDashboardWithTwoFolders: ScopeDashboardBinding = generateScopeDashboardBinding(
|
||||
'Alternative with two folders',
|
||||
['Folder 1', 'Folder 2'],
|
||||
'With two folders'
|
||||
);
|
||||
export const dashboardWithRootFolder: ScopeDashboardBinding = generateScopeDashboardBinding('With root folder', ['']);
|
||||
export const alternativeDashboardWithRootFolder: ScopeDashboardBinding = generateScopeDashboardBinding(
|
||||
'Alternative With root folder',
|
||||
[''],
|
||||
'With root folder'
|
||||
);
|
||||
export const dashboardWithRootFolderAndOtherFolder: ScopeDashboardBinding = generateScopeDashboardBinding(
|
||||
'With root folder and other folder',
|
||||
['', 'Folder 3']
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user