Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b910e2e39e | ||
|
|
8a93d5331a | ||
|
|
eb84c5d02f | ||
|
|
ba380fd15d | ||
|
|
646fb2aa35 | ||
|
|
34e3c20250 | ||
|
|
48a8d54794 | ||
|
|
62d83a1ba9 | ||
|
|
5626dc50f8 | ||
|
|
eea50c8e9b | ||
|
|
df2f528612 | ||
|
|
7b8191ba42 | ||
|
|
ba58506ffd | ||
|
|
f12cc5411d | ||
|
|
8daa228083 | ||
|
|
cd797b6789 | ||
|
|
c7ea3d17cc | ||
|
|
8e4be891c5 | ||
|
|
763067f8e1 | ||
|
|
eedb613a5e | ||
|
|
42d3673d04 | ||
|
|
8e73cc2f70 | ||
|
|
80fc87339a | ||
|
|
8515bcc6b0 | ||
|
|
f872fd7f2f | ||
|
|
4c869a21a4 | ||
|
|
cffca37999 | ||
|
|
95174454e3 | ||
|
|
1c8f4a745f | ||
|
|
fe3e2bf9cb | ||
|
|
f6dfbe0e15 | ||
|
|
b90b8f2a34 | ||
|
|
b473524787 | ||
|
|
cb05a4ae1b | ||
|
|
84a07be6e4 | ||
|
|
a8aef11926 | ||
|
|
f116539541 | ||
|
|
a455f9700d | ||
|
|
399370bc2c | ||
|
|
b08e7bc373 | ||
|
|
7eb467e561 | ||
|
|
d732bb2751 | ||
|
|
c29ed31c7a | ||
|
|
ebaccc781b | ||
|
|
ca8cad68c8 | ||
|
|
51d562eb81 | ||
|
|
4130bd9cd3 | ||
|
|
759d49a1df | ||
|
|
84fbe6bc7b | ||
|
|
9606e9c51c | ||
|
|
d49993ddab | ||
|
|
f0a394e67b | ||
|
|
c94bf34d0b | ||
|
|
5ba3139d4a | ||
|
|
e1a2f178e7 | ||
|
|
ae2e5f0df7 | ||
|
|
21c1d9aedd | ||
|
|
8c7170727b | ||
|
|
cef4449f14 | ||
|
|
a3dacabedf | ||
|
|
291e3ea9cf | ||
|
|
5538dfe73d | ||
|
|
513b81a531 | ||
|
|
136a0eb4d6 | ||
|
|
bfda534825 | ||
|
|
d226c35904 | ||
|
|
a99946d921 | ||
|
|
2544a3441e |
@@ -185,6 +185,7 @@
|
||||
/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
|
||||
|
||||
@@ -71,6 +71,7 @@ public/css/*.min.css
|
||||
.vs/
|
||||
.cursor/
|
||||
.devcontainer/
|
||||
.claude/
|
||||
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
+1
-1
@@ -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).
|
||||
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).
|
||||
|
||||
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,6 +43,12 @@ 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,6 +1690,20 @@ 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",
|
||||
|
||||
@@ -5,6 +5,7 @@ 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)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# 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.
|
||||
- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
|
||||
- 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 of the benefits of the migration assistant are:
|
||||
Some 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:
|
||||
|
||||
- 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 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).
|
||||
- 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="important">}}
|
||||
{{< admonition type="note" >}}
|
||||
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 >}}
|
||||
|
||||
|
||||
@@ -74,6 +74,21 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
|
||||
configure-grafana-azure-auth:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/
|
||||
configure-grafana-azure:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#azure
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#azure
|
||||
configure-grafana-azure-auth-scopes:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana
|
||||
---
|
||||
|
||||
# Configure the Microsoft SQL Server data source
|
||||
@@ -138,14 +153,19 @@ If you're using an older version of Microsoft SQL Server like 2008 and 2008R2, y
|
||||
|
||||
**Authentication:**
|
||||
|
||||
| Authentication Type | Description | Credentials / Fields |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username<br>- **Password**: SQL Server password |
|
||||
| **Windows Authentication**<br>(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials |
|
||||
| **Windows AD**<br>(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`<br>- **Password**: Active Directory password |
|
||||
| **Windows AD**<br>(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`<br>- **Keytab file path**: Path to your keytab file |
|
||||
| **Windows AD**<br>(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) |
|
||||
| **Windows AD**<br>(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`<br>- **Credential cache file path**: e.g., `/home/grot/cache.json` |
|
||||
{{< admonition type="note" >}}
|
||||
In order to use Azure AD Authentication the toggle `auth.azure_auth_enabled` must be set to `true` in the Grafana configuration file.
|
||||
{{< /admonition >}}
|
||||
|
||||
| Authentication Type | Description | Credentials / Fields |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username<br>- **Password**: SQL Server password |
|
||||
| **Windows Authentication**<br>(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials |
|
||||
| **Windows AD**<br>(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`<br>- **Password**: Active Directory password |
|
||||
| **Windows AD**<br>(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`<br>- **Keytab file path**: Path to your keytab file |
|
||||
| **Windows AD**<br>(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) |
|
||||
| **Windows AD**<br>(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`<br>- **Credential cache file path**: e.g., `/home/grot/cache.json` |
|
||||
| **Azure Entra ID (formerly Azure AD) Authentication** | Authenticates the data source using Azure authentication methods. | Details on the supported authentication methods and how to configure them can be found in the [Azure authentication section](./index.md#azure-entra-id-formerly-azure-ad-authentication). |
|
||||
|
||||
**Additional settings:**
|
||||
|
||||
@@ -185,6 +205,123 @@ After configuring your MSSQL data source options, click **Save & test** at the b
|
||||
|
||||
**Database Connection OK**
|
||||
|
||||
### Azure Entra ID (formerly Azure AD) Authentication
|
||||
|
||||
The following Azure authentication methods are supported:
|
||||
|
||||
- Current User authentication
|
||||
- App Registration
|
||||
- Managed Identity
|
||||
- Azure Entra Password
|
||||
|
||||
The Azure SQL Server that you are connecting to should support Azure Entra authentication to support adding the App Registration as a user in the database. For configuration details, refer to the [Azure SQL documentation](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure?view=azuresql&tabs=azure-portal).
|
||||
|
||||
#### Current User authentication
|
||||
|
||||
This is the recommended authentication mechanism when working with SQL Server instances that are hosted in Azure. It allows users to be authenticated to and query the database using their own credentials rather than long-lived credentials.
|
||||
|
||||
This authentication method requires your Grafana instance to be configured with Azure Entra ID (formerly Active Directory) authentication for login. With Azure Entra ID login, this method can be used to forward the currently logged in user’s credentials to the data source. The users credentials will then be used when requesting data from the data source. For details on how to configure your Grafana instance using Azure Entra refer to the [documentation](ref:configure-grafana-azure-auth).
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Additional configuration is required to ensure that the App Registration used to login a user via Azure provides an access token with the permissions required by the data source.
|
||||
|
||||
The App Registration must be configured to issue both **Access Tokens** and **ID Tokens**.
|
||||
|
||||
1. In the Azure Portal, open the App Registration that requires configuration.
|
||||
2. Select **Authentication** in the side menu.
|
||||
3. Under **Implicit grant and hybrid flows** check both the **Access tokens** and **ID tokens** boxes.
|
||||
4. Save the changes to ensure the App Registration is updated.
|
||||
|
||||
The App Registration must also be configured with additional **API Permissions** to provide authenticated users with access to the APIs utilised by the data source.
|
||||
|
||||
1. In the Azure Portal, open the App Registration that requires configuration.
|
||||
1. Select **API Permissions** in the side menu.
|
||||
1. Ensure the `openid`, `profile`, `email`, and `offline_access` permissions are present under the **Microsoft Graph** section. If not, they must be added.
|
||||
1. Select **Add a permission** and choose the following permissions. They must be added individually. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information.
|
||||
- Select **APIs my organization uses** > Search for **Azure SQL** and select it > **Delegated permissions** > `user_impersonation` > **Add permissions**
|
||||
|
||||
After all permissions have been added, the Azure authentication section in Grafana must be updated. The `scopes` section must be updated to include the `.default` scope to ensure that a token with access to all APIs declared on the App Registration is requested by Grafana. After updated the scopes value should equal: `.default openid email profile`.
|
||||
{{< /admonition >}}
|
||||
|
||||
This method of authentication doesn't inherently support all backend functionality as a user's credentials won't be in scope. Affected functionality includes alerting, reporting, and recorded queries. Also, note that query and resource caching is disabled by default for data sources using current user authentication.
|
||||
|
||||
**To enable current user authentication for Grafana:**
|
||||
|
||||
1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
user_identity_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Current User**.
|
||||
|
||||
### App Registration
|
||||
|
||||
You must create an app registration and service principal in Azure Entra to authenticate the data source.
|
||||
For configuration details, refer to the [Azure documentation for service principals](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in).
|
||||
|
||||
After the app registration has been created, make note of the tenant ID, client ID, and client secret. Take the following steps to add the app registration as a SQL user:
|
||||
|
||||
1. Connect to your Azure SQL database as a user with administrative permissions (the user used here must have the ability to read your Azure Entra directory e.g. by possessing the `Directory Readers` role).
|
||||
2. Run `CREATE USER [$IDENTITY_NAME] FROM EXTERNAL PROVIDER;`, substituting `IDENTITY_NAME` with the app registration name.
|
||||
3. Grant the created user the appropriate level of permissions for your use-case. It is recommended that users configured for data sources only have reader permissions.
|
||||
|
||||
After the appropriate permissions have been granted, configure the SQL Server data source to use the app registration:
|
||||
|
||||
1. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **App Registration**.
|
||||
2. Set the **Azure Cloud** value to the correct value. If you are using the Azure public cloud this will be **Azure**.
|
||||
3. Set the **Directory (tenant) ID**, **Application (client) ID**, and **Client Secret** values to those for your app registration.
|
||||
|
||||
### Managed Identity
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Managed Identity is available only in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or Grafana OSS/Enterprise when deployed in Azure. It is not available in Grafana Cloud.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can use managed identity to configure SQL Server in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM.
|
||||
This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations.
|
||||
For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview).
|
||||
|
||||
**To enable managed identity for Grafana:**
|
||||
|
||||
1. Set the `managed_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
managed_identity_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Managed Identity**.
|
||||
|
||||
This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to SQL Server.
|
||||
|
||||
3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure) to allow a user-assigned managed identity to be used instead of the default system-assigned identity.
|
||||
|
||||
Ensure that the managed identity used is added to your Azure SQL instance as a user.
|
||||
|
||||
### Azure Entra Password
|
||||
|
||||
{{< admonition type="warning" >}}
|
||||
Azure Entra Password is not a recommended authentication mechanism as it requires configuration using a single users password. Consider an alternative authentication method such as current user authentication or app registration.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can connect to an Azure SQL database using the username and password of a user that has permissions in the desired database. This also requires an app registration to be configured with access to the database.
|
||||
|
||||
**To enable Azure Entra password for Grafana:**
|
||||
|
||||
1. Set the `azure_entra_password_credentials_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure).
|
||||
|
||||
```ini
|
||||
[azure]
|
||||
azure_entra_password_credentials_enabled = true
|
||||
```
|
||||
|
||||
2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Azure Entra Password**.
|
||||
3. Set the **User ID** value to the username of the user in the Azure SQL database.
|
||||
4. Set the **Application Client ID** to the client ID of the app registration that has been added to the Azure SQL database
|
||||
5. Set the **Password** value to the password of the user in the Azure SQL database.
|
||||
|
||||
### Min time interval
|
||||
|
||||
The **Min time interval** setting defines a lower limit for the [`$__interval`](ref:add-template-variables-interval) and [`$__interval_ms`][add-template-variables-interval_ms] variables.
|
||||
|
||||
@@ -767,12 +767,12 @@ Status Codes:
|
||||
|
||||
Deletes a dashboard via the dashboard uid.
|
||||
|
||||
- namespace: to read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/developers/http_api/apis/).
|
||||
- uid: the unique identifier of the dashboard to update. this will be the _name_ in the dashboard response
|
||||
- **`namespace`**: To read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/developers/http_api/apis/).
|
||||
- **`uid`**: The unique identifier of the dashboard to update. This is the `metadata.name` field in the dashboard response and _not_ the `metadata.uid` field.
|
||||
|
||||
**Required permissions**
|
||||
|
||||
See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation.
|
||||
See note in the [introduction](#new-dashboard-apis) for an explanation.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
| Action | Scope |
|
||||
|
||||
@@ -1933,6 +1933,10 @@ The initial delay before retrying a failed alert evaluation. Default is `1s`.
|
||||
|
||||
This value is the starting point for exponential backoff.
|
||||
|
||||
#### `initialization_timeout`
|
||||
|
||||
Allows the context deadline for the `AlertNG` service to be configurable. The default timeout is 30s.
|
||||
|
||||
#### `max_retry_delay`
|
||||
|
||||
The maximum delay between retries during exponential backoff. Default is `4s`.
|
||||
|
||||
@@ -66,6 +66,7 @@ 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
|
||||
@@ -95,9 +96,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
-1
@@ -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
|
||||
|
||||
+51
-1
@@ -4,6 +4,8 @@ import { test, expect, E2ESelectorGroups, DashboardPage, DashboardPageArgs } fro
|
||||
|
||||
import testDashboard from '../dashboards/DashboardWithAllConditionalRendering.json';
|
||||
|
||||
import { checkRepeatedPanelTitles } from './utils';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: true,
|
||||
@@ -93,7 +95,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/default/dashboards/${uid}`);
|
||||
await request.delete(`/apis/dashboard.grafana.app/v1beta1/namespaces/stacks-12345/dashboards/${uid}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -407,4 +409,52 @@ 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})`),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -414,13 +414,13 @@ test.describe(
|
||||
).toBeVisible();
|
||||
|
||||
// Go back to dashboard options
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
// Expand layouts section
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select tabs layout
|
||||
await page.getByLabel('Tabs').click();
|
||||
await page.getByLabel('layout-selection-option-Tabs').click();
|
||||
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible();
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible();
|
||||
@@ -518,14 +518,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('Test row 1');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 1')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name
|
||||
await expect(
|
||||
@@ -543,14 +543,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('Test row 2');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 2')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name + 1 to avoid duplicates
|
||||
await expect(
|
||||
@@ -755,13 +755,13 @@ test.describe(
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible();
|
||||
|
||||
// Go back to dashboard options
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
// Expand layouts section
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select rows layout
|
||||
await page.getByLabel('Rows').click();
|
||||
await page.getByLabel('layout-selection-option-Rows').click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.DashboardRow.wrapper('New tab 1'))
|
||||
@@ -903,14 +903,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('Test tab 1');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 1')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
|
||||
@@ -923,14 +923,14 @@ test.describe(
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('Test tab 2');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// clear the title input to simulate no title and click away to trigger onBlur
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 2')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
|
||||
.fill('');
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click();
|
||||
|
||||
// title should be set to a default name + 1 to avoid duplicates
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
|
||||
|
||||
@@ -21,6 +21,7 @@ test.describe(
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
|
||||
// Should be able to click Variables item in outline to see add variable button
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
|
||||
@@ -28,6 +29,8 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
|
||||
// Clicking a panel should scroll that panel in view
|
||||
await expect(page.getByText('Dashboard panel 48')).toBeHidden();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click();
|
||||
|
||||
@@ -22,6 +22,9 @@ test.describe(
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
|
||||
|
||||
const undockButton = page.getByRole('button', { name: 'Undock menu' });
|
||||
await undockButton.click();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -199,6 +199,7 @@ test.describe(
|
||||
.click();
|
||||
|
||||
// Open the modal editor in the side pane
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click();
|
||||
await openModal(dashboardPage, selectors);
|
||||
|
||||
@@ -32,9 +32,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
@@ -50,6 +50,7 @@ test.describe(
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await checkAutoGridLayoutInputs(dashboardPage, selectors);
|
||||
});
|
||||
@@ -63,9 +64,10 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
// Get initial positions - standard width should have panels on different rows
|
||||
const firstPanelTop = await getPanelTop(dashboardPage, selectors);
|
||||
@@ -98,6 +100,7 @@ test.describe(
|
||||
await page.reload();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
@@ -123,9 +126,10 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth)
|
||||
@@ -134,7 +138,7 @@ test.describe(
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
|
||||
.fill('900');
|
||||
.fill('1100');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth)
|
||||
.blur();
|
||||
@@ -148,12 +152,13 @@ test.describe(
|
||||
await verifyPanelsStackedVertically(dashboardPage, selectors);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth
|
||||
)
|
||||
).toHaveValue('900');
|
||||
).toHaveValue('1100');
|
||||
|
||||
await verifyPanelsStackedVertically(dashboardPage, selectors);
|
||||
|
||||
@@ -180,9 +185,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
|
||||
@@ -198,6 +203,7 @@ test.describe(
|
||||
await verifyPanelsStackedVertically(dashboardPage, selectors);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns)
|
||||
@@ -215,9 +221,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
|
||||
|
||||
@@ -250,6 +256,7 @@ test.describe(
|
||||
}).toPass();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight)
|
||||
@@ -270,9 +277,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
|
||||
|
||||
@@ -303,6 +310,7 @@ test.describe(
|
||||
}).toPass();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
@@ -327,9 +335,9 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
|
||||
// Set narrow column width first to ensure panels fit horizontally
|
||||
await dashboardPage
|
||||
@@ -357,6 +365,7 @@ test.describe(
|
||||
}).toPass();
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen)
|
||||
|
||||
@@ -37,9 +37,10 @@ test.describe(
|
||||
},
|
||||
() => {
|
||||
test('can enable repeats', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(page, selectors, 'Auto grid repeats - add repeats');
|
||||
await importTestDashboard(page, selectors, 'Auto-grid repeats - add repeats');
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
|
||||
@@ -70,11 +71,12 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - update on variable change',
|
||||
'Auto-grid repeats - update on variable change',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
@@ -113,6 +115,7 @@ test.describe(
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
|
||||
@@ -138,11 +141,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - update through panel editor',
|
||||
'Auto-grid repeats - update through panel editor',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -202,11 +207,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - update through directly loaded panel editor',
|
||||
'Auto-grid repeats - update through directly loaded panel editor',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
|
||||
@@ -257,11 +264,12 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - move repeated panels',
|
||||
'Auto-grid repeats - move repeated panels',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
|
||||
@@ -304,11 +312,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - move repeated panels',
|
||||
'Auto-grid repeats - move repeated panels 2',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -332,9 +342,7 @@ test.describe(
|
||||
|
||||
const repeatedPanelUrl = page.url();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`))
|
||||
@@ -367,11 +375,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - view embedded repeated panel',
|
||||
'Auto-grid repeats - view embedded repeated panel',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -393,11 +403,13 @@ test.describe(
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Auto grid repeats - remove repeats',
|
||||
'Auto-grid repeats - remove repeats',
|
||||
JSON.stringify(testV2DashWithRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
await switchToAutoGrid(page);
|
||||
await saveDashboard(dashboardPage, page, selectors);
|
||||
await page.reload();
|
||||
@@ -453,5 +465,5 @@ test.describe(
|
||||
|
||||
async function switchToAutoGrid(page: Page) {
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
await page.getByLabel('Auto grid').click();
|
||||
await page.getByLabel('layout-selection-option-Auto grid').click();
|
||||
}
|
||||
|
||||
@@ -303,9 +303,7 @@ test.describe(
|
||||
|
||||
const repeatedPanelUrl = page.url();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`))
|
||||
|
||||
@@ -316,9 +316,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// repeated panel in original tab repeat
|
||||
await dashboardPage
|
||||
@@ -341,9 +339,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2'))
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// repeated panel in repeated tab
|
||||
await dashboardPage
|
||||
|
||||
@@ -21,11 +21,7 @@ test.describe(
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
|
||||
// Check that current dashboard title is visible in breadcrumb
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering'))
|
||||
).toBeVisible();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
|
||||
|
||||
const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input');
|
||||
await expect(titleInput).toHaveValue('Annotation filtering');
|
||||
|
||||
@@ -48,6 +48,7 @@ export const flows = {
|
||||
},
|
||||
async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) {
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
|
||||
@@ -96,12 +97,18 @@ export async function checkRepeatedPanelTitles(
|
||||
dashboardPage: DashboardPage,
|
||||
selectors: E2ESelectorGroups,
|
||||
title: string,
|
||||
options: Array<string | number>
|
||||
options: Array<string | number>,
|
||||
expectHidden = false
|
||||
) {
|
||||
for (const option of options) {
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`${title}${option}`))
|
||||
).toBeVisible();
|
||||
const titleLocator = dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.Panels.Panel.title(`${title}${option}`)
|
||||
);
|
||||
if (expectHidden) {
|
||||
await expect(titleLocator).toBeHidden();
|
||||
} else {
|
||||
await expect(titleLocator).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ const NUM_NESTED_DASHBOARDS = 60;
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import testDashboard from '../dashboards/TestDashboard.json';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
sharingDashboardImage: true, // Enable the export image feature
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import testDashboard from '../dashboards/DashboardLiveTest.json';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { SnapshotCreateResponse } from '../../public/app/features/dashboard/serv
|
||||
test.use({
|
||||
featureToggles: {
|
||||
scenes: true,
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const DASHBOARD_UID = 'HYaGDGIMk';
|
||||
test.use({
|
||||
timezoneId: 'Pacific/Easter',
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ const TIMEZONE_DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ test.use({
|
||||
origins: [],
|
||||
},
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import testDashboard from '../dashboards/TestDashboard.json';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Test variable output';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ async function assertPreviewValues(
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Test variable output';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ async function assertPreviewValues(
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Templating - Nested Template Variables';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const DASHBOARD_NAME = 'Test variable output';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ const DASHBOARD_UID = 'ZqZnVvFZz';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
dashboardScene: false, // this test is for the old sharing modal only used when scenes is turned off
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ const DASHBOARD_UID = 'yBCC3aKGk';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ const PAGE_UNDER_TEST = 'AejrN1AMz';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: process.env.KUBERNETES_DASHBOARDS === 'true',
|
||||
kubernetesDashboards: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
kubernetesDashboardsV2: process.env.FORCE_V2_DASHBOARDS_API === 'true',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3308,6 +3308,170 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -5091,6 +5255,80 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5122,6 +5360,39 @@
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1907,21 +1907,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": {
|
||||
"react-hooks/rules-of-hooks": {
|
||||
"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
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
// @ts-check
|
||||
// TODO: Migrate to Typescript.
|
||||
// @ts-nocheck
|
||||
const emotionPlugin = require('@emotion/eslint-plugin');
|
||||
const restrictedGlobals = require('confusing-browser-globals');
|
||||
const importPlugin = require('eslint-plugin-import');
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"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",
|
||||
@@ -166,6 +167,7 @@
|
||||
"@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,10 +110,9 @@ 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,4 +1,7 @@
|
||||
{
|
||||
"exclude": ["dist", "node_modules", "tests", "**/*.test.ts*", "**/*.story.tsx"],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -612,8 +612,8 @@ export type GetSearchApiArg = {
|
||||
tags?: string[];
|
||||
/** find dashboards that reference a given libraryPanel */
|
||||
libraryPanel?: string;
|
||||
/** permission needed for the resource (View, Edit, Admin) */
|
||||
permission?: 'View' | 'Edit' | 'Admin';
|
||||
/** permission needed for the resource (view, edit, admin) */
|
||||
permission?: 'view' | 'edit' | 'admin';
|
||||
/** sortable field */
|
||||
sort?: string;
|
||||
/** number of results to return */
|
||||
|
||||
+4
@@ -1581,6 +1581,8 @@ 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:
|
||||
@@ -1598,6 +1600,8 @@ 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,4 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts*"]
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts*"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +91,10 @@ describe('EventBus', () => {
|
||||
it('Supports legacy events', () => {
|
||||
const bus = new EventBusSrv();
|
||||
const events: LegacyEventPayload[] = [];
|
||||
const handler = (event: LegacyEventPayload) => {
|
||||
events.push(event);
|
||||
const handler = (event?: LegacyEventPayload) => {
|
||||
if (event) {
|
||||
events.push(event);
|
||||
}
|
||||
};
|
||||
|
||||
bus.on(legacyEvent, handler);
|
||||
@@ -111,7 +113,9 @@ describe('EventBus', () => {
|
||||
const newEvents: AlertSuccessEvent[] = [];
|
||||
|
||||
bus.on(legacyEvent, (event) => {
|
||||
legacyEvents.push(event);
|
||||
if (event) {
|
||||
legacyEvents.push(event);
|
||||
}
|
||||
});
|
||||
|
||||
bus.subscribe(AlertSuccessEvent, (event) => {
|
||||
|
||||
@@ -133,12 +133,12 @@ export interface LegacyEmitter {
|
||||
/**
|
||||
* @deprecated use $on
|
||||
*/
|
||||
off<T>(event: AppEvent<T> | string, handler: (payload?: T) => void): void;
|
||||
off<T>(event: AppEvent<T> | string, handler: LegacyEventHandler<T>): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface LegacyEventHandler<T> {
|
||||
(payload: T): void;
|
||||
(payload?: T): void;
|
||||
wrapper?: (event: BusEvent) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,6 +381,11 @@ 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,19 +1,27 @@
|
||||
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;
|
||||
/** The first frame that set's this value */
|
||||
preferredVisualisationType?: PreferredVisualisationType;
|
||||
/* 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[];
|
||||
|
||||
/* --- DEPRECATED FIELDS BELOW --- */
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
|
||||
@@ -23,60 +31,114 @@ export interface PanelDataSummary {
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
|
||||
stringFieldCount: number;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
|
||||
hasNumberField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
hasTimeField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
hasNumberField?: 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 {
|
||||
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,
|
||||
};
|
||||
export function getPanelDataSummary(frames?: DataFrame[]): PanelDataSummary {
|
||||
return new PanelDataSummaryImpl(frames);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ interface IndexOptions {
|
||||
asPercentile: boolean;
|
||||
}
|
||||
|
||||
const defaultReduceOptions: ReduceOptions = {
|
||||
const defaultNumericVizOptions: ReduceOptions = {
|
||||
reducer: ReducerID.sum,
|
||||
};
|
||||
|
||||
@@ -149,10 +149,10 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
||||
|
||||
switch (mode) {
|
||||
case CalculateFieldMode.ReduceRow:
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultNumericVizOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.CumulativeFunctions:
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultReduceOptions), data);
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultNumericVizOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.WindowFunctions:
|
||||
creator = getWindowCreator(defaults(options.window, defaultWindowOptions), data);
|
||||
|
||||
@@ -896,6 +896,42 @@ 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,6 +210,8 @@ 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];
|
||||
@@ -223,13 +225,17 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine
|
||||
curMax = curMax * bucketFactor;
|
||||
curMin = curMin * bucketFactor;
|
||||
|
||||
while (curMax < nextMax * 0.999999) {
|
||||
while (curMax < nextMax * 0.999999 && denseMaxs.length < MAX_DENSIFIED_BUCKETS) {
|
||||
denseMaxs.push(curMax);
|
||||
denseMins.push(curMin);
|
||||
|
||||
curMax = curMax * bucketFactor;
|
||||
curMin = curMin * bucketFactor;
|
||||
}
|
||||
|
||||
if (denseMaxs.length >= MAX_DENSIFIED_BUCKETS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +244,10 @@ export function getHistogramFields(frame: DataFrame): HistogramFields | undefine
|
||||
|
||||
for (let i = 0; i < yMaxField.values.length; i++) {
|
||||
let max = yMaxField.values[i];
|
||||
countsByMax.set(max, countsByMax.get(max) + countField.values[i]);
|
||||
let currentCount = countsByMax.get(max);
|
||||
if (currentCount !== undefined) {
|
||||
countsByMax.set(max, currentCount + countField.values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
let fields = {
|
||||
|
||||
@@ -643,6 +643,7 @@ export interface MetricFindValue {
|
||||
value?: string | number;
|
||||
group?: string;
|
||||
expandable?: boolean;
|
||||
properties?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DataSourceGetDrilldownsApplicabilityOptions<TQuery extends DataQuery = DataQuery> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { DataQuery, LogsSortOrder } from '@grafana/schema';
|
||||
|
||||
import { PreferredVisualisationType } from './data';
|
||||
import { SelectableValue } from './select';
|
||||
@@ -84,6 +84,7 @@ export interface ExploreLogsPanelState {
|
||||
// Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized
|
||||
refId?: string;
|
||||
displayedFields?: string[];
|
||||
sortOrder?: LogsSortOrder;
|
||||
}
|
||||
|
||||
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
|
||||
|
||||
+10
-2
@@ -361,6 +361,10 @@ 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;
|
||||
@@ -1091,7 +1095,7 @@ export interface FeatureToggles {
|
||||
graphiteBackendMode?: boolean;
|
||||
/**
|
||||
* Enables the updated Azure Monitor resource picker
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
azureResourcePickerUpdates?: boolean;
|
||||
/**
|
||||
@@ -1150,7 +1154,7 @@ export interface FeatureToggles {
|
||||
pluginStoreServiceLoading?: boolean;
|
||||
/**
|
||||
* Increases panel padding globally
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
newPanelPadding?: boolean;
|
||||
/**
|
||||
@@ -1189,4 +1193,8 @@ export interface FeatureToggles {
|
||||
* @default false
|
||||
*/
|
||||
rudderstackUpgrade?: boolean;
|
||||
/**
|
||||
* Adds support for Kubernetes alerting historian APIs
|
||||
*/
|
||||
kubernetesAlertingHistorian?: boolean;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ 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 +1,7 @@
|
||||
{
|
||||
"exclude": ["dist", "node_modules", "test", "**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const components = {
|
||||
|
||||
A few things to keep in mind:
|
||||
|
||||
- Strive to use e2e selector for all components in grafana/ui.
|
||||
- Strive to use e2e selectors for all components in grafana/ui.
|
||||
- Don't ever delete selectors. Even though a selector may not be used in the Grafana repository, it can still be used in external plugins.
|
||||
- Only create new selector in case you're creating a new piece of UI. If you're changing an existing piece of UI that already has a selector defined, you need to keep using that selector. Otherwise you might break plugin end-to-end tests.
|
||||
- Prefer using string selectors in favour of function selectors. The purpose of the selectors is to provide a canonical way to select elements.
|
||||
|
||||
@@ -57,6 +57,11 @@ export const versionedComponents = {
|
||||
'12.1.0': 'data-testid DashboardEditPaneSplitter primary body',
|
||||
},
|
||||
},
|
||||
Sidebar: {
|
||||
closePane: {
|
||||
'12.4.0': 'data-testid Sidebar close pane',
|
||||
},
|
||||
},
|
||||
EditPaneHeader: {
|
||||
deleteButton: {
|
||||
'12.1.0': 'data-testid EditPaneHeader delete panel',
|
||||
@@ -70,9 +75,6 @@ export const versionedComponents = {
|
||||
duplicate: {
|
||||
'12.1.0': 'data-testid EditPaneHeader duplicate',
|
||||
},
|
||||
backButton: {
|
||||
'12.1.0': 'data-testid EditPaneHeader back',
|
||||
},
|
||||
},
|
||||
TimePicker: {
|
||||
openButton: {
|
||||
|
||||
@@ -183,6 +183,14 @@ export const versionedPages = {
|
||||
url: {
|
||||
[MIN_GRAFANA_VERSION]: (uid: string) => `/d/${uid}`,
|
||||
},
|
||||
Sidebar: {
|
||||
optionsButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar options button',
|
||||
},
|
||||
outlineButton: {
|
||||
'12.4.0': 'data-testid Dashboard Sidebar outline button',
|
||||
},
|
||||
},
|
||||
DashNav: {
|
||||
nav: {
|
||||
[MIN_GRAFANA_VERSION]: 'Dashboard navigation',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@emotion/core": ["./src/types/emotion-core-stub.d.ts"]
|
||||
}
|
||||
},
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"exclude": ["**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"exclude": ["dist", "node_modules", "test", "**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"allowJs": true,
|
||||
"rootDirs": ["."]
|
||||
"rootDirs": ["."],
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"include": [
|
||||
|
||||
@@ -8,5 +8,8 @@
|
||||
"src/querybuilder/testUtils.ts",
|
||||
"../../public/test/setupTests.ts"
|
||||
],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts*", "../../public/test/setupTests.ts"],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,18 +312,7 @@ export const handyTestingSchema: Spec = {
|
||||
label: 'Custom Variable',
|
||||
multi: true,
|
||||
name: 'customVar',
|
||||
options: [
|
||||
{
|
||||
selected: true,
|
||||
text: 'option1',
|
||||
value: 'option1',
|
||||
},
|
||||
{
|
||||
selected: false,
|
||||
text: 'option2',
|
||||
value: 'option2',
|
||||
},
|
||||
],
|
||||
options: [],
|
||||
query: 'option1, option2',
|
||||
skipUrlSync: false,
|
||||
allowCustomValue: true,
|
||||
@@ -490,5 +479,18 @@ 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 +1,7 @@
|
||||
{
|
||||
"exclude": ["dist/**/*", "**/*.test.ts*"],
|
||||
"extends": "./tsconfig.json"
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import AutoSizer, { type Size } 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 }) => {
|
||||
{({ width, height }: Size) => {
|
||||
return renderQueryEditor(width, height);
|
||||
}}
|
||||
</AutoSizer>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"rootDirs": ["."]
|
||||
"rootDirs": ["."],
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"]
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"rootDirs": ["."],
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"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 } from '@grafana/data';
|
||||
import { dateTime, makeTimeRange, TimeRange, BootData } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { TimeRangeProvider } from './TimeRangeContext';
|
||||
@@ -152,6 +152,58 @@ 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,6 +19,7 @@ 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';
|
||||
@@ -243,13 +244,22 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
|
||||
|
||||
TimeRangePicker.displayName = 'TimeRangePicker';
|
||||
|
||||
const ZoomOutTooltip = () => (
|
||||
<>
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</Trans>
|
||||
</>
|
||||
);
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => {
|
||||
const styles = useStyles2(getLabelStyles);
|
||||
|
||||
@@ -59,8 +59,9 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) {
|
||||
{context.hasOpenPane && (
|
||||
<SidebarButton
|
||||
icon={'web-section-alt'}
|
||||
onClick={context.onDockChange}
|
||||
onClick={context.onToggleDock}
|
||||
title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')}
|
||||
data-testid="sidebar-dock-toggle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useContext } from 'react';
|
||||
import React, { ButtonHTMLAttributes, useContext } from 'react';
|
||||
|
||||
import { GrafanaTheme2, IconName, isIconName } from '@grafana/data';
|
||||
|
||||
@@ -11,38 +11,48 @@ import { Tooltip } from '../Tooltip/Tooltip';
|
||||
|
||||
import { SidebarContext } from './useSidebar';
|
||||
|
||||
export interface Props {
|
||||
export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon: IconName;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function SidebarButton({ icon, active, onClick, title, tooltip }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const context = useContext(SidebarContext);
|
||||
export const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ icon, active, onClick, title, tooltip, ...restProps }, ref) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const context = useContext(SidebarContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Sidebar.Button must be used within a Sidebar component');
|
||||
if (!context) {
|
||||
throw new Error('Sidebar.Button must be used within a Sidebar component');
|
||||
}
|
||||
|
||||
const buttonClass = cx(
|
||||
styles.button,
|
||||
context.compact && styles.compact,
|
||||
active && styles.active,
|
||||
context.position === 'left' && styles.leftButton
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip ref={ref} content={tooltip ?? title} placement={context.position === 'left' ? 'right' : 'left'}>
|
||||
<button
|
||||
className={buttonClass}
|
||||
aria-label={title}
|
||||
aria-expanded={active}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
|
||||
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const buttonClass = cx(
|
||||
styles.button,
|
||||
context.compact && styles.compact,
|
||||
active && styles.active,
|
||||
context.position === 'left' && styles.leftButton
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip ?? title} placement={context.position === 'left' ? 'right' : 'left'}>
|
||||
<button className={buttonClass} aria-label={title} aria-expanded={active} type="button" onClick={onClick}>
|
||||
<div className={styles.iconWrapper}>{renderIcon(icon, context.compact)}</div>
|
||||
{!context.compact && <div className={cx(styles.title, active && styles.titleActive)}>{title}</div>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
SidebarButton.displayName = 'SidebarButton';
|
||||
|
||||
function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) {
|
||||
if (!icon) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
@@ -27,6 +28,7 @@ export function SidebarPaneHeader({ children, onClose, title }: Props) {
|
||||
onClick={onClose}
|
||||
aria-label={t('grafana-ui.sidebar.close', 'Close')}
|
||||
tooltip={t('grafana-ui.sidebar.close', 'Close')}
|
||||
data-testid={selectors.components.Sidebar.closePane}
|
||||
/>
|
||||
)}
|
||||
<Text weight="medium" variant="h6" truncate data-testid="sidebar-pane-header-title">
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface SidebarContextValue {
|
||||
bottomMargin: number;
|
||||
edgeMargin: number;
|
||||
contentMargin: number;
|
||||
onDockChange: () => void;
|
||||
onToggleDock: () => void;
|
||||
onResize: (diff: number) => void;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function useSidebar({
|
||||
// Used to accumulate drag distance to know when to change compact mode
|
||||
const [_, setCompactDrag] = React.useState(0);
|
||||
|
||||
const onDockChange = useCallback(() => setIsDocked((prev) => !prev), []);
|
||||
const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []);
|
||||
|
||||
const prop = position === 'right' ? 'paddingRight' : 'paddingLeft';
|
||||
const toolbarWidth =
|
||||
@@ -98,7 +98,7 @@ export function useSidebar({
|
||||
|
||||
return {
|
||||
isDocked,
|
||||
onDockChange,
|
||||
onToggleDock,
|
||||
onResize,
|
||||
outerWrapperProps,
|
||||
position,
|
||||
|
||||
@@ -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 };
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean };
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
readonly uid = Math.random().toString(36).slice(2);
|
||||
|
||||
+15
-1
@@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
expect(mockQueryZoom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set isPanning state during drag and clear on mouseup', () => {
|
||||
it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
@@ -153,6 +153,20 @@ 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,11 +96,14 @@ export const setupXAxisPan = (
|
||||
|
||||
xAxisEl.style.cursor = 'grab';
|
||||
|
||||
config.setState({ isPanning: false });
|
||||
const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST;
|
||||
|
||||
if (Math.abs(dragPixels) >= MIN_PAN_DIST) {
|
||||
if (isSignificantDrag) {
|
||||
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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user