Compare commits

..
Author SHA1 Message Date
konsalex 794b094044 fix lint issues 2025-11-26 16:11:31 +01:00
konsalex 91f9caa4ef Merge remote-tracking branch 'origin/main' into apiextensions-2 2025-11-26 15:46:51 +01:00
konsalex 02839523a0 mt first setup and cleanup access 2025-11-26 15:35:54 +01:00
konsalex 24b6ce3bad chore: update ff 2025-11-26 13:14:57 +01:00
konsalex b550c12e40 chore: update codeowners 2025-11-26 13:09:28 +01:00
konsalex d8e3dda0e2 chore: update wire 2025-11-26 13:07:45 +01:00
konsalex e4e45b9271 rebasing 2025-11-26 11:50:37 +01:00
konsalex 3f23ae36c9 rebasing 2025-11-26 11:50:14 +01:00
konsalex 5272cfcc99 rebasing 2025-11-26 11:46:27 +01:00
konsalex 151b81361c rebasing 2025-11-26 11:42:53 +01:00
konsalex 90a830ad71 use FF and cleanup access code 2025-11-26 11:41:58 +01:00
konsalex 3a9559c8d0 rebasing 2025-11-26 11:41:50 +01:00
302 changed files with 4048 additions and 10343 deletions
+1 -1
View File
@@ -185,7 +185,6 @@
/pkg/services/search/ @grafana/grafana-search-and-storage
/pkg/services/searchusers/ @grafana/grafana-search-and-storage
/pkg/services/secrets/ @grafana/grafana-operator-experience-squad
/pkg/services/setting/ @grafana/grafana-backend-services-squad
/pkg/services/shorturls/ @grafana/sharing-squad
/pkg/services/sqlstore/ @grafana/grafana-search-and-storage
/pkg/services/ssosettings/ @grafana/identity-squad
@@ -1323,6 +1322,7 @@ embed.go @grafana/grafana-as-code
/conf/provisioning/datasources/ @grafana/plugins-platform-backend
/conf/provisioning/plugins/ @grafana/plugins-platform-backend
/conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team
/conf/apiextensions.ini @grafana/grafana-app-platform-squad
# Security
/relyance.yaml @grafana/security-team
+4 -1
View File
@@ -71,7 +71,6 @@ public/css/*.min.css
.vs/
.cursor/
.devcontainer/
.claude/
.eslintcache
.stylelintcache
@@ -135,6 +134,10 @@ profile.cov
/pkg/operators/enterprise_*
/pkg/operators/**/enterprise_*
# Enterprise apiextensions server
pkg/registry/apis/apiextensions/*
!pkg/registry/apis/apiextensions/register.go
debug.test
/examples/*/dist
/packaging/**/*.rpm
+1 -1
View File
@@ -110,7 +110,7 @@ If you believe you've found a security vulnerability, please read our [security
### Suggest features
If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). To learn how to write an effective feature request, refer to [Create a feature request](contribute/create-feature-request.md).
If you have an idea of how to improve Grafana, submit a [feature request](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md).
We want to make Grafana accessible to even more people. Submit an [accessibility issue](https://github.com/grafana/grafana/issues/new?template=2-accessibility.md) to help us understand what we can improve.
@@ -43,12 +43,6 @@ type RepositoryView struct {
// For git, this is the target branch
Branch string `json:"branch,omitempty"`
// For git, this is the target URL
URL string `json:"url,omitempty"`
// For git, this is the target path
Path string `json:"path,omitempty"`
// The supported workflows
Workflows []Workflow `json:"workflows"`
}
@@ -1690,20 +1690,6 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref common.ReferenceCa
Format: "",
},
},
"url": {
SchemaProps: spec.SchemaProps{
Description: "For git, this is the target URL",
Type: []string{"string"},
Format: "",
},
},
"path": {
SchemaProps: spec.SchemaProps{
Description: "For git, this is the target path",
Type: []string{"string"},
Format: "",
},
},
"workflows": {
SchemaProps: spec.SchemaProps{
Description: "The supported workflows",
+53
View File
@@ -0,0 +1,53 @@
; Run locally unified storage with SQLite to test
; new API registration changes
app_mode = development
target = all
[log]
level = debug
[server]
; HTTPS is required for kubectl (but HTTP works for testing with curl)
protocol = https
http_port = 1111
[feature_toggles]
; Enable the apiextensions feature
apiExtensions = true
; Enable unified storage globally
unifiedStorage = true
; Enable search indexing for unified storage
unifiedStorageSearch = true
; Enable the grafana-apiserver explicitly
grafanaAPIServer = true
; Enable K8s aggregator for API discovery aggregation
; NOTE: This is an enterprise-only feature that requires TLS certificates
; This will surface the new registered group APIs to the `/apis` endpoint.
kubernetesAggregator = true
[grafana-apiserver]
; Use unified storage backed by SQL (uses your Grafana database)
storage_type = unified
; Certificates for the Kubernetes aggregator (generated by hack/make-aggregator-pki.sh)
proxy_client_cert_file = data/grafana-aggregator/client.crt
proxy_client_key_file = data/grafana-aggregator/client.key
; Configure dashboards to use unified storage
[unified_storage.dashboards.dashboard.grafana.app]
dualWriterMode = 5
; Configure folders to use unified storage (required for dashboards)
[unified_storage.folders.folder.grafana.app]
dualWriterMode = 5
[database]
; SQLite database for testing
type = sqlite3
path = grafana.db
high_availability = false
; Will only be used for the MT grafana
; apiextensions service
; [auth.extended_jwt]
; enabled = true
; jwks_url = "http://localhost:6481/jwks"
-1
View File
@@ -5,7 +5,6 @@ We're excited that you're considering making a contribution to the Grafana proje
These are some good resources to explore for developers:
- [Create a pull request](create-pull-request.md)
- [Create a feature request](create-feature-request.md)
- [Developer guide](developer-guide.md)
- [Triage issues](triage-issues.md)
- [Merge a pull request](merge-pull-request.md)
-160
View File
@@ -1,160 +0,0 @@
# Create a feature request
Feature requests help us understand what you need from Grafana. This document guides you through writing effective feature requests that help maintainers understand your needs and prioritize improvements.
## Before you begin
We're excited to hear your ideas! Before you submit a feature request, consider these resources:
- Read the [Code of Conduct](../CODE_OF_CONDUCT.md) to understand our community guidelines.
- Search [existing feature requests](https://github.com/grafana/grafana/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Ffeature-request) to see if someone already suggested something similar.
- Discuss your idea in the [Grafana community forums](https://community.grafana.com/) to refine it and gather feedback.
## Your first feature request
When you're ready to submit a feature request, use the [feature request template](https://github.com/grafana/grafana/issues/new?template=1-feature_requests.md). The template has three sections that help maintainers understand what you need and why.
Here's an [example of how all three sections work together in an actual feature request](https://github.com/grafana/grafana/issues/105298) from the Grafana community. We'll analyze each section based on this example feature request.
### Why is this needed
This section describes the real problem or limitation you're facing.
Explain what's difficult, inefficient, or impossible with the current implementation. Focus on the problem rather than proposing a solution. This helps maintainers understand your use case and potentially find better solutions.
**What to include:**
- The specific problem or pain point you're experiencing
- How the current behavior falls short for your workflow
- Why this matters to you and your work
- A concrete example that clarifies the issue (optional but helpful)
**What to avoid:**
- Jumping directly to the solution (save that for the next section)
- Vague statements like "it would be nice if..."
- Assuming maintainers know your context or workflow
**Example of a strong answer:**
```
When using a datasource variable in dashboards and using the "Export" feature in a dashboard,
this will automatically create an input for the datasource(s) being used, but it will also
effectively override the use of the datasource variable in all panels.
This makes a confusing
experience when importing the dashboard, because users are prompted for an input, but the
selected datasource won't be reflected in the datasource variable, and any changes to the
datasource variable will not have any effect on the dashboard.
```
**Example of a weak answer:**
```
Dashboard export doesn't work well with variables.
```
The first example clearly explains what's broken, why it's confusing, and what the specific consequences are. The second example is too vague and doesn't explain the actual problem.
### What would you like to be added
This section describes what you want Grafana to do differently.
Be specific and concrete about the expected behavior. If you're suggesting a UI change, describe the interaction or include a screenshot or sketch. If it's data or API related, provide an example query or expected output.
**What to include:**
- Exactly what behavior you expect
- How the feature should work in practice
- Examples, screenshots, or code snippets that illustrate your idea
- Expected output or results
**What to avoid:**
- Vague or abstract descriptions
- Multiple unrelated features in one request (create separate requests instead)
- Implementation details unless they're critical to your request
**Example of a strong answer:**
```
Ideal behavior here would be that when using the export feature, either:
1. No inputs section is created for datasource types that are used as datasource variables.
2. IF an input is created, it should only be used to replace the currently selected value of
the datasource variable, rather than override the datasource in panels.
```
**Example of a weak answer:**
```
Fix the dashboard export feature.
```
The first example provides clear, actionable options for how the feature should work. The second example is too vague and doesn't specify what the fix should do.
### Who is this feature for?
This section describes who benefits from this feature and in what context.
Help maintainers understand the scope and impact of your request. Be specific about user types, workflows, or scenarios where this feature matters.
**What to include:**
- The type of user who needs this (for example, Tempo users, dashboard editors, plugin developers)
- Whether this affects all Grafana users or only those using specific features or data sources
- The workflow or use case this feature improves (optional but helpful)
**What to avoid:**
- Saying "everyone" without clarifying who actually needs it
- Being overly narrow if the feature has broader appeal
**Example of a strong answer:**
```
Any Grafana Dashboard users or authors that use datasource variables.
```
**Example of a weak answer:**
```
Dashboard users.
```
The first example identifies the specific users and the feature they use (datasource variables). The second example is too generic and doesn't clarify which users or workflow are affected.
## Best practices for feature requests
Follow these guidelines to increase the chances of your feature request being accepted:
### Keep it focused
Request one feature at a time. If you have multiple ideas, create separate feature requests for each one. This makes it easier to discuss, prioritize, and implement each feature independently.
### Research first
Before submitting, search for similar requests. If you find an existing request that's close to your idea, add your use case and context to that discussion instead of creating a duplicate.
### Provide context
The more context you provide, the better maintainers can understand your needs. Include:
- Your environment or setup (which data sources, plugins, or features you're using)
- Your workflow or process
- Why this matters to you
- Any workarounds you've tried
### Be open to alternatives
Maintainers might suggest different approaches to solve your problem. Be open to these alternatives as they might be easier to implement or more maintainable in the long term.
### Stay engaged
After submitting your feature request, monitor the discussion. Answer questions from maintainers and provide clarification when needed. This helps move your request forward.
## Contributing the feature yourself
If you want to implement the feature yourself, feel free to create a pull request following the [pull request guidelines](create-pull-request.md).
We welcome community contributions and appreciate your help making Grafana better!
@@ -14,10 +14,10 @@ weight: 400
The Grafana Cloud Migration Assistant, generally available from Grafana v12.0, automatically migrates resources from your Grafana OSS/Enterprise instance to Grafana Cloud. It provides the following functionality:
- Securely connect your self-managed instance to a Grafana Cloud instance.
- Migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
- View the migration status of your resources in real-time.
Some benefits of the migration assistant are:
Some of the benefits of the migration assistant are:
Ease of use
: Follow the steps provided by the UI to easily migrate all your resources to Grafana Cloud without using Grafana APIs or scripts.
@@ -44,7 +44,7 @@ The following resources are supported by the migration assistant:
To use the Grafana migration assistant, you need:
- A self-managed Grafana instance version v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles).
- Grafana v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles).
- A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) you intend to migrate your resources to.
- [`Admin`](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/) access to the Grafana Cloud Stack. To check your access level, go to `https://grafana.com/orgs/<YOUR-ORG-NAME>/members`.
- [Grafana server administrator](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators) access to your existing Grafana OSS/Enterprise instance. To check your access level, go to `https://<GRAFANA-ONPREM-URL>/admin/users`.
@@ -64,7 +64,7 @@ In Grafana Enterprise, the server administrator has access to the migration assi
### Grant access in Grafana Enterprise
{{< admonition type="note" >}}
{{< admonition type="important">}}
You must [configure RBAC](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant.
{{< /admonition >}}
@@ -66,7 +66,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `grafanaAssistantInProfilesDrilldown` | Enables integration with Grafana Assistant in Profiles Drilldown | Yes |
| `sharingDashboardImage` | Enables image sharing functionality for dashboards | Yes |
| `tabularNumbers` | Use fixed-width numbers globally in the UI | |
| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | Yes |
| `tempoSearchBackendMigration` | Run search queries through the tempo backend | |
## Public preview feature toggles
@@ -96,9 +95,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown |
| `logsPanelControls` | Enables a control component for the logs panel in Explore |
| `interactiveLearning` | Enables the interactive learning app |
| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker |
| `newVizSuggestions` | Enable new visualization suggestions |
| `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; |
| `newPanelPadding` | Increases panel padding globally |
| `transformationsEmptyPlaceholder` | Show transformation quick-start cards in empty transformations state |
## Development feature toggles
@@ -1,6 +1,6 @@
---
aliases:
- ../../../panels-visualizations/query-transform-data/sql-expressions/ # /docs/grafana/next/panels-visualizations/query-transform-data/sql-expressions/
- ../../panels-visualizations/query-transform-data/sql-expressions/ # /docs/grafana/next/panels-visualizations/query-transform-data/sql-expressions/
labels:
products:
- cloud
@@ -4,8 +4,6 @@ import { test, expect, E2ESelectorGroups, DashboardPage, DashboardPageArgs } fro
import testDashboard from '../dashboards/DashboardWithAllConditionalRendering.json';
import { checkRepeatedPanelTitles } from './utils';
test.use({
featureToggles: {
kubernetesDashboards: true,
@@ -95,7 +93,7 @@ test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@d
test.afterAll(async ({ request }) => {
if (uid) {
await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/stacks-12345/dashboards/${uid}`);
await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/default/dashboards/${uid}`);
}
});
@@ -409,52 +407,4 @@ test.describe('Dashboard - Conditional Rendering - Load and Change', { tag: ['@d
await expect(getTabShowNotMatches(dashboardPage, selectors)).toBeVisible();
await expect(getTabHideNotMatches(dashboardPage, selectors)).not.toBeVisible();
});
test.describe('Variable repeat', () => {
const repeatOptions = ['a', 'b', 'c'];
async function failTestDataRequestForOption(page: Page, option: string) {
await page.route(/\/api\/ds\/query\?.*\bds_type=grafana-testdata-datasource/, async (route) => {
const rawPostData = route.request().postData();
if (!rawPostData) {
return;
}
// the first panel query has a label set to the current variable value
if (JSON.parse(rawPostData).queries[0].labels === `key=${option}`) {
await route.fulfill({ status: 500, body: '{}' });
} else {
await route.continue();
}
});
}
test('Hide when equals, hide when no data', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await loadDashboard(page, gotoDashboardPage);
await getTab(dashboardPage, selectors, 'repeated items').click();
const optionForHiddenPanels = repeatOptions[0];
await failTestDataRequestForOption(page, optionForHiddenPanels);
await checkRepeatedPanelTitles(
dashboardPage,
selectors,
'Hide panel - ',
[
`custom variable equals ${optionForHiddenPanels} (current = ${optionForHiddenPanels})`,
`no data (current = ${optionForHiddenPanels})`,
],
true
);
const optionsForVisiblePanels = repeatOptions.slice(1);
await checkRepeatedPanelTitles(dashboardPage, selectors, 'Hide panel - ', [
...optionsForVisiblePanels.map((o) => `custom variable equals ${optionForHiddenPanels} (current = ${o})`),
...optionsForVisiblePanels.map((o) => `no data (current = ${o})`),
]);
});
});
});
+4 -10
View File
@@ -97,18 +97,12 @@ export async function checkRepeatedPanelTitles(
dashboardPage: DashboardPage,
selectors: E2ESelectorGroups,
title: string,
options: Array<string | number>,
expectHidden = false
options: Array<string | number>
) {
for (const option of options) {
const titleLocator = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title(`${title}${option}`)
);
if (expectHidden) {
await expect(titleLocator).toBeHidden();
} else {
await expect(titleLocator).toBeVisible();
}
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${title}${option}`))
).toBeVisible();
}
}
@@ -9,8 +9,7 @@ const NUM_NESTED_DASHBOARDS = 60;
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ import testDashboard from '../dashboards/TestDashboard.json';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -6,8 +6,7 @@ test.use({
featureToggles: {
scenes: true,
sharingDashboardImage: true, // Enable the export image feature
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -2,8 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -2,8 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ import testDashboard from '../dashboards/DashboardLiveTest.json';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -2,8 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
},
});
@@ -2,8 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
},
});
@@ -3,8 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scenes: true,
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -3,8 +3,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scenes: true,
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -5,8 +5,7 @@ import { SnapshotCreateResponse } from '../../public/app/features/dashboard/serv
test.use({
featureToggles: {
scenes: true,
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -5,8 +5,7 @@ const DASHBOARD_UID = 'HYaGDGIMk';
test.use({
timezoneId: 'Pacific/Easter',
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -7,8 +7,7 @@ const TIMEZONE_DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -16,8 +16,7 @@ test.use({
origins: [],
},
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -2,8 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ import testDashboard from '../dashboards/TestDashboard.json';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -5,8 +5,7 @@ const DASHBOARD_NAME = 'Test variable output';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -52,8 +52,7 @@ async function assertPreviewValues(
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -5,8 +5,7 @@ const DASHBOARD_NAME = 'Test variable output';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -18,8 +18,7 @@ async function assertPreviewValues(
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -5,8 +5,7 @@ const DASHBOARD_NAME = 'Templating - Nested Template Variables';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -5,8 +5,7 @@ const DASHBOARD_NAME = 'Test variable output';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -4,8 +4,7 @@ const DASHBOARD_UID = 'ZqZnVvFZz';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
},
});
@@ -4,8 +4,7 @@ const DASHBOARD_UID = 'yBCC3aKGk';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -6,8 +6,7 @@ const PAGE_UNDER_TEST = 'AejrN1AMz';
test.use({
featureToggles: {
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
},
});
@@ -3308,170 +3308,6 @@
}
}
},
"panel-37": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"group": "",
"kind": "DataQuery",
"spec": {},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 37,
"links": [],
"title": "Hide panel - custom variable equals a (current = ${myCustomVariable})",
"vizConfig": {
"group": "text",
"kind": "VizConfig",
"spec": {
"fieldConfig": {
"defaults": {},
"overrides": []
},
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "",
"mode": "markdown"
}
},
"version": "12.2.0-pre"
}
}
},
"panel-38": {
"kind": "Panel",
"spec": {
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"hidden": false,
"query": {
"datasource": {
"name": "PD8C576611E62080A"
},
"group": "grafana-testdata-datasource",
"kind": "DataQuery",
"spec": {
"labels": "key=$myCustomVariable",
"scenarioId": "random_walk",
"seriesCount": 1
},
"version": "v0"
},
"refId": "A"
}
}
],
"queryOptions": {},
"transformations": []
}
},
"description": "",
"id": 38,
"links": [],
"title": "Hide panel - no data (current = ${myCustomVariable})",
"vizConfig": {
"group": "timeseries",
"kind": "VizConfig",
"spec": {
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
}
},
"version": "12.2.0-pre"
}
}
},
"panel-4": {
"kind": "Panel",
"spec": {
@@ -5255,80 +5091,6 @@
},
"title": "Tab - hide - time range <7d"
}
},
{
"kind": "TabsLayoutTab",
"spec": {
"layout": {
"kind": "AutoGridLayout",
"spec": {
"columnWidthMode": "standard",
"items": [
{
"kind": "AutoGridLayoutItem",
"spec": {
"conditionalRendering": {
"kind": "ConditionalRenderingGroup",
"spec": {
"condition": "and",
"items": [
{
"kind": "ConditionalRenderingVariable",
"spec": {
"operator": "equals",
"value": "a",
"variable": "myCustomVariable"
}
}
],
"visibility": "hide"
}
},
"element": {
"kind": "ElementReference",
"name": "panel-37"
},
"repeat": {
"mode": "variable",
"value": "myCustomVariable"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"conditionalRendering": {
"kind": "ConditionalRenderingGroup",
"spec": {
"condition": "and",
"items": [
{
"kind": "ConditionalRenderingData",
"spec": {
"value": false
}
}
],
"visibility": "hide"
}
},
"element": {
"kind": "ElementReference",
"name": "panel-38"
},
"repeat": {
"mode": "variable",
"value": "myCustomVariable"
}
}
}
],
"maxColumnCount": 3,
"rowHeightMode": "standard"
}
},
"title": "Tab - repeated items"
}
}
]
}
@@ -5360,39 +5122,6 @@
"query": "",
"skipUrlSync": false
}
},
{
"kind": "CustomVariable",
"spec": {
"allowCustomValue": false,
"current": {
"text": "All",
"value": "$__all"
},
"hide": "dontHide",
"includeAll": true,
"multi": false,
"name": "myCustomVariable",
"options": [
{
"selected": false,
"text": "a",
"value": "a"
},
{
"selected": false,
"text": "b",
"value": "b"
},
{
"selected": false,
"text": "c",
"value": "c"
}
],
"query": "a, b, c",
"skipUrlSync": false
}
}
]
},
+5
View File
@@ -1912,6 +1912,11 @@
"count": 4
}
},
"public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
}
},
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": {
"no-restricted-syntax": {
"count": 3
+1 -2
View File
@@ -1,5 +1,4 @@
// TODO: Migrate to Typescript.
// @ts-nocheck
// @ts-check
const emotionPlugin = require('@emotion/eslint-plugin');
const restrictedGlobals = require('confusing-browser-globals');
const importPlugin = require('eslint-plugin-import');
+9
View File
@@ -407,6 +407,7 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA=
github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw=
github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
@@ -846,8 +847,10 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
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/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=
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
@@ -896,6 +899,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88=
github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA=
github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4=
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA=
github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c=
@@ -923,6 +927,7 @@ github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU=
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco=
github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ=
@@ -939,6 +944,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
@@ -1324,6 +1330,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8=
@@ -1375,6 +1382,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs=
github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
@@ -1932,6 +1940,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-2
View File
@@ -66,7 +66,6 @@
"themes-generate": "yarn themes-schema && esbuild --target=es6 ./scripts/cli/generateSassVariableFiles.ts --bundle --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",
"typecheck:tsgo": "tsgo --noEmit",
"plugins:build-bundled": "echo 'bundled plugins are no longer supported'",
"watch": "yarn start -d watch,start core:start --watchTheme",
"i18n:stats": "node ./scripts/cli/reportI18nStats.mjs",
@@ -167,7 +166,6 @@
"@types/yargs": "17.0.33",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript/native-preview": "^7.0.0-dev.20251128.1",
"autoprefixer": "10.4.21",
"babel-loader": "10.0.0",
"blob-polyfill": "9.0.20240710",
@@ -110,9 +110,10 @@ export function getInheritedProperties<T extends Route>(
...propertiesParentInherited,
} as const;
// @ts-expect-error we're using "keyof" for the property so the type checker can help us out but this makes the
// reduce function signature unhappy
const inherited = reduce(
inheritableProperties,
// @ts-expect-error we're using "keyof" for the property so the type checker can help us out but this makes the
(inheritedProperties: InheritableProperties, parentValue, property: keyof InheritableProperties) => {
const parentHasValue = parentValue != null;
@@ -1,7 +1,4 @@
{
"exclude": ["dist", "node_modules", "tests", "**/*.test.ts*", "**/*.story.tsx"],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
@@ -1581,8 +1581,6 @@ export type RepositoryView = {
branch?: string;
/** The k8s name for this repository */
name: string;
/** For git, this is the target path */
path?: string;
/** When syncing, where values are saved
Possible enum values:
@@ -1600,8 +1598,6 @@ export type RepositoryView = {
- `"gitlab"`
- `"local"` */
type: 'bitbucket' | 'git' | 'github' | 'gitlab' | 'local';
/** For git, this is the target URL */
url?: string;
/** The supported workflows */
workflows: ('branch' | 'write')[];
};
@@ -1,7 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "node_modules", "**/*.test.ts*"],
"compilerOptions": {
"rootDir": "./src"
}
"exclude": ["dist", "node_modules", "**/*.test.ts*"]
}
@@ -91,10 +91,8 @@ describe('EventBus', () => {
it('Supports legacy events', () => {
const bus = new EventBusSrv();
const events: LegacyEventPayload[] = [];
const handler = (event?: LegacyEventPayload) => {
if (event) {
events.push(event);
}
const handler = (event: LegacyEventPayload) => {
events.push(event);
};
bus.on(legacyEvent, handler);
@@ -113,9 +111,7 @@ describe('EventBus', () => {
const newEvents: AlertSuccessEvent[] = [];
bus.on(legacyEvent, (event) => {
if (event) {
legacyEvents.push(event);
}
legacyEvents.push(event);
});
bus.subscribe(AlertSuccessEvent, (event) => {
+2 -2
View File
@@ -133,12 +133,12 @@ export interface LegacyEmitter {
/**
* @deprecated use $on
*/
off<T>(event: AppEvent<T> | string, handler: LegacyEventHandler<T>): void;
off<T>(event: AppEvent<T> | string, handler: (payload?: T) => void): void;
}
/** @public */
export interface LegacyEventHandler<T> {
(payload?: T): void;
(payload: T): void;
wrapper?: (event: BusEvent) => void;
}
@@ -381,11 +381,6 @@ export class PanelPlugin<
const appender = builder.getListAppender<TOptions, TFieldConfigOptions>({
pluginId: this.meta.id,
name: this.meta.name,
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
});
const result = supplier(builder.dataSummary);
@@ -1,27 +1,19 @@
import { PreferredVisualisationType } from '../../types/data';
import { DataFrame, FieldType } from '../../types/dataFrame';
import { DataFrameType } from '../../types/dataFrameTypes';
/**
* @alpha
*/
export interface PanelDataSummary {
hasData?: boolean;
rowCountTotal: number;
/** max number of rows in any given dataframe in the panel data */
rowCountMax: number;
frameCount: number;
fieldCount: number;
/** max number of fields in any given dataframe in the panel data */
fieldCountMax: number;
/** given a field type, return the number of fields across all dataframes which match this type */
fieldCountByType: (type: FieldType) => number;
/** returns true if any fields in any frames match the field type */
hasFieldType: (type: FieldType) => boolean;
/* returns true if any of the frames in this panel data summary have the type */
hasDataFrameType: (type: DataFrameType) => boolean;
/* returns true if any of the frames in this panel data summary have the type */
hasPreferredVisualisationType: (type: PreferredVisualisationType) => boolean;
/** pass along a reference to the DataFrame array in case it's needed by the plugin */
rawFrames?: DataFrame[];
/** The first frame that set's this value */
preferredVisualisationType?: PreferredVisualisationType;
/* --- DEPRECATED FIELDS BELOW --- */
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
@@ -31,114 +23,60 @@ export interface PanelDataSummary {
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
stringFieldCount: number;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
hasTimeField?: boolean;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
hasNumberField?: boolean;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
hasTimeField?: boolean;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
hasStringField?: boolean;
}
/**
* @alpha
*/
class PanelDataSummaryImpl implements PanelDataSummary {
public rowCountTotal = 0;
/** max number of rows in any single dataframe in the panel data */
public rowCountMax = 0;
public fieldCount = 0;
/** max number of fields in any single dataframe in the panel data */
public fieldCountMax = 0;
private countByType: Partial<Record<FieldType, number>> = {};
private preferredVisualisationTypes: Set<PreferredVisualisationType> = new Set<PreferredVisualisationType>();
private dataFrameTypes: Set<DataFrameType> = new Set<DataFrameType>();
public get hasData(): boolean {
return this.rowCountTotal > 0;
}
public get frameCount(): number {
return this.rawFrames?.length ?? 0;
}
constructor(public rawFrames?: DataFrame[]) {
this._processFrames();
}
private _processFrames() {
for (const frame of this.rawFrames ?? []) {
this.rowCountTotal += frame.length;
if (frame.meta?.preferredVisualisationType) {
this.preferredVisualisationTypes.add(frame.meta.preferredVisualisationType);
}
if (frame.meta?.type) {
this.dataFrameTypes.add(frame.meta.type);
}
for (const field of frame.fields) {
this.fieldCount++;
this.countByType[field.type] = (this.countByType[field.type] || 0) + 1;
}
if (frame.length > this.rowCountMax) {
this.rowCountMax = frame.length;
}
if (frame.fields.length > this.fieldCountMax) {
this.fieldCountMax = frame.fields.length;
}
}
}
public fieldCountByType(type: FieldType): number {
return this.countByType[type] ?? 0;
}
public hasFieldType(type: FieldType): boolean {
return this.fieldCountByType(type) > 0;
}
public hasPreferredVisualisationType(type: PreferredVisualisationType): boolean {
return this.preferredVisualisationTypes.has(type);
}
public hasDataFrameType(type: DataFrameType): boolean {
return this.dataFrameTypes.has(type);
}
/**** DEPRECATED ****/
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
public get numberFieldCount(): number {
return this.fieldCountByType(FieldType.number);
}
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */
public get timeFieldCount(): number {
return this.fieldCountByType(FieldType.time);
}
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
public get stringFieldCount() {
return this.fieldCountByType(FieldType.string);
}
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
public get hasTimeField() {
return this.fieldCountByType(FieldType.time) > 0;
}
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
public get hasNumberField() {
return this.fieldCountByType(FieldType.number) > 0;
}
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
public get hasStringField() {
return this.fieldCountByType(FieldType.string) > 0;
}
}
/**
* @alpha
* given a list of dataframes, summarize attributes of those frames for features like suggestions.
* @param frames - dataframes to summarize
* @returns summary of the dataframes
*/
export function getPanelDataSummary(frames?: DataFrame[]): PanelDataSummary {
return new PanelDataSummaryImpl(frames);
export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary {
let rowCountTotal = 0;
let rowCountMax = 0;
let fieldCount = 0;
const countByType: Partial<Record<FieldType, number>> = {};
let preferredVisualisationType: PreferredVisualisationType | undefined;
for (const frame of frames) {
rowCountTotal += frame.length;
if (frame.meta?.preferredVisualisationType) {
preferredVisualisationType = frame.meta.preferredVisualisationType;
}
for (const field of frame.fields) {
fieldCount++;
countByType[field.type] = (countByType[field.type] || 0) + 1;
}
if (frame.length > rowCountMax) {
rowCountMax = frame.length;
}
}
const fieldCountByType = (f: FieldType) => countByType[f] ?? 0;
return {
rowCountTotal,
rowCountMax,
fieldCount,
preferredVisualisationType,
frameCount: frames.length,
hasData: rowCountTotal > 0,
hasFieldType: (f: FieldType) => fieldCountByType(f) > 0,
fieldCountByType,
// deprecated
numberFieldCount: fieldCountByType(FieldType.number),
timeFieldCount: fieldCountByType(FieldType.time),
stringFieldCount: fieldCountByType(FieldType.string),
hasTimeField: fieldCountByType(FieldType.time) > 0,
hasNumberField: fieldCountByType(FieldType.number) > 0,
hasStringField: fieldCountByType(FieldType.string) > 0,
};
}
@@ -72,7 +72,7 @@ interface IndexOptions {
asPercentile: boolean;
}
const defaultNumericVizOptions: ReduceOptions = {
const defaultReduceOptions: ReduceOptions = {
reducer: ReducerID.sum,
};
@@ -149,10 +149,10 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
switch (mode) {
case CalculateFieldMode.ReduceRow:
creator = getReduceRowCreator(defaults(options.reduce, defaultNumericVizOptions), data);
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
break;
case CalculateFieldMode.CumulativeFunctions:
creator = getCumulativeCreator(defaults(options.cumulative, defaultNumericVizOptions), data);
creator = getCumulativeCreator(defaults(options.cumulative, defaultReduceOptions), data);
break;
case CalculateFieldMode.WindowFunctions:
creator = getWindowCreator(defaults(options.window, defaultWindowOptions), data);
@@ -896,42 +896,6 @@ describe('getHistogramFields', () => {
}
`);
});
it('should prevent excessive densification when sparse histogram has large gaps', () => {
const result = getHistogramFields(
toDataFrame({
meta: {
type: DataFrameType.HeatmapCells,
},
fields: [
{ name: 'yMin', type: FieldType.number, values: [0.001, 1000] },
{ name: 'yMax', type: FieldType.number, values: [0.00101, 1010] },
{ name: 'count', type: FieldType.number, values: [10, 20] },
],
})
);
expect(result).toBeDefined();
expect(result!.counts[0].values.length).toBeLessThanOrEqual(1001);
});
it('should handle multiple observed buckets when hitting densification limit', () => {
const result = getHistogramFields(
toDataFrame({
meta: {
type: DataFrameType.HeatmapCells,
},
fields: [
{ name: 'yMin', type: FieldType.number, values: [0.001, 1000, 2000] },
{ name: 'yMax', type: FieldType.number, values: [0.00101, 1010, 2020] },
{ name: 'count', type: FieldType.number, values: [10, 20, 30] },
],
})
);
expect(result).toBeDefined();
expect(result!.counts[0].values.every((v) => !isNaN(v))).toBe(true);
});
});
describe('joinHistograms', () => {
@@ -210,8 +210,6 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine
let denseMins: number[] = [];
let denseMaxs: number[] = [];
const MAX_DENSIFIED_BUCKETS = 1000;
for (let i = 0; i < uniqueMaxs.length; i++) {
let curMax = uniqueMaxs[i];
let curMin = uniqueMins[i];
@@ -225,17 +223,13 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine
curMax = curMax * bucketFactor;
curMin = curMin * bucketFactor;
while (curMax < nextMax * 0.999999 && denseMaxs.length < MAX_DENSIFIED_BUCKETS) {
while (curMax < nextMax * 0.999999) {
denseMaxs.push(curMax);
denseMins.push(curMin);
curMax = curMax * bucketFactor;
curMin = curMin * bucketFactor;
}
if (denseMaxs.length >= MAX_DENSIFIED_BUCKETS) {
break;
}
}
}
@@ -244,10 +238,7 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine
for (let i = 0; i < yMaxField.values.length; i++) {
let max = yMaxField.values[i];
let currentCount = countsByMax.get(max);
if (currentCount !== undefined) {
countsByMax.set(max, currentCount + countField.values[i]);
}
countsByMax.set(max, countsByMax.get(max) + countField.values[i]);
}
let fields = {
@@ -643,7 +643,6 @@ export interface MetricFindValue {
value?: string | number;
group?: string;
expandable?: boolean;
properties?: Record<string, string>;
}
export interface DataSourceGetDrilldownsApplicabilityOptions<TQuery extends DataQuery = DataQuery> {
+6 -10
View File
@@ -361,10 +361,6 @@ export interface FeatureToggles {
*/
dashboardNewLayouts?: boolean;
/**
* Use the v2 kubernetes API in the frontend for dashboards
*/
kubernetesDashboardsV2?: boolean;
/**
* Enables undo/redo in dynamic dashboards
*/
dashboardUndoRedo?: boolean;
@@ -467,6 +463,10 @@ export interface FeatureToggles {
*/
kubernetesAggregatorCapTokenAuth?: boolean;
/**
* Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
*/
apiExtensions?: boolean;
/**
* Enable groupBy variable support in scenes dashboards
*/
groupByVariable?: boolean;
@@ -1095,7 +1095,7 @@ export interface FeatureToggles {
graphiteBackendMode?: boolean;
/**
* Enables the updated Azure Monitor resource picker
* @default true
* @default false
*/
azureResourcePickerUpdates?: boolean;
/**
@@ -1154,7 +1154,7 @@ export interface FeatureToggles {
pluginStoreServiceLoading?: boolean;
/**
* Increases panel padding globally
* @default true
* @default false
*/
newPanelPadding?: boolean;
/**
@@ -1193,8 +1193,4 @@ export interface FeatureToggles {
* @default false
*/
rudderstackUpgrade?: boolean;
/**
* Adds support for Kubernetes alerting historian APIs
*/
kubernetesAlertingHistorian?: boolean;
}
@@ -75,9 +75,7 @@ export interface VisualizationSuggestion<TOptions extends unknown = {}, TFieldCo
* mutate the suggestion object which is passed in as the first argument.
*/
previewModifier?: (suggestion: VisualizationSuggestion<TOptions, TFieldConfig>) => void;
/** @deprecated this will no longer be supported in the new Suggestions UI. */
icon?: string;
/** @deprecated this will no longer be supported in the new Suggestions UI. */
imgSrc?: string;
};
}
+1 -4
View File
@@ -1,7 +1,4 @@
{
"exclude": ["dist", "node_modules", "test", "**/*.test.ts*"],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
@@ -1,7 +1,4 @@
{
"exclude": ["dist", "node_modules", "**/*.test.ts*"],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
@@ -2,8 +2,7 @@
"compilerOptions": {
"paths": {
"@emotion/core": ["./src/types/emotion-core-stub.d.ts"]
},
"rootDir": "./src"
}
},
"exclude": ["**/*.test.ts*"],
"extends": "./tsconfig.json"
+1 -4
View File
@@ -1,7 +1,4 @@
{
"exclude": ["dist", "node_modules", "test", "**/*.test.ts*"],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
@@ -8,8 +8,7 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"allowJs": true,
"rootDirs": ["."],
"rootDir": "../.."
"rootDirs": ["."]
},
"exclude": ["dist/**/*"],
"include": [
@@ -8,8 +8,5 @@
"src/querybuilder/testUtils.ts",
"../../public/test/setupTests.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
+1 -4
View File
@@ -1,7 +1,4 @@
{
"exclude": ["dist", "node_modules", "**/*.test.ts*", "../../public/test/setupTests.ts"],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
@@ -312,7 +312,18 @@ export const handyTestingSchema: Spec = {
label: 'Custom Variable',
multi: true,
name: 'customVar',
options: [],
options: [
{
selected: true,
text: 'option1',
value: 'option1',
},
{
selected: false,
text: 'option2',
value: 'option2',
},
],
query: 'option1, option2',
skipUrlSync: false,
allowCustomValue: true,
@@ -479,18 +490,5 @@ export const handyTestingSchema: Spec = {
allowCustomValue: true,
},
},
{
kind: 'SwitchVariable',
spec: {
name: 'switchVar',
label: 'Switch Variable',
description: 'A switch variable',
current: 'false',
enabledValue: 'true',
disabledValue: 'false',
hide: 'dontHide',
skipUrlSync: false,
},
},
],
};
+1 -4
View File
@@ -1,7 +1,4 @@
{
"exclude": ["dist/**/*", "**/*.test.ts*"],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
"extends": "./tsconfig.json"
}
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import AutoSizer, { type Size } from 'react-virtualized-auto-sizer';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
@@ -61,7 +61,7 @@ export function RawEditor({ db, query, onChange, onRunQuery, onValidate, queryTo
const renderEditor = (standalone = false) => {
return standalone ? (
<AutoSizer>
{({ width, height }: Size) => {
{({ width, height }) => {
return renderQueryEditor(width, height);
}}
</AutoSizer>
+1 -2
View File
@@ -8,8 +8,7 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"strict": true,
"rootDirs": ["."],
"rootDir": "./src"
"rootDirs": ["."]
},
"exclude": ["dist/**/*"],
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"]
+1 -2
View File
@@ -7,8 +7,7 @@
"emitDeclarationOnly": true,
"isolatedModules": true,
"rootDirs": ["."],
"moduleResolution": "bundler",
"rootDir": "./src"
"moduleResolution": "bundler"
},
"ts-node": {
"compilerOptions": {
@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { dateTime, makeTimeRange, TimeRange, BootData } from '@grafana/data';
import { dateTime, makeTimeRange, TimeRange } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { TimeRangeProvider } from './TimeRangeContext';
@@ -152,58 +152,6 @@ it('does not submit wrapping forms', async () => {
expect(onSubmit).not.toHaveBeenCalled();
});
it('shows CTRL+Z in zoom out tooltip when feature flag is disabled', async () => {
window.grafanaBootData = {
settings: {
featureToggles: {
newTimeRangeZoomShortcuts: false,
},
},
} as BootData;
render(
<TimeRangePicker
onChangeTimeZone={() => {}}
onChange={(value) => {}}
value={value}
onMoveBackward={() => {}}
onMoveForward={() => {}}
onZoom={() => {}}
/>
);
const zoomButton = screen.getByLabelText('Zoom out time range');
await userEvent.hover(zoomButton);
expect(await screen.findByText(/CTRL\+Z/)).toBeInTheDocument();
});
it('shows t - in zoom out tooltip when feature flag is enabled', async () => {
window.grafanaBootData = {
settings: {
featureToggles: {
newTimeRangeZoomShortcuts: true,
},
},
} as BootData;
render(
<TimeRangePicker
onChangeTimeZone={() => {}}
onChange={(value) => {}}
value={value}
onMoveBackward={() => {}}
onMoveForward={() => {}}
onZoom={() => {}}
/>
);
const zoomButton = screen.getByLabelText('Zoom out time range');
await userEvent.hover(zoomButton);
expect(await screen.findByText(/t -/)).toBeInTheDocument();
});
describe('TimePickerTooltip', () => {
beforeAll(() => {
const mockIntl = {
@@ -19,7 +19,6 @@ import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { useStyles2 } from '../../themes/ThemeContext';
import { getFeatureToggle } from '../../utils/featureToggle';
import { ButtonGroup } from '../Button/ButtonGroup';
import { getModalStyles } from '../Modal/getModalStyles';
import { getPortalContainer } from '../Portal/Portal';
@@ -244,22 +243,13 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
TimeRangePicker.displayName = 'TimeRangePicker';
const ZoomOutTooltip = () => {
const newShortcuts = getFeatureToggle('newTimeRangeZoomShortcuts');
return (
<>
{newShortcuts ? (
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip-new">
Time range zoom out <br /> t -
</Trans>
) : (
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
Time range zoom out <br /> CTRL+Z
</Trans>
)}
</>
);
};
const ZoomOutTooltip = () => (
<>
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
Time range zoom out <br /> CTRL+Z
</Trans>
</>
);
export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => {
const styles = useStyles2(getLabelStyles);
@@ -29,7 +29,7 @@ const cursorDefaults: Cursor = {
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean };
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number };
export class UPlotConfigBuilder {
readonly uid = Math.random().toString(36).slice(2);
@@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => {
expect(mockQueryZoom).not.toHaveBeenCalled();
});
it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => {
it('should set isPanning state during drag and clear on mouseup', () => {
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
@@ -153,20 +153,6 @@ describe('XAxisInteractionAreaPlugin', () => {
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 350, bubbles: true }));
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({
isPanning: true,
min: expectedRange.from,
max: expectedRange.to,
isTimeRangePending: true,
});
});
it('should clear isPanning state immediately for small drags below threshold', () => {
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 402, bubbles: true }));
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ isPanning: false });
});
});
@@ -96,14 +96,11 @@ export const setupXAxisPan = (
xAxisEl.style.cursor = 'grab';
const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST;
config.setState({ isPanning: false });
if (isSignificantDrag) {
if (Math.abs(dragPixels) >= MIN_PAN_DIST) {
const newRange = calculatePanRange(startMin, startMax, dragPixels, u.bbox.width);
config.setState({ isPanning: true, min: newRange.from, max: newRange.to, isTimeRangePending: true });
queryZoom(newRange);
} else {
config.setState({ isPanning: false });
}
document.removeEventListener('mousemove', onMove);
+1 -2
View File
@@ -3,8 +3,7 @@
"paths": {
"@emotion/core": ["./src/types/emotion-core-stub.d.ts"],
"@grafana/ui": ["."]
},
"rootDir": "./src"
}
},
"exclude": [
"**/*.story.tsx",
-21
View File
@@ -9,7 +9,6 @@ import (
"sort"
"strconv"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -19,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/setting"
@@ -202,11 +200,6 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds())
}()
ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID())
if err != nil {
@@ -238,11 +231,6 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds())
}()
uid := web.Params(c.Req)[":uid"]
if uid == "" {
@@ -373,11 +361,6 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
// 409: conflictError
// 500: internalServerError
func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds())
}()
cmd := datasources.AddDataSourceCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
@@ -495,10 +478,6 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
// 409: conflictError
// 500: internalServerError
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds())
}()
cmd := datasources.UpdateDataSourceCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
-23
View File
@@ -9,7 +9,6 @@ import (
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -17,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
@@ -83,19 +81,6 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
}, mockSQLStore)
}
// setupDsConfigMetrics creates and registers the prometheus metrics needed for HTTPServer tests
// that call methods using dsConfigHandlerRequestsDuration.
func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.HistogramVec) {
promRegister := prometheus.NewRegistry()
dsConfigHandlerRequestsDuration := metricutil.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "grafana",
Name: "ds_config_handler_requests_duration_seconds",
Help: "Duration of requests handled by datasource configuration handlers",
}, []string{"code_path", "handler"})
promRegister.MustRegister(dsConfigHandlerRequestsDuration)
return promRegister, dsConfigHandlerRequestsDuration
}
// Adding data sources with invalid URLs should lead to an error.
func TestAddDataSource_InvalidURL(t *testing.T) {
sc := setupScenarioContext(t, "/api/datasources")
@@ -103,7 +88,6 @@ func TestAddDataSource_InvalidURL(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
@@ -134,7 +118,6 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
accesscontrolService: actest.FakeService{},
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc := setupScenarioContext(t, "/api/datasources")
@@ -160,7 +143,6 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc := setupScenarioContext(t, "/api/datasources")
@@ -193,7 +175,6 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc := setupScenarioContext(t, "/api/datasources/1234")
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
@@ -218,7 +199,6 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc := setupScenarioContext(t, "/api/datasources/1234")
hs.Cfg.AuthProxy.Enabled = true
@@ -256,7 +236,6 @@ func TestAddDataSourceTeamHTTPHeaders(t *testing.T) {
ExpectedErr: nil,
},
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID))
hs.Cfg.AuthProxy.Enabled = true
@@ -310,7 +289,6 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
accesscontrolService: actest.FakeService{},
}
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
sc := setupScenarioContext(t, "/api/datasources/1234")
@@ -451,7 +429,6 @@ func TestAPI_datasources_AccessControl(t *testing.T) {
hs.DataSourcesService = &dataSourcesServiceMock{expectedDatasource: &datasources.DataSource{}}
hs.accesscontrolService = actest.FakeService{}
hs.Live = newTestLive(t, hs.SQLStore)
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
})
for _, url := range tt.urls {
+21 -28
View File
@@ -203,28 +203,27 @@ type HTTPServer struct {
pluginsCDNService *pluginscdn.Service
managedPluginsService managedplugins.Manager
userService user.Service
tempUserService tempUser.Service
loginAttemptService loginAttempt.Service
orgService org.Service
orgDeletionService org.DeletionService
TeamService team.Service
accesscontrolService accesscontrol.Service
annotationsRepo annotations.Repository
tagService tag.Service
oauthTokenService oauthtoken.OAuthTokenService
statsService stats.Service
authnService authn.Service
starApi *starApi.API
promRegister prometheus.Registerer
promGatherer prometheus.Gatherer
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
namespacer request.NamespaceMapper
anonService anonymous.Service
userVerifier user.Verifier
tlsCerts TLSCerts
htmlHandlerRequestsDuration *prometheus.HistogramVec
dsConfigHandlerRequestsDuration *prometheus.HistogramVec
userService user.Service
tempUserService tempUser.Service
loginAttemptService loginAttempt.Service
orgService org.Service
orgDeletionService org.DeletionService
TeamService team.Service
accesscontrolService accesscontrol.Service
annotationsRepo annotations.Repository
tagService tag.Service
oauthTokenService oauthtoken.OAuthTokenService
statsService stats.Service
authnService authn.Service
starApi *starApi.API
promRegister prometheus.Registerer
promGatherer prometheus.Gatherer
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
namespacer request.NamespaceMapper
anonService anonymous.Service
userVerifier user.Verifier
tlsCerts TLSCerts
htmlHandlerRequestsDuration *prometheus.HistogramVec
}
type TLSCerts struct {
@@ -383,15 +382,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
Name: "html_handler_requests_duration_seconds",
Help: "Duration of requests handled by the index.go HTML handler",
}, []string{"handler"}),
dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "grafana",
Name: "ds_config_handler_requests_duration_seconds",
Help: "Duration of requests handled by datasource configuration handlers",
}, []string{"code_path", "handler"}),
}
promRegister.MustRegister(hs.htmlHandlerRequestsDuration)
promRegister.MustRegister(hs.dsConfigHandlerRequestsDuration)
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+1
View File
@@ -160,6 +160,7 @@ var serviceIdentityTokenPermissions = []string{
"iam.grafana.app:*",
"preferences.grafana.app:*", // user, team, and org preferences
"collections.grafana.app:*", // user stars
"apiextensions.grafana.app:*",
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
"secret.grafana.app/securevalues:decrypt",
+26 -14
View File
@@ -131,19 +131,31 @@ func NamespaceKeyFunc(gr schema.GroupResource) func(ctx context.Context, name st
}
}
// NoNamespaceKeyFunc is the default function for constructing storage paths
// to a resource relative to the given prefix without a namespace.
func NoNamespaceKeyFunc(ctx context.Context, prefix string, gr schema.GroupResource, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
// ClusterScopedKeyFunc constructs storage paths for cluster-scoped resources (no namespace).
func ClusterScopedKeyFunc(gr schema.GroupResource) func(ctx context.Context, name string) (string, error) {
return func(ctx context.Context, name string) (string, error) {
if len(name) == 0 {
return "", apierrors.NewBadRequest("Name parameter required.")
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return key.String(), nil
}
}
// ClusterScopedKeyRootFunc is used by the generic registry store for cluster-scoped resources.
func ClusterScopedKeyRootFunc(gr schema.GroupResource) func(ctx context.Context) string {
return func(ctx context.Context) string {
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
}
return key.String()
}
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
}
key := &Key{
Group: gr.Group,
Resource: gr.Resource,
Name: name,
}
return prefix + key.String(), nil
}
+16 -3
View File
@@ -1,6 +1,8 @@
package generic
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/generic/registry"
@@ -12,16 +14,27 @@ func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, o
gv := resourceInfo.GroupVersion()
gv.Version = runtime.APIVersionInternal
strategy := NewStrategy(scheme, gv)
gr := resourceInfo.GroupResource()
var keyRootFunc func(ctx context.Context) string
var keyFunc func(ctx context.Context, name string) (string, error)
if resourceInfo.IsClusterScoped() {
strategy = strategy.WithClusterScope()
keyRootFunc = ClusterScopedKeyRootFunc(gr)
keyFunc = ClusterScopedKeyFunc(gr)
} else {
keyRootFunc = KeyRootFunc(gr)
keyFunc = NamespaceKeyFunc(gr)
}
store := &registry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
KeyRootFunc: KeyRootFunc(resourceInfo.GroupResource()),
KeyFunc: NamespaceKeyFunc(resourceInfo.GroupResource()),
KeyRootFunc: keyRootFunc,
KeyFunc: keyFunc,
PredicateFunc: Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
DefaultQualifiedResource: gr,
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: resourceInfo.TableConverter(),
CreateStrategy: strategy,
+3 -3
View File
@@ -15,7 +15,6 @@ import (
_ "github.com/blugelabs/bluge"
_ "github.com/blugelabs/bluge_segment_api"
_ "github.com/crewjam/saml"
_ "github.com/docker/go-connections/nat"
_ "github.com/go-jose/go-jose/v4"
_ "github.com/gobwas/glob"
_ "github.com/googleapis/gax-go/v2"
@@ -31,7 +30,6 @@ import (
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
_ "github.com/spyzhov/ajson"
_ "github.com/stretchr/testify/require"
_ "github.com/testcontainers/testcontainers-go"
_ "gocloud.dev/secrets/awskms"
_ "gocloud.dev/secrets/azurekeyvault"
_ "gocloud.dev/secrets/gcpkms"
@@ -56,7 +54,9 @@ import (
_ "github.com/grafana/e2e"
_ "github.com/grafana/gofpdf"
_ "github.com/grafana/gomemcache/memcache"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/testcontainers/testcontainers-go"
)
+228
View File
@@ -0,0 +1,228 @@
package apiextensions
import (
"github.com/prometheus/client_golang/prometheus"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstorage "k8s.io/apiserver/pkg/server/storage"
"k8s.io/kube-openapi/pkg/common"
authlib "github.com/grafana/authlib/types"
genericregistry "k8s.io/apiserver/pkg/registry/generic"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
// CRDStorageProvider is an interface for creating CRD REST options getters.
// Enterprise provides the real implementation, OSS returns nil.
type CRDStorageProvider interface {
NewCRDRESTOptionsGetter(
delegate *apistore.RESTOptionsGetter,
unifiedClient resource.ResourceClient,
) genericregistry.RESTOptionsGetter
}
// OSSCRDStorageProvider is the OSS implementation that returns nil (feature disabled)
type OSSCRDStorageProvider struct{}
func ProvideOSSCRDStorageProvider() CRDStorageProvider {
return &OSSCRDStorageProvider{}
}
func (p *OSSCRDStorageProvider) NewCRDRESTOptionsGetter(
delegate *apistore.RESTOptionsGetter,
unifiedClient resource.ResourceClient,
) genericregistry.RESTOptionsGetter {
return nil
}
var _ builder.APIGroupBuilder = (*Builder)(nil)
// Builder implements builder.APIGroupBuilder for CustomResourceDefinitions.
// This implementation uses the Kubernetes apiextensions-apiserver for CRD handling,
// adapted to work with Grafana's unified storage backend.
//
// IMPORTANT: This builder only registers the CRD types with the scheme.
// The actual CRD storage and custom resource handling is done by the
// Kubernetes apiextensions-apiserver, which is created separately and
// chained as a delegate server.
type Builder struct {
features featuremgmt.FeatureToggles
accessClient authlib.AccessClient
unifiedClient resource.ResourceClient
apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions
storageProvider CRDStorageProvider
}
// RegisterAPIService registers the apiextensions API group in single-tenant mode
func RegisterAPIService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
accessClient authlib.AccessClient,
registerer prometheus.Registerer,
unified resource.ResourceClient,
storageProvider CRDStorageProvider,
) (*Builder, error) {
//nolint:staticcheck // not yet migrated to OpenFeature
if !features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) {
return nil, nil
}
b := &Builder{
features: features,
accessClient: accessClient,
unifiedClient: unified,
storageProvider: storageProvider,
}
// Register the builder to install the schema
apiregistration.RegisterAPI(b)
return b, nil
}
// GetAuthorizer returns the authorizer for CRD resources
// Breaks locally now for ST, will need to test in MT
// For ST just comment this out to test
// func (b *Builder) GetAuthorizer() authorizer.Authorizer {
// return grafanaauthorizer.NewServiceAuthorizer()
// }
// NewAPIService creates an Builder for multi-tenant mode
func NewAPIService(
accessClient authlib.AccessClient,
unified resource.ResourceClient,
registerer prometheus.Registerer,
features featuremgmt.FeatureToggles,
storageProvider CRDStorageProvider,
) (*Builder, error) {
return &Builder{
features: features,
accessClient: accessClient,
unifiedClient: unified,
storageProvider: storageProvider,
}, nil
}
// GetGroupVersion returns the API group version for apiextensions.k8s.io/v1
func (b *Builder) GetGroupVersion() schema.GroupVersion {
return apiextensionsv1.SchemeGroupVersion
}
// InstallSchema installs the CRD types into the scheme
func (b *Builder) InstallSchema(scheme *runtime.Scheme) error {
gv := b.GetGroupVersion()
// Register the apiextensions types from the K8s apiextensions-apiserver
// This uses the types and scheme from the K8s package
metav1.AddToGroupVersion(scheme, gv)
// Add the CRD types to the scheme
scheme.AddKnownTypes(gv,
&apiextensionsv1.CustomResourceDefinition{},
&apiextensionsv1.CustomResourceDefinitionList{},
)
return scheme.SetVersionPriority(gv)
}
func (b *Builder) AllowedV0Alpha1Resources() []string {
return nil
}
// UpdateAPIGroupInfo is a no-op for the apiextensions builder.
// The actual CRD storage is created by the apiextensions server, not the builder.
// This is called by the builder framework but we don't need to do anything here
// since we're using the K8s apiextensions-apiserver which creates its own storage.
func (b *Builder) UpdateAPIGroupInfo(
_ *genericapiserver.APIGroupInfo,
_ builder.APIGroupOptions,
) error {
// Don't install any storage here - the apiextensions server handles this
return nil
}
// GetOpenAPIDefinitions returns the OpenAPI definitions for CRD types
func (b *Builder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return apiextensionsopenapi.GetOpenAPIDefinitions(ref)
}
}
// CreateAPIExtensionsServer creates the Kubernetes apiextensions-apiserver
// This server handles CRD storage and custom resource (CR) handling.
// It should be used as a delegate for the main Grafana API server.
func (b *Builder) CreateAPIExtensionsServer(
serverConfig genericapiserver.RecommendedConfig,
delegationTarget genericapiserver.DelegationTarget,
restOptsGetter *apistore.RESTOptionsGetter,
) (*apiextensionsapiserver.CustomResourceDefinitions, error) {
if restOptsGetter == nil {
return nil, nil
}
// Create the CRD REST options getter that uses unified storage
crdRestOptsGetter := b.storageProvider.NewCRDRESTOptionsGetter(restOptsGetter, b.unifiedClient)
if crdRestOptsGetter == nil {
// Enterprise feature not available
return nil, nil
}
// Create a fresh copy of the config for the apiextensions server
// We need to clear PostStartHooks to avoid conflicts with hooks
// already registered by the main server (e.g., "playlist")
apiExtensionsGenericConfig := serverConfig
apiExtensionsGenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
// Set the RESTOptionsGetter on the GenericConfig
// The K8s apiextensions-apiserver uses GenericConfig.RESTOptionsGetter for CRD storage
// and ExtraConfig.CRDRESTOptionsGetter for Custom Resource storage
apiExtensionsGenericConfig.RESTOptionsGetter = crdRestOptsGetter
// Enable the CRD resources in the API resource config
apiResourceConfig := serverstorage.NewResourceConfig()
apiResourceConfig.EnableVersions(apiextensionsv1.SchemeGroupVersion)
apiExtensionsGenericConfig.MergedResourceConfig = apiResourceConfig
// Configure the apiextensions server
apiextensionsConfig := &apiextensionsapiserver.Config{
GenericConfig: &apiExtensionsGenericConfig,
ExtraConfig: apiextensionsapiserver.ExtraConfig{
// CRDRESTOptionsGetter is used for Custom Resource (CR) storage, not CRD storage
CRDRESTOptionsGetter: crdRestOptsGetter,
MasterCount: 1,
// Webhook conversion is not supported yet
ServiceResolver: nil,
AuthResolverWrapper: nil,
},
}
server, err := apiextensionsConfig.Complete().New(delegationTarget)
if err != nil {
return nil, err
}
b.apiExtensionsServer = server
return server, nil
}
// GetAPIExtensionsServer returns the apiextensions server (if created)
func (b *Builder) GetAPIExtensionsServer() *apiextensionsapiserver.CustomResourceDefinitions {
return b.apiExtensionsServer
}
// SetAPIServer is a no-op for compatibility with the builder interface
func (b *Builder) SetAPIServer(server *genericapiserver.GenericAPIServer) {
}
+2
View File
@@ -1,6 +1,7 @@
package apiregistry
import (
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
"github.com/grafana/grafana/pkg/registry/apis/collections"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
@@ -20,6 +21,7 @@ type Service struct{}
// ProvideRegistryServiceSink is an entry point for each service that will force initialization
// and give each builder the chance to register itself with the main server
func ProvideRegistryServiceSink(
_ *apiextensions.Builder,
_ *dashboardinternal.DashboardsAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *datasource.DataSourceAPIBuilder,
+1 -1
View File
@@ -198,7 +198,7 @@ func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles,
}
func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion {
if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts, featuremgmt.FlagKubernetesDashboardsV2) {
if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) {
// If dashboards v2 is enabled, we want to use v2beta1 as the default API version.
return []schema.GroupVersion{
dashv2beta1.DashboardResourceInfo.GroupVersion(),
+2 -26
View File
@@ -3,9 +3,7 @@ package datasource
import (
"context"
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -13,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
)
var (
@@ -29,9 +26,8 @@ var (
)
type legacyStorage struct {
datasources PluginDatasourceProvider
resourceInfo *utils.ResourceInfo
dsConfigHandlerRequestsDuration *prometheus.HistogramVec
datasources PluginDatasourceProvider
resourceInfo *utils.ResourceInfo
}
func (s *legacyStorage) New() runtime.Object {
@@ -61,21 +57,11 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
}
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds())
}()
return s.datasources.GetDataSource(ctx, name)
}
// Create implements rest.Creater.
func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
}()
ds, ok := obj.(*v0alpha1.DataSource)
if !ok {
return nil, fmt.Errorf("expected a datasource object")
@@ -85,11 +71,6 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa
// Update implements rest.Updater.
func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
}()
old, err := s.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
@@ -126,11 +107,6 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
// Delete implements rest.GracefulDeleter.
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
start := time.Now()
defer func() {
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
}()
err := s.datasources.DeleteDataSource(ctx, name)
return nil, false, err
}
-6
View File
@@ -20,7 +20,6 @@ import (
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/promlib/models"
@@ -219,11 +218,6 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
legacyStore := &legacyStorage{
datasources: b.datasources,
resourceInfo: &ds,
dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "grafana",
Name: "ds_config_handler_requests_duration_seconds",
Help: "Duration of requests handled by datasource configuration handlers",
}, []string{"code_path", "handler"}),
}
unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter)
if err != nil {
-6
View File
@@ -346,12 +346,6 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
if err != nil {
return err
}
if enableZanzanaSync {
b.logger.Info("Enabling hooks for RoleBinding to sync to Zanzana")
roleBindingStore.AfterCreate = b.AfterRoleBindingCreate
roleBindingStore.AfterDelete = b.AfterRoleBindingDelete
roleBindingStore.BeginUpdate = b.BeginRoleBindingUpdate
}
storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore
}
//nolint:staticcheck // not yet migrated to OpenFeature
-302
View File
@@ -1,302 +0,0 @@
package iam
import (
"context"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic/registry"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
)
const resourceType = "rolebinding"
// AfterRoleBindingCreate is a post-create hook that writes the role binding to Zanzana (openFGA)
func (b *IdentityAccessManagementAPIBuilder) AfterRoleBindingCreate(obj runtime.Object, _ *metav1.CreateOptions) {
if b.zClient == nil {
return
}
rb, ok := obj.(*iamv0.RoleBinding)
if !ok {
b.logger.Error("failed to convert object to RoleBinding type", "object", obj)
return
}
operation := "create"
// Grab a ticket to write to Zanzana
// This limits the amount of concurrent connections to Zanzana
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds())
go func(rb *iamv0.RoleBinding) {
start := time.Now()
status := "success"
defer func() {
// Release the ticket after write is done
<-b.zTickets
// Record operation duration and count
hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
}()
b.logger.Debug("writing role binding to zanzana",
"namespace", rb.Namespace,
"name", rb.Name,
"subject", rb.Spec.Subject.Name,
"roleRefs", rb.Spec.RoleRefs,
)
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
defer cancel()
operations := make([]*v1.MutateOperation, 0, len(rb.Spec.RoleRefs))
for _, roleRef := range rb.Spec.RoleRefs {
operations = append(operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_CreateRoleBinding{
CreateRoleBinding: &v1.CreateRoleBindingOperation{
SubjectKind: string(rb.Spec.Subject.Kind),
SubjectName: rb.Spec.Subject.Name,
RoleKind: string(roleRef.Kind),
RoleName: roleRef.Name,
},
},
})
}
if len(operations) == 0 {
return
}
err := b.zClient.Mutate(ctx, &v1.MutateRequest{
Namespace: rb.Namespace,
Operations: operations,
})
if err != nil {
status = "failure"
b.logger.Error("failed to write role binding to zanzana",
"err", err,
"namespace", rb.Namespace,
"name", rb.Name,
"subject", rb.Spec.Subject.Name,
"roleRefs", rb.Spec.RoleRefs,
)
}
}(rb.DeepCopy()) // Pass a copy of the object
}
// AfterRoleBindingDelete is a post-delete hook that removes the role binding from Zanzana (openFGA)
func (b *IdentityAccessManagementAPIBuilder) AfterRoleBindingDelete(obj runtime.Object, _ *metav1.DeleteOptions) {
if b.zClient == nil {
return
}
rb, ok := obj.(*iamv0.RoleBinding)
if !ok {
b.logger.Error("failed to convert object to RoleBinding type", "object", obj)
return
}
operation := "delete"
// Grab a ticket to write to Zanzana
// This limits the amount of concurrent connections to Zanzana
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds())
go func(rb *iamv0.RoleBinding) {
start := time.Now()
status := "success"
defer func() {
// Release the ticket after write is done
<-b.zTickets
// Record operation duration and count
hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds())
}()
b.logger.Debug("deleting role binding from zanzana",
"namespace", rb.Namespace,
"name", rb.Name,
"subject", rb.Spec.Subject.Name,
"roleRefs", rb.Spec.RoleRefs,
)
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
defer cancel()
operations := make([]*v1.MutateOperation, 0, len(rb.Spec.RoleRefs))
for _, roleRef := range rb.Spec.RoleRefs {
operations = append(operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_DeleteRoleBinding{
DeleteRoleBinding: &v1.DeleteRoleBindingOperation{
SubjectKind: string(rb.Spec.Subject.Kind),
SubjectName: rb.Spec.Subject.Name,
RoleKind: string(roleRef.Kind),
RoleName: roleRef.Name,
},
},
})
}
if len(operations) == 0 {
return
}
err := b.zClient.Mutate(ctx, &v1.MutateRequest{
Namespace: rb.Namespace,
Operations: operations,
})
if err != nil {
status = "failure"
b.logger.Error("failed to delete role binding from zanzana",
"err", err,
"namespace", rb.Namespace,
"name", rb.Name,
"subject", rb.Spec.Subject.Name,
"roleRefs", rb.Spec.RoleRefs,
)
}
}(rb.DeepCopy()) // Pass a copy of the object
}
// BeginRoleBindingUpdate is a pre-update hook that prepares zanzana updates.
// It performs the zanzana write after K8s update succeeds.
func (b *IdentityAccessManagementAPIBuilder) BeginRoleBindingUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) {
if b.zClient == nil {
return nil, nil
}
// Extract role bindings from both old and new objects
oldRB, ok := oldObj.(*iamv0.RoleBinding)
if !ok {
return nil, nil
}
newRB, ok := obj.(*iamv0.RoleBinding)
if !ok {
return nil, nil
}
if oldRB.Spec.Subject.Name == newRB.Spec.Subject.Name && roleRefsEqual(oldRB.Spec.RoleRefs, newRB.Spec.RoleRefs) {
return nil, nil // No changes to the role binding
}
if newRB.Spec.Subject.Name == "" {
b.logger.Error("invalid role binding",
"namespace", newRB.Namespace,
"name", newRB.Name,
"subject", newRB.Spec.Subject.Name,
"roleRefs", newRB.Spec.RoleRefs,
)
return nil, nil
}
// Return a finish function that performs the zanzana write only on success
return func(ctx context.Context, success bool) {
if !success {
return
}
wait := time.Now()
b.zTickets <- true
hooksWaitHistogram.WithLabelValues(resourceType, "update").Observe(time.Since(wait).Seconds())
go func() {
start := time.Now()
status := "success"
defer func() {
<-b.zTickets
// Record operation duration and count
hooksDurationHistogram.WithLabelValues(resourceType, "update", status).Observe(time.Since(start).Seconds())
}()
b.logger.Debug("updating role binding in zanzana",
"namespace", newRB.Namespace,
"name", newRB.Name,
"oldSubject", oldRB.Spec.Subject.Name,
"newSubject", newRB.Spec.Subject.Name,
"oldRoleRefs", oldRB.Spec.RoleRefs,
"newRoleRefs", newRB.Spec.RoleRefs,
)
ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
defer cancel()
operations := make([]*v1.MutateOperation, 0, len(oldRB.Spec.RoleRefs))
for _, roleRef := range oldRB.Spec.RoleRefs {
operations = append(operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_DeleteRoleBinding{
DeleteRoleBinding: &v1.DeleteRoleBindingOperation{
SubjectKind: string(oldRB.Spec.Subject.Kind),
SubjectName: oldRB.Spec.Subject.Name,
RoleKind: string(roleRef.Kind),
RoleName: roleRef.Name,
},
},
})
}
for _, roleRef := range newRB.Spec.RoleRefs {
operations = append(operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_CreateRoleBinding{
CreateRoleBinding: &v1.CreateRoleBindingOperation{
SubjectKind: string(newRB.Spec.Subject.Kind),
SubjectName: newRB.Spec.Subject.Name,
RoleKind: string(roleRef.Kind),
RoleName: roleRef.Name,
},
},
})
}
// Only make the request if there are deletes or writes
if len(operations) == 0 {
b.logger.Debug("no role bindings to update in zanzana", "namespace", newRB.Namespace, "name", newRB.Name)
return
}
err := b.zClient.Mutate(ctx, &v1.MutateRequest{
Namespace: newRB.Namespace,
Operations: operations,
})
if err != nil {
status = "failure"
b.logger.Error("failed to update role binding in zanzana",
"err", err,
"namespace", newRB.Namespace,
"name", newRB.Name,
)
}
}()
}, nil
}
func roleRefsEqual(oldRoleRefs, newRoleRefs []iamv0.RoleBindingspecRoleRef) bool {
if len(oldRoleRefs) != len(newRoleRefs) {
return false
}
oldRoleRefsMap := make(map[string]string)
for _, roleRef := range oldRoleRefs {
oldRoleRefsMap[roleRef.Name] = string(roleRef.Kind)
}
for _, roleRef := range newRoleRefs {
refKind, ok := oldRoleRefsMap[roleRef.Name]
if !ok {
return false
}
if refKind != string(roleRef.Kind) {
return false
}
}
return true
}
@@ -1,448 +0,0 @@
package iam
import (
"context"
"slices"
"sync"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/require"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
)
func TestAfterRoleBindingCreate(t *testing.T) {
var wg sync.WaitGroup
b := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
}
t.Run("should create zanzana entry for role binding", func(t *testing.T) {
wg.Add(1)
roleBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-1",
Namespace: "org-1",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-1",
},
},
},
}
testRoleBinding := func(ctx context.Context, req *v1.MutateRequest) error {
defer wg.Done()
require.NotNil(t, req)
require.NotNil(t, req.Operations)
require.Len(t, req.Operations, 1)
require.Equal(t, "org-1", req.Namespace)
expectedOperation := &v1.MutateOperation{
Operation: &v1.MutateOperation_CreateRoleBinding{
CreateRoleBinding: &v1.CreateRoleBindingOperation{
SubjectKind: "user",
SubjectName: "user-1",
RoleKind: "role",
RoleName: "role-1",
},
},
}
actualCreate := req.Operations[0].Operation.(*v1.MutateOperation_CreateRoleBinding).CreateRoleBinding
expectedCreate := expectedOperation.Operation.(*v1.MutateOperation_CreateRoleBinding).CreateRoleBinding
require.Equal(t, expectedCreate.SubjectKind, actualCreate.SubjectKind)
require.Equal(t, expectedCreate.SubjectName, actualCreate.SubjectName)
require.Equal(t, expectedCreate.RoleKind, actualCreate.RoleKind)
require.Equal(t, expectedCreate.RoleName, actualCreate.RoleName)
return nil
}
b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBinding}
b.AfterRoleBindingCreate(&roleBinding, nil)
wg.Wait()
})
t.Run("should not write to zanzana when zClient is nil", func(t *testing.T) {
builder := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
zClient: nil,
}
roleBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-3",
Namespace: "org-3",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-3",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-3",
},
},
},
}
// Should not panic or error when zClient is nil
builder.AfterRoleBindingCreate(&roleBinding, nil)
})
}
func TestBeginRoleBindingUpdate(t *testing.T) {
var wg sync.WaitGroup
b := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
}
t.Run("should update zanzana entry when role binding changed", func(t *testing.T) {
wg.Add(1)
oldBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-1",
Namespace: "org-1",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-foo",
},
{
Kind: "role",
Name: "role-2",
},
},
},
}
newBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-1",
Namespace: "org-1",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-bar",
},
},
},
}
testRoleBindingUpdate := func(ctx context.Context, req *v1.MutateRequest) error {
defer wg.Done()
require.NotNil(t, req)
require.Equal(t, "org-1", req.Namespace)
require.NotNil(t, req.Operations)
require.Len(t, req.Operations, 3)
// Should write new binding and delete old one
require.True(t, containsOperation(req.Operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_DeleteRoleBinding{
DeleteRoleBinding: &v1.DeleteRoleBindingOperation{
SubjectKind: "user",
SubjectName: "user-1",
RoleKind: "role",
RoleName: "role-foo",
},
},
}))
require.True(t, containsOperation(req.Operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_CreateRoleBinding{
CreateRoleBinding: &v1.CreateRoleBindingOperation{
SubjectKind: "user",
SubjectName: "user-1",
RoleKind: "role",
RoleName: "role-bar",
},
},
}))
return nil
}
b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBindingUpdate}
finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
require.NoError(t, err)
require.NotNil(t, finishFunc)
finishFunc(context.Background(), true)
wg.Wait()
})
t.Run("should return nil finish func when bindings are identical", func(t *testing.T) {
oldBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-2",
Namespace: "org-2",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-1",
},
},
},
}
newBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-2",
Namespace: "org-2",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-1",
},
},
},
}
writeCalled := false
testNoWriteOnNoChange := func(ctx context.Context, req *v1.MutateRequest) error {
writeCalled = true
require.Fail(t, "Write should not be called when bindings are identical")
return nil
}
b.zClient = &FakeZanzanaClient{mutateCallback: testNoWriteOnNoChange}
finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
require.NoError(t, err)
require.Nil(t, finishFunc) // Should return nil when bindings are identical
// Verify write was never called
time.Sleep(100 * time.Millisecond)
require.False(t, writeCalled, "Write callback should not be called when bindings are identical")
})
t.Run("should return nil finish func when new binding has empty subject name", func(t *testing.T) {
oldBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-8",
Namespace: "org-8",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-1",
},
},
},
}
newBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-8",
Namespace: "org-8",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "",
Name: "", // Empty name - should cause early return
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-1",
},
},
},
}
writeCalled := false
testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.MutateRequest) error {
writeCalled = true
require.Fail(t, "Write should not be called when new binding has empty subject name")
return nil
}
b.zClient = &FakeZanzanaClient{mutateCallback: testNoWriteOnInvalidBinding}
finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
require.NoError(t, err)
require.Nil(t, finishFunc) // Should return nil when new binding has empty subject name
// Verify write was never called
time.Sleep(100 * time.Millisecond)
require.False(t, writeCalled, "Write callback should not be called when new binding has empty subject name")
})
}
func TestAfterRoleBindingDelete(t *testing.T) {
var wg sync.WaitGroup
b := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
}
t.Run("should delete zanzana entry for team binding with member permission", func(t *testing.T) {
wg.Add(1)
roleBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-1",
Namespace: "org-1",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-1",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-1",
},
{
Kind: "role",
Name: "role-2",
},
},
},
}
testRoleBindingDelete := func(ctx context.Context, req *v1.MutateRequest) error {
defer wg.Done()
require.NotNil(t, req)
require.Equal(t, "org-1", req.Namespace)
// Should have deletes but no writes
require.NotNil(t, req.Operations)
require.Len(t, req.Operations, 2)
require.True(t, containsOperation(req.Operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_DeleteRoleBinding{
DeleteRoleBinding: &v1.DeleteRoleBindingOperation{
SubjectKind: "user",
SubjectName: "user-1",
RoleKind: "role",
RoleName: "role-1",
},
},
}))
require.True(t, containsOperation(req.Operations, &v1.MutateOperation{
Operation: &v1.MutateOperation_DeleteRoleBinding{
DeleteRoleBinding: &v1.DeleteRoleBindingOperation{
SubjectKind: "user",
SubjectName: "user-1",
RoleKind: "role",
RoleName: "role-2",
},
},
}))
return nil
}
b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBindingDelete}
b.AfterRoleBindingDelete(&roleBinding, nil)
wg.Wait()
})
t.Run("should not delete from zanzana when zClient is nil", func(t *testing.T) {
builder := &IdentityAccessManagementAPIBuilder{
logger: log.NewNopLogger(),
zTickets: make(chan bool, 1),
zClient: nil,
}
roleBinding := iamv0.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "binding-3",
Namespace: "org-3",
},
Spec: iamv0.RoleBindingSpec{
Subject: iamv0.RoleBindingspecSubject{
Kind: "user",
Name: "user-3",
},
RoleRefs: []iamv0.RoleBindingspecRoleRef{
{
Kind: "role",
Name: "role-3",
},
},
},
}
// Should not panic or error when zClient is nil
builder.AfterRoleBindingDelete(&roleBinding, nil)
})
}
func containsOperation(operations []*v1.MutateOperation, operation *v1.MutateOperation) bool {
return slices.ContainsFunc(operations, func(o *v1.MutateOperation) bool {
switch operation.Operation.(type) {
case *v1.MutateOperation_DeleteRoleBinding:
deleteOperation := operation.Operation.(*v1.MutateOperation_DeleteRoleBinding)
deleteO, ok := o.Operation.(*v1.MutateOperation_DeleteRoleBinding)
if !ok {
return false
}
return deleteO.DeleteRoleBinding.SubjectKind == deleteOperation.DeleteRoleBinding.SubjectKind &&
deleteO.DeleteRoleBinding.SubjectName == deleteOperation.DeleteRoleBinding.SubjectName &&
deleteO.DeleteRoleBinding.RoleKind == deleteOperation.DeleteRoleBinding.RoleKind &&
deleteO.DeleteRoleBinding.RoleName == deleteOperation.DeleteRoleBinding.RoleName
case *v1.MutateOperation_CreateRoleBinding:
createOperation := operation.Operation.(*v1.MutateOperation_CreateRoleBinding)
createO, ok := o.Operation.(*v1.MutateOperation_CreateRoleBinding)
if !ok {
return false
}
return createO.CreateRoleBinding.SubjectKind == createOperation.CreateRoleBinding.SubjectKind &&
createO.CreateRoleBinding.SubjectName == createOperation.CreateRoleBinding.SubjectName &&
createO.CreateRoleBinding.RoleKind == createOperation.CreateRoleBinding.RoleKind &&
createO.CreateRoleBinding.RoleName == createOperation.CreateRoleBinding.RoleName
}
return false
})
}
-4
View File
@@ -64,10 +64,6 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri
}
func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) {
if !cfg.OpenFeature.APIEnabled {
return nil, nil
}
var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider
var err error
if cfg.OpenFeature.ProviderType == setting.StaticProviderType {
-4
View File
@@ -172,8 +172,6 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) {
for i, val := range all {
branch := val.Branch()
url := val.URL()
path := val.Path()
settings.Items[i] = provisioning.RepositoryView{
Name: val.Name,
@@ -181,8 +179,6 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) {
Type: val.Spec.Type,
Target: val.Spec.Sync.Target,
Branch: branch,
URL: url,
Path: path,
Workflows: val.Spec.Workflows,
}
}
+2
View File
@@ -3,6 +3,7 @@ package apiregistry
import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
"github.com/grafana/grafana/pkg/registry/apis/collections"
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
@@ -59,6 +60,7 @@ var WireSet = wire.NewSet(
provisioningExtras,
// Each must be added here *and* in the ServiceSink above
apiextensions.RegisterAPIService,
dashboardinternal.RegisterAPIService,
dashboardsnapshot.RegisterAPIService,
datasource.RegisterAPIService,

Some files were not shown because too many files have changed in this diff Show More