Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-07-08 18:00:28 -07:00
74 changed files with 2267 additions and 1229 deletions
@@ -134,7 +134,7 @@ To integrate your OAuth2 provider with Grafana using our Generic OAuth authentic
### Configure login
Grafana can resolve a user's login from the OAuth2 ID token or user information retrieved from the OAuth2 UserInfo endpoint.
Grafana can resolve a user's login from the OAuth2 ID token, user information retrieved from the OAuth2 UserInfo endpoint, or the OAuth2 access token.
Grafana looks at these sources in the order listed until it finds a login.
If no login is found, then the user's login is set to user's email address.
@@ -146,10 +146,12 @@ Refer to the following table for information on what to configure based on how y
| Another field of the OAuth2 ID token. | Set `login_attribute_path` configuration option. |
| `login` or `username` field of the user information from the UserInfo endpoint. | N/A |
| Another field of the user information from the UserInfo endpoint. | Set `login_attribute_path` configuration option. |
| `login` or `username` field of the OAuth2 access token. | N/A |
| Another field of the OAuth2 access token. | Set `login_attribute_path` configuration option. |
### Configure display name
Grafana can resolve a user's display name from the OAuth2 ID token or user information retrieved from the OAuth2 UserInfo endpoint.
Grafana can resolve a user's display name from the OAuth2 ID token, user information retrieved from the OAuth2 UserInfo endpoint, or the OAuth2 access token.
Grafana looks at these sources in the order listed until it finds a display name.
If no display name is found, then user's login is displayed instead.
@@ -161,10 +163,12 @@ Refer to the following table for information on what you need to configure depen
| Another field of the OAuth2 ID token. | Set `name_attribute_path` configuration option. |
| `name` or `display_name` field of the user information from the UserInfo endpoint. | N/A |
| Another field of the user information from the UserInfo endpoint. | Set `name_attribute_path` configuration option. |
| `name` or `display_name` field of the OAuth2 access token. | N/A |
| Another field of the OAuth2 access token. | Set `name_attribute_path` configuration option. |
### Configure email address
Grafana can resolve the user's email address from the OAuth2 ID token, the user information retrieved from the OAuth2 UserInfo endpoint, or the OAuth2 `/emails` endpoint.
Grafana can resolve the user's email address from the OAuth2 ID token, the user information retrieved from the OAuth2 UserInfo endpoint, the OAuth2 access token, or the OAuth2 `/emails` endpoint.
Grafana looks at these sources in the order listed until an email address is found.
If no email is found, then the email address of the user is set to an empty string.
@@ -177,6 +181,10 @@ Refer to the following table for information on what to configure based on how t
| `upn` field of the OAuth2 ID token. | N/A |
| `email` field of the user information from the UserInfo endpoint. | N/A |
| Another field of the user information from the UserInfo endpoint. | Set `email_attribute_path` configuration option. |
| `email` field of the OAuth2 access token. | N/A |
| `attributes` map of the OAuth2 access token. | Set `email_attribute_name` configuration option. By default, Grafana searches for email under `email:primary` key. |
| `upn` field of the OAuth2 access token. | N/A |
| Another field of the OAuth2 access token. | Set `email_attribute_path` configuration option. |
| Email address marked as primary from the `/emails` endpoint of <br /> the OAuth2 provider (obtained by appending `/emails` to the URL <br /> configured with `api_url`) | N/A |
### Configure a refresh token
@@ -199,6 +207,7 @@ The `accessTokenExpirationCheck` feature toggle has been removed in Grafana v10.
Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from the auth provider upon user login.
The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option.
Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no role is found, the expression will be evaluated using the OAuth2 access token.
To map the server administrator role, use the `allow_assign_grafana_admin` configuration option.
Refer to [configuration options](#configuration-options) for more information.
@@ -326,6 +335,7 @@ By using Team Sync, you can link your OAuth2 groups to teams within Grafana. Thi
Teams for each user are synchronized when the user logs in.
Generic OAuth groups can be referenced by group ID, such as `8bab1c86-8fba-33e5-2089-1d1c80ec267d` or `myteam`.
Group information can be extracted from the OAuth2 ID token, user information from the UserInfo endpoint, or the OAuth2 access token.
For information on configuring OAuth2 groups with Grafana using the `groups_attribute_path` configuration option, refer to [configuration options](#configuration-options).
To learn more about Team Sync, refer to [Configure team sync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-team-sync/).
@@ -359,46 +369,46 @@ The following table outlines the various Generic OAuth configuration options. Yo
If the configuration option requires a JMESPath expression that includes a colon, enclose the entire expression in quotes to prevent parsing errors. For example `role_attribute_path: "role:view"`
{{< /admonition >}}
| Setting | Required | Supported on Cloud | Description | Default |
| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `enabled` | No | Yes | Enables Generic OAuth authentication. | `false` |
| `name` | No | Yes | Name that refers to the Generic OAuth authentication from the Grafana user interface. | `OAuth` |
| `icon` | No | Yes | Icon used for the Generic OAuth authentication in the Grafana user interface. | `signin` |
| `client_id` | Yes | Yes | Client ID provided by your OAuth2 app. | |
| `client_secret` | Yes | Yes | Client secret provided by your OAuth2 app. | |
| `auth_url` | Yes | Yes | Authorization endpoint of your OAuth2 provider. | |
| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | |
| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | |
| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` |
| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `user:email` |
| `empty_scopes` | No | Yes | Set to `true` to use an empty scope during authentication. | `false` |
| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Generic OAuth login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` |
| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `id_token_attribute_name` | No | Yes | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` |
| `login_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login](#configure-login). | |
| `name_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user name lookup from the user ID token. This name will be used as the user's display name. For more information on how user display name is retrieved, refer to [Configure display name](#configure-display-name). | |
| `email_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | |
| `email_attribute_name` | No | Yes | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | `email:primary` |
| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | |
| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` |
| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `org_mapping` | No | No | List of comma- or space-separated `<ExternalOrgName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` |
| `groups_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. The result of the evaluation should be a string array of groups. | |
| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | |
| `allowed_organizations` | No | Yes | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | |
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `team_ids` | No | Yes | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | |
| `team_ids_attribute_path` | No | Yes | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | |
| `teams_url` | No | Yes | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` |
| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. | `false` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
| Setting | Required | Supported on Cloud | Description | Default |
| ---------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `enabled` | No | Yes | Enables Generic OAuth authentication. | `false` |
| `name` | No | Yes | Name that refers to the Generic OAuth authentication from the Grafana user interface. | `OAuth` |
| `icon` | No | Yes | Icon used for the Generic OAuth authentication in the Grafana user interface. | `signin` |
| `client_id` | Yes | Yes | Client ID provided by your OAuth2 app. | |
| `client_secret` | Yes | Yes | Client secret provided by your OAuth2 app. | |
| `auth_url` | Yes | Yes | Authorization endpoint of your OAuth2 provider. | |
| `token_url` | Yes | Yes | Endpoint used to obtain the OAuth2 access token. | |
| `api_url` | Yes | Yes | Endpoint used to obtain user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | |
| `auth_style` | No | Yes | Name of the [OAuth2 AuthStyle](https://pkg.go.dev/golang.org/x/oauth2#AuthStyle) to be used when ID token is requested from OAuth2 provider. It determines how `client_id` and `client_secret` are sent to Oauth2 provider. Available values are `AutoDetect`, `InParams` and `InHeader`. | `AutoDetect` |
| `scopes` | No | Yes | List of comma- or space-separated OAuth2 scopes. | `user:email` |
| `empty_scopes` | No | Yes | Set to `true` to use an empty scope during authentication. | `false` |
| `allow_sign_up` | No | Yes | Controls Grafana user creation through the Generic OAuth login. Only existing Grafana users can log in with Generic OAuth if set to `false`. | `true` |
| `auto_login` | No | Yes | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `id_token_attribute_name` | No | Yes | The name of the key used to extract the ID token from the returned OAuth2 token. | `id_token` |
| `login_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user login lookup from the user ID token. For more information on how user login is retrieved, refer to [Configure login](#configure-login). | |
| `name_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user name lookup from the user ID token. This name will be used as the user's display name. For more information on how user display name is retrieved, refer to [Configure display name](#configure-display-name). | |
| `email_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user email lookup from the user information. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | |
| `email_attribute_name` | No | Yes | Name of the key to use for user email lookup within the `attributes` map of OAuth2 ID token. For more information on how user email is retrieved, refer to [Configure email address](#configure-email-address). | `email:primary` |
| `role_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no role is found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no role is found, the expression will be evaluated using the OAuth2 access token. The result of the evaluation should be a valid Grafana role (`None`, `Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | |
| `role_attribute_strict` | No | Yes | Set to `true` to deny user login if the Grafana org role cannot be extracted using `role_attribute_path` or `org_mapping`. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` |
| `skip_org_role_sync` | No | Yes | Set to `true` to stop automatically syncing user roles. This will allow you to set organization roles for your users from within Grafana manually. | `false` |
| `org_attribute_path` | No | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana org to role lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no value is returned, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no value is returned, the expression will be evaluated using the OAuth2 access token. The result of the evaluation will be mapped to org roles based on `org_mapping`. For more information on org to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `org_mapping` | No | No | List of comma- or space-separated `<ExternalOrgName>:<OrgIdOrName>:<Role>` mappings. Value can be `*` meaning "All users". Role is optional and can have the following values: `None`, `Viewer`, `Editor` or `Admin`. For more information on external organization to role mapping, refer to [Org roles mapping example](#org-roles-mapping-example). | |
| `allow_assign_grafana_admin` | No | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping](#configure-role-mapping). | `false` |
| `groups_attribute_path` | No | Yes | [JMESPath](http://jmespath.org/examples.html) expression to use for user group lookup. Grafana will first evaluate the expression using the OAuth2 ID token. If no groups are found, the expression will be evaluated using the user information obtained from the UserInfo endpoint. If still no groups are found, the expression will be evaluated using the OAuth2 access token. The result of the evaluation should be a string array of groups. | |
| `allowed_groups` | No | Yes | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | |
| `allowed_organizations` | No | Yes | List of comma- or space-separated organizations. The user should be a member of at least one organization to log in. | |
| `allowed_domains` | No | Yes | List of comma- or space-separated domains. The user should belong to at least one domain to log in. | |
| `team_ids` | No | Yes | String list of team IDs. If set, the user must be a member of one of the given teams to log in. If you configure `team_ids`, you must also configure `teams_url` and `team_ids_attribute_path`. | |
| `team_ids_attribute_path` | No | Yes | The [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana team ID lookup within the results returned by the `teams_url` endpoint. | |
| `teams_url` | No | Yes | The URL used to query for team IDs. If not set, the default value is `/teams`. If you configure `teams_url`, you must also configure `team_ids_attribute_path`. | |
| `tls_skip_verify_insecure` | No | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | No | The path to the certificate. | |
| `tls_client_key` | No | No | The path to the key. | |
| `tls_client_ca` | No | No | The path to the trusted certificate authority list. | |
| `use_pkce` | No | Yes | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `false` |
| `use_refresh_token` | No | Yes | Set to `true` to use refresh token and check access token expiration. | `false` |
| `signout_redirect_url` | No | Yes | URL to redirect to after the user logs out. | |
## Examples of setting up Generic OAuth
+6 -6
View File
@@ -111,7 +111,7 @@
"@testing-library/user-event": "14.6.1",
"@types/babel__core": "^7",
"@types/babel__preset-env": "^7",
"@types/chance": "^1.1.3",
"@types/chance": "^1.1.7",
"@types/common-tags": "^1.8.0",
"@types/confusing-browser-globals": "^1",
"@types/d3": "7.4.3",
@@ -164,19 +164,19 @@
"@types/yargs": "17.0.33",
"@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.35.1",
"autoprefixer": "10.4.20",
"autoprefixer": "10.4.21",
"babel-loader": "9.2.1",
"blob-polyfill": "9.0.20240710",
"browserslist": "^4.21.4",
"chance": "^1.0.10",
"chrome-remote-interface": "0.33.2",
"chance": "^1.1.13",
"chrome-remote-interface": "0.33.3",
"codeowners": "^5.1.1",
"confusing-browser-globals": "^1.0.11",
"copy-webpack-plugin": "12.0.2",
"core-js": "3.40.0",
"crashme": "0.0.15",
"css-loader": "7.1.2",
"css-minimizer-webpack-plugin": "7.0.0",
"css-minimizer-webpack-plugin": "7.0.2",
"cypress": "14.3.2",
"cypress-file-upload": "5.0.8",
"cypress-recurse": "^1.35.3",
@@ -281,7 +281,7 @@
"@grafana/flamegraph": "workspace:*",
"@grafana/google-sdk": "0.3.2",
"@grafana/i18n": "workspace:*",
"@grafana/lezer-logql": "0.2.7",
"@grafana/lezer-logql": "0.2.8",
"@grafana/llm": "0.22.1",
"@grafana/monaco-logql": "^0.0.8",
"@grafana/o11y-ds-frontend": "workspace:*",
@@ -2,7 +2,7 @@ import { PropsWithChildren, ReactElement, useMemo } from 'react';
import { DataSourceInstanceSettings } from '../../types/datasource';
import { Context, DataSourcePluginContextType } from './PluginContext';
import { PluginContext, DataSourcePluginContextType } from './PluginContext';
export type DataSourcePluginContextProviderProps = {
instanceSettings: DataSourceInstanceSettings;
@@ -16,5 +16,5 @@ export function DataSourcePluginContextProvider(
return { instanceSettings, meta: instanceSettings.meta };
}, [instanceSettings]);
return <Context.Provider value={value}>{children}</Context.Provider>;
return <PluginContext.Provider value={value}>{children}</PluginContext.Provider>;
}
@@ -1,14 +1,15 @@
import { createContext } from 'react';
import { KeyValue } from '../../types/data';
import { DataSourceInstanceSettings } from '../../types/datasource';
import { PluginMeta } from '../../types/plugin';
export interface PluginContextType {
meta: PluginMeta;
export interface PluginContextType<T extends KeyValue = KeyValue> {
meta: PluginMeta<T>;
}
export interface DataSourcePluginContextType extends PluginContextType {
export interface DataSourcePluginContextType<T extends KeyValue = KeyValue> extends PluginContextType<T> {
instanceSettings: DataSourceInstanceSettings;
}
export const Context = createContext<PluginContextType | undefined>(undefined);
export const PluginContext = createContext<PluginContextType | undefined>(undefined);
@@ -2,7 +2,7 @@ import { PropsWithChildren, ReactElement } from 'react';
import { PluginMeta } from '../../types/plugin';
import { Context } from './PluginContext';
import { PluginContext } from './PluginContext';
export type PluginContextProviderProps = {
meta: PluginMeta;
@@ -10,5 +10,5 @@ export type PluginContextProviderProps = {
export function PluginContextProvider(props: PropsWithChildren<PluginContextProviderProps>): ReactElement {
const { children, ...rest } = props;
return <Context.Provider value={rest}>{children}</Context.Provider>;
return <PluginContext.Provider value={rest}>{children}</PluginContext.Provider>;
}
@@ -1,5 +1,9 @@
import { KeyValue } from '../../types/data';
import { type DataSourcePluginContextType, type PluginContextType } from './PluginContext';
export function isDataSourcePluginContext(context: PluginContextType): context is DataSourcePluginContextType {
export function isDataSourcePluginContext<T extends KeyValue = KeyValue>(
context: PluginContextType<T>
): context is DataSourcePluginContextType<T> {
return 'instanceSettings' in context && 'meta' in context;
}
@@ -1,9 +1,11 @@
import { useContext } from 'react';
import { Context, PluginContextType } from './PluginContext';
import { KeyValue } from '../../types/data';
export function usePluginContext(): PluginContextType | null {
const context = useContext(Context);
import { PluginContext, PluginContextType } from './PluginContext';
export function usePluginContext<T extends KeyValue = KeyValue>(): PluginContextType<T> | null {
const context = useContext(PluginContext);
// The extensions hooks (e.g. `usePluginLinks()`) are using this hook to check
// if they are inside a plugin or not (core Grafana), so we should be able to return an empty state as well (`null`).
@@ -11,5 +13,6 @@ export function usePluginContext(): PluginContextType | null {
return null;
}
return context;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return context as PluginContextType<T>;
}
+1 -1
View File
@@ -435,7 +435,7 @@ export { type GroupingToMatrixTransformerOptions } from './transformations/trans
export {
type PluginContextType,
type DataSourcePluginContextType,
Context as PluginContext,
PluginContext,
} from './context/plugins/PluginContext';
export { type PluginContextProviderProps, PluginContextProvider } from './context/plugins/PluginContextProvider';
export {
@@ -35,18 +35,21 @@ describe('Limit transformer', () => {
{
name: 'time',
type: FieldType.time,
state: { calcs: undefined },
values: [3000, 4000, 5000],
config: {},
},
{
name: 'message',
type: FieldType.string,
state: { calcs: undefined },
values: ['one', 'two', 'two'],
config: {},
},
{
name: 'values',
type: FieldType.number,
state: { calcs: undefined },
values: [1, 2, 2],
config: {},
},
@@ -79,18 +82,21 @@ describe('Limit transformer', () => {
{
name: 'time',
type: FieldType.time,
state: { calcs: undefined },
values: [6000, 7000, 8000],
config: {},
},
{
name: 'message',
type: FieldType.string,
state: { calcs: undefined },
values: ['three', 'three', 'three'],
config: {},
},
{
name: 'values',
type: FieldType.number,
state: { calcs: undefined },
values: [3, 3, 3],
config: {},
},
@@ -37,6 +37,12 @@ export const limitTransformer: DataTransformerInfo<LimitTransformerOptions> = {
fields: frame.fields.map((f) => {
return {
...f,
// Clear cached field calculations since applying a limit changes the dataset
// and previously computed stats (min, max, mean, etc.) are no longer valid
state: {
...f.state,
calcs: undefined,
},
values:
limit >= 0 ? f.values.slice(0, limit) : f.values.slice(f.values.length + limit, f.values.length),
};
@@ -48,6 +48,14 @@ describe('fuzzySearch', () => {
expect(result.map((idx) => haystack[idx])).toEqual(['A水']);
});
it('should do case-insensitive substring match when needle contains non-ascii characters', () => {
const haystack = ['Über'];
const needle = 'ü';
const result = fuzzySearch(haystack, needle);
expect(result.map((idx) => haystack[idx])).toEqual(['Über']);
});
it('should handle multiple non-latin characters', () => {
const haystack = ['台灣省', '台中市', '台北市', '台南市', '南投縣', '高雄市', '台中第一高級中學'];
const needle = '南';
@@ -1,5 +1,7 @@
import uFuzzy from '@leeoniya/ufuzzy';
import { escapeRegex } from '../text/string';
// https://catonmat.net/my-favorite-regex :)
const REGEXP_NON_ASCII = /[^ -~]/m;
// https://www.asciitable.com/
@@ -36,11 +38,13 @@ export function fuzzySearch(haystack: string[], needle: string): number[] {
needle.length > maxNeedleLength ||
uf.split(needle).length > maxFuzzyTerms
) {
const needleRegex = new RegExp(escapeRegex(needle), 'i');
const indices: number[] = [];
for (let i = 0; i < haystack.length; i++) {
let item = haystack[i];
if (item.includes(needle)) {
if (needleRegex.test(item)) {
indices.push(i);
}
}
@@ -1179,11 +1179,6 @@ export const versionedComponents = {
'12.1.0': 'data-testid Data links actions tooltip wrapper',
},
},
TablePanel: {
autoCell: {
'12.1.0': 'data-testid Table panel auto cell',
},
},
CodeEditor: {
container: {
'10.2.3': 'data-testid Code editor container',
@@ -708,6 +708,7 @@ export enum TableCellDisplayMode {
Image = 'image',
JSONView = 'json-view',
LcdGauge = 'lcd-gauge',
Pill = 'pill',
Sparkline = 'sparkline',
}
@@ -838,7 +839,38 @@ export enum TableCellHeight {
* Table cell options. Each cell has a display mode
* and other potential options for that display.
*/
export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions);
export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions);
/**
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
* Generally defines alignment, filtering capabilties, display options, etc.
*/
export interface TableFieldOptions {
align: FieldTextAlignment;
cellOptions: TableCellOptions;
/**
* This field is deprecated in favor of using cellOptions
*/
displayMode?: TableCellDisplayMode;
filterable?: boolean;
hidden?: boolean; // ?? default is missing or false ??
/**
* Hides any header for a column, useful for columns that show some static content or buttons.
*/
hideHeader?: boolean;
inspect: boolean;
minWidth?: number;
width?: number;
/**
* Enables text wrapping for column headers
*/
wrapHeaderText?: boolean;
}
export const defaultTableFieldOptions: Partial<TableFieldOptions> = {
align: 'auto',
inspect: false,
};
/**
* Use UTC/GMT timezone
@@ -944,37 +976,12 @@ export enum ComparisonOperation {
NEQ = 'neq',
}
/**
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
* Generally defines alignment, filtering capabilties, display options, etc.
*/
export interface TableFieldOptions {
align: FieldTextAlignment;
cellOptions: TableCellOptions;
/**
* This field is deprecated in favor of using cellOptions
*/
displayMode?: TableCellDisplayMode;
filterable?: boolean;
hidden?: boolean; // ?? default is missing or false ??
/**
* Hides any header for a column, useful for columns that show some static content or buttons.
*/
hideHeader?: boolean;
inspect: boolean;
minWidth?: number;
width?: number;
/**
* Enables text wrapping for column headers
*/
wrapHeaderText?: boolean;
export interface TablePillCellOptions {
color?: string;
colorMode?: ('auto' | 'fixed' | 'mapped');
type: TableCellDisplayMode.Pill;
}
export const defaultTableFieldOptions: Partial<TableFieldOptions> = {
align: 'auto',
inspect: false,
};
/**
* A specific timezone from https://en.wikipedia.org/wiki/Tz_database
*/
+8 -2
View File
@@ -4,7 +4,7 @@ package common
// in the table such as colored text, JSON, gauge, etc.
// The color-background-solid, gradient-gauge, and lcd-gauge
// modes are deprecated in favor of new cell subOptions
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions")
TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill")
// Display mode to the "Colored Background" display
// mode for table cells. Either displays a solid color (basic mode)
@@ -89,7 +89,7 @@ TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum")
// Table cell options. Each cell has a display mode
// and other potential options for that display.
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions @cuetsy(kind="type")
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
// Generally defines alignment, filtering capabilties, display options, etc.
@@ -109,3 +109,9 @@ TableFieldOptions: {
wrapHeaderText?: bool
} @cuetsy(kind="interface")
TablePillCellOptions: {
type: TableCellDisplayMode & "pill"
color?: string
colorMode?: "auto" | "fixed" | "mapped"
} @cuetsy(kind="interface")
+2 -2
View File
@@ -156,7 +156,7 @@
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
"@types/chance": "1.1.6",
"@types/chance": "^1.1.7",
"@types/common-tags": "^1.8.0",
"@types/d3": "7.4.3",
"@types/hoist-non-react-statics": "3.3.6",
@@ -176,7 +176,7 @@
"@types/slate-react": "0.22.9",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"chance": "1.1.12",
"chance": "^1.1.13",
"common-tags": "1.8.2",
"core-js": "3.40.0",
"css-loader": "7.1.2",
@@ -74,8 +74,6 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt
const dismiss = useDismiss(context);
const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length);
const { getFloatingProps, getReferenceProps } = useInteractions([dismiss]);
if (links.length === 0 && !Boolean(actions?.length)) {
@@ -84,23 +82,22 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt
return (
<>
{/* TODO: we can remove `value` from this component when tableNextGen is fully rolled out */}
{value}
{hasMultipleLinksOrActions && (
<Portal>
<div
ref={refCallback}
{...getReferenceProps()}
{...getFloatingProps()}
style={floatingStyles}
className={styles.tooltipWrapper}
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
>
<VizTooltipWrapper>
<VizTooltipFooter dataLinks={links} actions={actions} />
</VizTooltipWrapper>
</div>
</Portal>
)}
<Portal>
<div
ref={refCallback}
{...getReferenceProps()}
{...getFloatingProps()}
style={floatingStyles}
className={styles.tooltipWrapper}
data-testid={selectors.components.DataLinksActionsTooltip.tooltipWrapper}
>
<VizTooltipWrapper>
<VizTooltipFooter dataLinks={links} actions={actions} />
</VizTooltipWrapper>
</div>
</Portal>
</>
);
};
@@ -1,4 +1,5 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@@ -6,12 +7,16 @@ import { useStyles2 } from '../../../../themes/ThemeContext';
import { ActionButton } from '../../../Actions/ActionButton';
import { ActionCellProps } from '../types';
export const ActionsCell = ({ actions }: ActionCellProps) => {
export const ActionsCell = ({ field, rowIdx, getActions }: ActionCellProps) => {
const styles = useStyles2(getStyles);
const actions = useMemo(() => getActions(field, rowIdx), [getActions, field, rowIdx]);
return (
<div className={styles.buttonsGap}>
{actions && actions.map((action, i) => <ActionButton key={i} action={action} variant="secondary" />)}
{actions.map((action, i) => (
<ActionButton key={i} action={action} variant="secondary" />
))}
</div>
);
};
@@ -1,97 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Field, FieldType, LinkModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { TableCellDisplayMode } from '../../types';
import AutoCell from './AutoCell';
describe('AutoCell', () => {
describe('Displays data Links', () => {
const getFieldWithLinks = (links: LinkModel[]): Field => {
return {
name: 'Category',
type: FieldType.string,
values: ['A', 'B', 'A', 'B', 'A'],
config: {
custom: {
cellOptions: {
type: TableCellDisplayMode.Auto,
wrapText: false,
},
},
},
display: (value: unknown) => ({
text: String(value),
numeric: 0,
color: undefined,
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: () => links,
};
};
it('shows multiple datalinks in the tooltip', async () => {
const linksForField = [
{ href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
{ href: 'http://asdasd2.com', title: 'Test Title2' } as LinkModel,
];
jest.mock('../utils', () => ({
getCellLinks: () => linksForField,
}));
const field = getFieldWithLinks(linksForField);
render(
<AutoCell
value="test"
field={field}
justifyContent="normal"
rowIdx={0}
cellOptions={{ type: TableCellDisplayMode.Auto }}
/>
);
const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
await userEvent.click(cell);
const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper);
expect(tooltip).toBeInTheDocument();
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Title2')).toBeInTheDocument();
});
it('does not show tooltip for multiple links if one is invalid', async () => {
const linksForField = [
{ href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
{ title: 'Test Title2' } as LinkModel,
];
jest.mock('../utils', () => ({
getCellLinks: () => linksForField,
}));
const field = getFieldWithLinks(linksForField);
render(
<AutoCell
value="test"
field={field}
justifyContent="normal"
rowIdx={0}
cellOptions={{ type: TableCellDisplayMode.Auto }}
/>
);
const cell = screen.getByTestId(selectors.components.TablePanel.autoCell);
await userEvent.click(cell);
expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
});
});
});
@@ -1,49 +1,24 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import { useState } from 'react';
import { GrafanaTheme2, formattedValueToString } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellOptions, TableCellDisplayMode } from '../../types';
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils';
import { useSingleLink } from '../hooks';
import { AutoCellProps } from '../types';
import { getCellLinks } from '../utils';
export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions, actions }: AutoCellProps) {
export default function AutoCell({ value, field, justifyContent, rowIdx, cellOptions }: AutoCellProps) {
const styles = useStyles2(getStyles, justifyContent);
const displayValue = field.display!(value);
const formattedValue = formattedValueToString(displayValue);
const links = getCellLinks(field, rowIdx) || [];
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
const link = useSingleLink(field, rowIdx);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
className={styles.cell}
onClick={tooltipOnClickHandler(setTooltipCoords)}
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
data-testid={selectors.components.TablePanel.autoCell}
>
{shouldShowLink ? (
renderSingleLink(links[0], formattedValue, getLinkStyle(styles, cellOptions))
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={formattedValue}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
formattedValue
)}
<div className={styles.cell}>
{link == null ? formattedValue : renderSingleLink(link, formattedValue, getLinkStyle(styles, cellOptions))}
</div>
);
}
@@ -1,13 +1,11 @@
import { useState } from 'react';
import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMinMax } from '@grafana/data';
import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema';
import { BarGauge } from '../../../BarGauge/BarGauge';
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { useSingleLink } from '../hooks';
import { BarGaugeCellProps } from '../types';
import { extractPixelValue, getCellOptions, getAlignmentFactor, getCellLinks } from '../utils';
import { extractPixelValue, getCellOptions, getAlignmentFactor } from '../utils';
const defaultScale: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
@@ -23,7 +21,7 @@ const defaultScale: ThresholdsConfig = {
],
};
export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actions }: BarGaugeCellProps) => {
export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => {
const displayValue = field.display!(value);
const cellOptions = getCellOptions(field);
const heightOffset = extractPixelValue(theme.spacing(1));
@@ -48,51 +46,26 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx, actio
}
const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!);
const links = getCellLinks(field, rowIdx) || [];
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
const renderComponent = () => {
return (
<BarGauge
width={width}
height={height - heightOffset}
field={config}
display={field.display}
text={{ valueSize: 14 }}
value={displayValue}
orientation={VizOrientation.Horizontal}
theme={theme}
alignmentFactors={alignmentFactors}
itemSpacing={1}
lcdCellWidth={8}
displayMode={barGaugeMode}
valueDisplayMode={valueDisplayMode}
/>
);
};
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
onClick={tooltipOnClickHandler(setTooltipCoords)}
>
{shouldShowLink ? (
renderSingleLink(links[0], renderComponent())
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={renderComponent()}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
renderComponent()
)}
</div>
const barGaugeComponent = (
<BarGauge
width={width}
height={height - heightOffset}
field={config}
display={field.display}
text={{ valueSize: 14 }}
value={displayValue}
orientation={VizOrientation.Horizontal}
theme={theme}
alignmentFactors={alignmentFactors}
itemSpacing={1}
lcdCellWidth={8}
displayMode={barGaugeMode}
valueDisplayMode={valueDisplayMode}
/>
);
const link = useSingleLink(field, rowIdx);
return link == null ? barGaugeComponent : renderSingleLink(link, barGaugeComponent);
};
@@ -1,55 +1,28 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { TableCellDisplayMode } from '../../types';
import { DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils, tooltipOnClickHandler } from '../../utils';
import { useSingleLink } from '../hooks';
import { ImageCellProps } from '../types';
import { getCellLinks } from '../utils';
const DATALINKS_HEIGHT_OFFSET = 10;
export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx, actions }: ImageCellProps) => {
export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => {
const calculatedHeight = height - DATALINKS_HEIGHT_OFFSET;
const styles = useStyles2(getStyles, calculatedHeight, justifyContent);
const links = getCellLinks(field, rowIdx) || [];
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
const { text } = field.display!(value);
const { alt, title } =
cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { alt: undefined, title: undefined };
const img = <img alt={alt} src={text} className={styles.image} title={title} />;
const link = useSingleLink(field, rowIdx);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
className={styles.imageContainer}
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
onClick={tooltipOnClickHandler(setTooltipCoords)}
>
{shouldShowLink ? (
renderSingleLink(links[0], img)
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={img}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
img
)}
</div>
);
return <div className={styles.imageContainer}>{link == null ? img : renderSingleLink(link, img)}</div>;
};
const getStyles = (theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({
@@ -1,16 +1,14 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { DataLinksActionsTooltip, renderSingleLink } from '../../DataLinksActionsTooltip';
import { tooltipOnClickHandler, DataLinksActionsTooltipCoords, getDataLinksActionsTooltipUtils } from '../../utils';
import { renderSingleLink } from '../../DataLinksActionsTooltip';
import { useSingleLink } from '../hooks';
import { JSONCellProps } from '../types';
import { getCellLinks } from '../utils';
export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSONCellProps) => {
export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => {
const styles = useStyles2(getStyles, justifyContent);
let displayValue = value;
@@ -33,34 +31,9 @@ export const JSONCell = ({ value, justifyContent, field, rowIdx, actions }: JSON
}
}
const links = getCellLinks(field, rowIdx) || [];
const link = useSingleLink(field, rowIdx);
const [tooltipCoords, setTooltipCoords] = useState<DataLinksActionsTooltipCoords>();
const { shouldShowLink, hasMultipleLinksOrActions } = getDataLinksActionsTooltipUtils(links, actions);
const shouldShowTooltip = hasMultipleLinksOrActions && tooltipCoords !== undefined;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div
className={styles.jsonText}
onClick={tooltipOnClickHandler(setTooltipCoords)}
style={{ cursor: hasMultipleLinksOrActions ? 'context-menu' : 'auto' }}
>
{shouldShowLink ? (
renderSingleLink(links[0], displayValue)
) : shouldShowTooltip ? (
<DataLinksActionsTooltip
links={links}
actions={actions}
value={displayValue}
coords={tooltipCoords}
onTooltipClose={() => setTooltipCoords(undefined)}
/>
) : (
displayValue
)}
</div>
);
return <div className={styles.jsonText}>{link == null ? displayValue : renderSingleLink(link, displayValue)}</div>;
};
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({
@@ -0,0 +1,219 @@
import { render, screen } from '@testing-library/react';
import { DataFrame, Field, FieldType, GrafanaTheme2, MappingType, createTheme } from '@grafana/data';
import { TableCellDisplayMode, TablePillCellOptions } from '@grafana/schema';
import { mockThemeContext } from '../../../../themes/ThemeContext';
import { PillCell, inferPills } from './PillCell';
describe('PillCell', () => {
let restoreThemeContext: () => void;
beforeEach(() => {
restoreThemeContext = mockThemeContext(createTheme());
});
afterEach(() => {
restoreThemeContext();
});
const mockCellOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'auto',
};
const mockField: Field = {
name: 'test',
type: FieldType.string,
values: [],
config: {},
};
const mockFrame: DataFrame = {
name: 'test',
fields: [mockField],
length: 1,
};
const defaultProps = {
value: 'test-value',
field: mockField,
justifyContent: 'flex-start' as const,
cellOptions: mockCellOptions,
rowIdx: 0,
frame: mockFrame,
height: 30,
width: 100,
theme: {} as GrafanaTheme2,
cellInspect: false,
showFilters: false,
};
describe('pill parsing', () => {
it('should render pills for single values', () => {
render(<PillCell {...defaultProps} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
});
it('should render pills for CSV values', () => {
render(<PillCell {...defaultProps} value="value1,value2,value3" />);
expect(screen.getByText('value1')).toBeInTheDocument();
expect(screen.getByText('value2')).toBeInTheDocument();
expect(screen.getByText('value3')).toBeInTheDocument();
});
it('should render pills for JSON array values', () => {
render(<PillCell {...defaultProps} value='["item1","item2","item3"]' />);
expect(screen.getByText('item1')).toBeInTheDocument();
expect(screen.getByText('item2')).toBeInTheDocument();
expect(screen.getByText('item3')).toBeInTheDocument();
});
it('should show dash for empty values', () => {
render(<PillCell {...defaultProps} value="" />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should show dash for null values', () => {
render(<PillCell {...defaultProps} value={null as unknown as string} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
});
describe('color mapping', () => {
// These tests primarily ensure the color logic executes without throwing.
// For true color verification, visual regression tests would be needed.
it('should use mapped colors when colorMode is mapped', () => {
const mappedOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'mapped',
};
render(<PillCell {...defaultProps} value="success,error,warning,unknown" cellOptions={mappedOptions} />);
const successPill = screen.getByText('success');
const errorPill = screen.getByText('error');
const warningPill = screen.getByText('warning');
const unknownPill = screen.getByText('unknown');
expect(successPill).toBeInTheDocument();
expect(errorPill).toBeInTheDocument();
expect(warningPill).toBeInTheDocument();
expect(unknownPill).toBeInTheDocument();
});
it('should use field-level value mappings when available', () => {
const mappedOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'mapped',
};
// Mock field with value mappings
const fieldWithMappings: Field = {
...mockField,
config: {
...mockField.config,
mappings: [
{
type: MappingType.ValueToText,
options: {
success: { color: '#00FF00' },
error: { color: '#FF0000' },
warning: { color: '#FFFF00' },
},
},
],
},
display: (value: unknown) => ({
text: String(value),
color:
String(value) === 'success'
? '#00FF00'
: String(value) === 'error'
? '#FF0000'
: String(value) === 'warning'
? '#FFFF00'
: '#FF780A',
numeric: 0,
}),
};
render(
<PillCell
{...defaultProps}
value="success,error,warning,unknown"
cellOptions={mappedOptions}
field={fieldWithMappings}
/>
);
const successPill = screen.getByText('success');
const errorPill = screen.getByText('error');
const warningPill = screen.getByText('warning');
const unknownPill = screen.getByText('unknown');
expect(successPill).toBeInTheDocument();
expect(errorPill).toBeInTheDocument();
expect(warningPill).toBeInTheDocument();
expect(unknownPill).toBeInTheDocument();
});
it('should use fixed color when colorMode is fixed', () => {
const fixedOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'fixed',
color: '#FF00FF',
};
render(<PillCell {...defaultProps} cellOptions={fixedOptions} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
});
it('should use auto color when colorMode is auto', () => {
const autoOptions: TablePillCellOptions = {
type: TableCellDisplayMode.Pill,
colorMode: 'auto',
};
render(<PillCell {...defaultProps} cellOptions={autoOptions} />);
expect(screen.getByText('test-value')).toBeInTheDocument();
});
});
});
describe('inferPills', () => {
// These tests verify the pill parsing logic handles various input formats correctly.
// They ensure the function can extract pill values from different data structures.
it('should return empty array for null/undefined values', () => {
expect(inferPills(null)).toEqual([]);
expect(inferPills(undefined)).toEqual([]);
expect(inferPills('')).toEqual([]);
});
it('should parse single values', () => {
expect(inferPills('test')).toEqual(['test']);
expect(inferPills('"quoted"')).toEqual(['quoted']);
expect(inferPills("'quoted'")).toEqual(['quoted']);
});
it('should parse CSV strings', () => {
expect(inferPills('value1,value2,value3')).toEqual(['value1', 'value2', 'value3']);
expect(inferPills(' value1 , value2 , value3 ')).toEqual(['value1', 'value2', 'value3']);
expect(inferPills('value1, ,value3')).toEqual(['value1', 'value3']);
});
it('should parse JSON arrays', () => {
expect(inferPills('["item1","item2","item3"]')).toEqual(['item1', 'item2', 'item3']);
expect(inferPills('["item1", "item2", "item3"]')).toEqual(['item1', 'item2', 'item3']);
expect(inferPills('["item1", null, "item3"]')).toEqual(['item1', 'item3']);
});
it('should handle mixed content', () => {
// When JSON parsing fails, it falls back to CSV parsing
expect(inferPills('["item1", "item2"],extra')).toEqual(['["item1"', '"item2"]', 'extra']);
expect(inferPills('not-json,value')).toEqual(['not-json', 'value']);
});
});
@@ -0,0 +1,185 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import { useMemo } from 'react';
import { GrafanaTheme2, isDataFrame, classicColors, colorManipulator, Field } from '@grafana/data';
import { TablePillCellOptions } from '@grafana/schema';
import { useStyles2 } from '../../../../themes/ThemeContext';
import { TableCellRendererProps } from '../types';
const DEFAULT_PILL_BG_COLOR = '#FF780A';
interface Pill {
value: string;
key: string;
bgColor: string;
color: string;
}
function createPills(pillValues: string[], cellOptions: TableCellRendererProps['cellOptions'], field: Field): Pill[] {
return pillValues.map((pill, index) => {
const bgColor = getPillColor(pill, cellOptions, field);
const textColor = colorManipulator.getContrastRatio('#FFFFFF', bgColor) >= 4.5 ? '#FFFFFF' : '#000000';
return {
value: pill,
key: `${pill}-${index}`,
bgColor,
color: textColor,
};
});
}
export function PillCell({ value, field, justifyContent, cellOptions }: TableCellRendererProps) {
const styles = useStyles2(getStyles, justifyContent);
const pills: Pill[] = useMemo(() => {
const pillValues = inferPills(value);
return createPills(pillValues, cellOptions, field);
}, [value, cellOptions, field]);
if (pills.length === 0) {
return <div className={styles.cell}>-</div>;
}
return (
<div className={styles.cell}>
<div className={styles.pillsContainer}>
{pills.map((pill) => (
<span
key={pill.key}
className={styles.pill}
style={{
backgroundColor: pill.bgColor,
color: pill.color,
}}
>
{pill.value}
</span>
))}
</div>
</div>
);
}
export function inferPills(value: unknown): string[] {
if (!value) {
return [];
}
// Handle DataFrame - not supported for pills
if (isDataFrame(value)) {
return [];
}
// Handle different value types
const stringValue = String(value);
// Try to parse as JSON first
try {
const parsed = JSON.parse(stringValue);
if (Array.isArray(parsed)) {
// JSON array of strings
return parsed
.filter((item) => item != null && item !== '')
.map(String)
.map((text) => text.trim())
.filter((item) => item !== '');
}
} catch {
// Not valid JSON, continue with other parsing
}
// Handle CSV string
if (stringValue.includes(',')) {
return stringValue
.split(',')
.map((text) => text.trim())
.filter((item) => item !== '');
}
// Single value - strip quotes
return [stringValue.replace(/["'`]/g, '').trim()];
}
function isPillCellOptions(cellOptions: TableCellRendererProps['cellOptions']): cellOptions is TablePillCellOptions {
return cellOptions?.type === 'pill';
}
function getPillColor(pill: string, cellOptions: TableCellRendererProps['cellOptions'], field: Field): string {
if (!isPillCellOptions(cellOptions)) {
return getDeterministicColor(pill);
}
const colorMode = cellOptions.colorMode || 'auto';
// Fixed color mode (highest priority)
if (colorMode === 'fixed' && cellOptions.color) {
return cellOptions.color;
}
// Mapped color mode - use field's value mappings
if (colorMode === 'mapped') {
// Check if field has value mappings
if (field.config.mappings && field.config.mappings.length > 0) {
// Use the field's display processor to get the mapped value
const displayValue = field.display!(pill);
if (displayValue.color) {
return displayValue.color;
}
}
// Fallback to default color for unmapped values
return cellOptions.color || DEFAULT_PILL_BG_COLOR;
}
// Auto mode - deterministic color assignment based on string hash
if (colorMode === 'auto') {
return getDeterministicColor(pill);
}
// Default color for unknown values or fallback
return DEFAULT_PILL_BG_COLOR;
}
function getDeterministicColor(text: string): string {
// Create a simple hash of the string to get consistent colors
let hash = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Use absolute value and modulo to get a consistent index
const colorValues = Object.values(classicColors);
const index = Math.abs(hash) % colorValues.length;
return colorValues[index];
}
const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({
cell: css({
display: 'flex',
justifyContent: justifyContent || 'flex-start',
alignItems: 'center',
height: '100%',
padding: theme.spacing(0.5),
}),
pillsContainer: css({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
maxWidth: '100%',
}),
pill: css({
display: 'inline-block',
padding: theme.spacing(0.25, 0.75),
borderRadius: theme.shape.radius.default,
fontSize: theme.typography.bodySmall.fontSize,
lineHeight: theme.typography.bodySmall.lineHeight,
fontWeight: theme.typography.fontWeightMedium,
whiteSpace: 'nowrap',
textAlign: 'center',
minWidth: 'fit-content',
}),
});
@@ -24,7 +24,10 @@ export function TableCellActions(props: TableCellActionsProps) {
} = props;
return (
<div className={className}>
// stopping propagation to prevent clicks within the actions menu from triggering the cell click events
// for things like the data links tooltip.
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={className} onClick={(ev) => ev.stopPropagation()}>
{cellInspect && (
<IconButton
name="eye"
@@ -12,6 +12,7 @@ import { DataLinksCell } from './DataLinksCell';
import { GeoCell } from './GeoCell';
import { ImageCell } from './ImageCell';
import { JSONCell } from './JSONCell';
import { PillCell } from './PillCell';
import { SparklineCell } from './SparklineCell';
export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode;
@@ -24,7 +25,6 @@ const GAUGE_RENDERER: TableCellRenderer = (props) => (
height={props.height}
width={props.width}
rowIdx={props.rowIdx}
actions={props.actions}
/>
);
@@ -35,7 +35,6 @@ const AUTO_RENDERER: TableCellRenderer = (props) => (
justifyContent={props.justifyContent}
rowIdx={props.rowIdx}
cellOptions={props.cellOptions}
actions={props.actions}
/>
);
@@ -52,13 +51,7 @@ const SPARKLINE_RENDERER: TableCellRenderer = (props) => (
);
const JSON_RENDERER: TableCellRenderer = (props) => (
<JSONCell
justifyContent={props.justifyContent}
value={props.value}
field={props.field}
rowIdx={props.rowIdx}
actions={props.actions}
/>
<JSONCell justifyContent={props.justifyContent} value={props.value} field={props.field} rowIdx={props.rowIdx} />
);
const GEO_RENDERER: TableCellRenderer = (props) => (
@@ -73,13 +66,16 @@ const IMAGE_RENDERER: TableCellRenderer = (props) => (
justifyContent={props.justifyContent}
value={props.value}
rowIdx={props.rowIdx}
actions={props.actions}
/>
);
const DATA_LINKS_RENDERER: TableCellRenderer = (props) => <DataLinksCell field={props.field} rowIdx={props.rowIdx} />;
const ACTIONS_RENDERER: TableCellRenderer = (props) => <ActionsCell actions={props.actions} />;
const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => [] }) => (
<ActionsCell field={field} rowIdx={rowIdx} getActions={getActions} />
);
const PILL_RENDERER: TableCellRenderer = (props) => <PillCell {...props} />;
function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions {
return options.type === TableCellDisplayMode.Custom;
@@ -104,6 +100,7 @@ const CELL_RENDERERS: Record<TableCellOptions['type'], TableCellRenderer> = {
[TableCellDisplayMode.ColorText]: AUTO_RENDERER,
[TableCellDisplayMode.ColorBackground]: AUTO_RENDERER,
[TableCellDisplayMode.Auto]: AUTO_RENDERER,
[TableCellDisplayMode.Pill]: PILL_RENDERER,
};
/** @internal */
@@ -1,7 +1,17 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { applyFieldOverrides, createTheme, DataFrame, EventBus, FieldType, toDataFrame } from '@grafana/data';
import {
applyFieldOverrides,
createTheme,
DataFrame,
DataLink,
EventBus,
FieldType,
LinkModel,
toDataFrame,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { TableCellBackgroundDisplayMode } from '@grafana/schema';
import { PanelContext, PanelContextProvider } from '../../../components/PanelChrome';
@@ -1682,4 +1692,55 @@ describe('TableNG', () => {
expect(mockEventBus.publish).not.toHaveBeenCalled();
});
});
describe('Displays data Links', () => {
function toLinkModel(link: DataLink): LinkModel {
return {
href: link.url,
title: link.title,
target: link.targetBlank ? '_blank' : '_self',
origin: link.origin || 'panel',
};
}
it('shows multiple datalinks in the tooltip', async () => {
const dataFrame = createBasicDataFrame();
const links: DataLink[] = [
{ url: 'http://asdasd.com', title: 'Test Title' },
{ url: 'http://asdasd2.com', title: 'Test Title2' },
];
dataFrame.fields[0].config.links = links;
dataFrame.fields[0].getLinks = () => links.map(toLinkModel);
render(<TableNG enableVirtualization={false} data={dataFrame} width={800} height={600} />);
const cell = screen.getByText('A1');
await userEvent.click(cell);
const tooltip = screen.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper);
expect(tooltip).toBeInTheDocument();
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Title2')).toBeInTheDocument();
});
it('does not show tooltip for a single link', async () => {
const dataFrame = createBasicDataFrame();
const links: DataLink[] = [{ url: 'http://asdasd.com', title: 'Test Title' }];
dataFrame.fields[0].config.links = links;
dataFrame.fields[0].getLinks = () => links.map(toLinkModel);
render(<TableNG enableVirtualization={false} data={dataFrame} width={800} height={600} />);
const cell = screen.getByText('A1');
// we need to click the parent since the cell itself is a link.
await userEvent.click(cell.parentElement!);
expect(screen.queryByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).not.toBeInTheDocument();
});
});
});
@@ -1,7 +1,7 @@
import 'react-data-grid/lib/styles.css';
import { css, cx } from '@emotion/css';
import { Property } from 'csstype';
import { Key, ReactNode, useLayoutEffect, useMemo, useState } from 'react';
import { Key, ReactNode, useCallback, useLayoutEffect, useMemo, useState } from 'react';
import {
Cell,
CellRendererProps,
@@ -22,8 +22,10 @@ import { ContextMenu } from '../../ContextMenu/ContextMenu';
import { MenuItem } from '../../Menu/MenuItem';
import { Pagination } from '../../Pagination/Pagination';
import { PanelContext, usePanelContext } from '../../PanelChrome';
import { DataLinksActionsTooltip } from '../DataLinksActionsTooltip';
import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspector';
import { CellColors, TableCellDisplayMode } from '../types';
import { DataLinksActionsTooltipState } from '../utils';
import { HeaderCell } from './Cells/HeaderCell';
import { RowExpander } from './Cells/RowExpander';
@@ -56,6 +58,8 @@ import {
getCellOptions,
shouldTextWrap,
isCellInspectEnabled,
getCellLinks,
withDataLinksActionsTooltip,
} from './utils';
type CellRootRenderer = (key: React.Key, props: CellRendererProps<TableRow, TableSummaryRow>) => React.ReactNode;
@@ -68,14 +72,13 @@ export function TableNG(props: TableNGProps) {
enableSharedCrosshair = false,
enableVirtualization,
footerOptions,
getActions,
getActions = () => [],
height,
initialSortBy,
noHeader,
onCellFilterAdded,
onColumnResize,
onSortByChange,
replaceVariables,
showTypeIcons,
structureRev,
width,
@@ -88,6 +91,11 @@ export function TableNG(props: TableNGProps) {
});
const panelContext = usePanelContext();
const getCellActions = useCallback(
(field: Field, rowIdx: number) => getActions(data, field, rowIdx),
[getActions, data]
);
const hasHeader = !noHeader;
const hasFooter = Boolean(footerOptions?.show && footerOptions.reducer?.length);
const isCountRowsSet = Boolean(
@@ -256,13 +264,15 @@ export function TableNG(props: TableNGProps) {
interface Schema {
columns: TableColumn[];
cellRootRenderers: Record<string, CellRootRenderer>;
colsWithTooltip: Record<string, boolean>;
}
const { columns, cellRootRenderers } = useMemo(() => {
const { columns, cellRootRenderers, colsWithTooltip } = useMemo(() => {
const fromFields = (f: Field[], widths: number[]) => {
const result: Schema = {
columns: [],
cellRootRenderers: {},
colsWithTooltip: {},
};
let lastRowIdx = -1;
@@ -280,7 +290,6 @@ export function TableNG(props: TableNGProps) {
const showFilters = Boolean(field.config.filterable && onCellFilterAdded != null);
const showActions = cellInspect || showFilters;
const width = widths[i];
const frame = data;
// helps us avoid string cx and emotion per-cell
const cellActionClassName = showActions
@@ -294,6 +303,9 @@ export function TableNG(props: TableNGProps) {
const cellType = cellOptions.type;
const shouldOverflow = shouldTextOverflow(field);
const shouldWrap = shouldTextWrap(field);
const withTooltip = withDataLinksActionsTooltip(field, cellType);
result.colsWithTooltip[displayName] = withTooltip;
// this fires first
const renderCellRoot = (key: Key, props: CellRendererProps<TableRow, TableSummaryRow>): ReactNode => {
@@ -317,7 +329,7 @@ export function TableNG(props: TableNGProps) {
colors = {};
}
const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, colors);
const cellStyle = getCellStyles(theme, field, _rowHeight, shouldWrap, shouldOverflow, withTooltip, colors);
return (
<Cell
@@ -335,14 +347,11 @@ export function TableNG(props: TableNGProps) {
const renderCellContent = (props: RenderCellProps<TableRow, TableSummaryRow>): JSX.Element => {
const rowIdx = props.row.__index;
const value = props.row[props.column.key];
// TODO: defer until click?
const actions = getActions?.(frame, field, props.row.__index, replaceVariables);
const frame = data;
return (
<>
{renderFieldCell({
actions,
cellOptions,
frame,
field,
@@ -354,6 +363,7 @@ export function TableNG(props: TableNGProps) {
width,
cellInspect,
showFilters,
getActions: getCellActions,
})}
{showActions && (
<TableCellActions
@@ -504,12 +514,10 @@ export function TableNG(props: TableNGProps) {
expandedRows,
filter,
footerCalcs,
getActions,
hasNestedFrames,
isCountRowsSet,
onCellFilterAdded,
panelContext,
replaceVariables,
rowHeight,
rows,
setFilter,
@@ -519,6 +527,7 @@ export function TableNG(props: TableNGProps) {
theme,
visibleFields,
widths,
getCellActions,
]);
// invalidate columns on every structureRev change. this supports width editing in the fieldConfig.
@@ -534,6 +543,8 @@ export function TableNG(props: TableNGProps) {
return cellRootRenderers[props.column.key](key, props);
};
const [tooltipState, setTooltipState] = useState<DataLinksActionsTooltipState>();
return (
<>
<DataGrid<TableRow, TableSummaryRow>
@@ -541,6 +552,24 @@ export function TableNG(props: TableNGProps) {
className={styles.grid}
columns={structureRevColumns}
rows={paginatedRows}
onCellClick={({ column, row }, { clientX, clientY, preventGridDefault }) => {
// Note: could be column.field; JS says yes, but TS says no!
const field = columns[column.idx].field;
if (colsWithTooltip[getDisplayName(field)]) {
const rowIdx = row.__index;
setTooltipState({
coords: {
clientX,
clientY,
},
links: getCellLinks(field, rowIdx),
actions: getCellActions(field, rowIdx),
});
preventGridDefault();
}
}}
onCellKeyDown={
hasNestedFrames
? (_, event) => {
@@ -577,6 +606,15 @@ export function TableNG(props: TableNGProps) {
</div>
)}
{tooltipState && (
<DataLinksActionsTooltip
links={tooltipState.links ?? []}
actions={tooltipState.actions}
coords={tooltipState.coords}
onTooltipClose={() => setTooltipState(undefined)}
/>
)}
{isContextMenuOpen && (
<ContextMenu
x={contextMenuProps?.left || 0}
@@ -770,31 +808,35 @@ const getCellStyles = (
rowHeight: number,
shouldWrap: boolean,
shouldOverflow: boolean,
hasTooltip: boolean,
colors: CellColors
) => ({
cell: css({
textOverflow: 'initial',
background: colors.bgColor ?? 'inherit',
alignContent: 'center',
justifyContent: getTextAlign(field),
paddingInline: TABLE.CELL_PADDING,
height: '100%',
minHeight: rowHeight, // min height interacts with the fit-content property on the overflow container
...(shouldWrap && { whiteSpace: 'pre-line' }),
'&:last-child': {
borderInlineEnd: 'none',
},
'&:hover': {
background: colors.bgHoverColor,
'.table-cell-actions': {
display: 'flex',
) => {
return {
cell: css({
textOverflow: 'initial',
background: colors.bgColor ?? 'inherit',
alignContent: 'center',
justifyContent: getTextAlign(field),
paddingInline: TABLE.CELL_PADDING,
height: '100%',
minHeight: rowHeight, // min height interacts with the fit-content property on the overflow container
...(shouldWrap && { whiteSpace: 'pre-line' }),
...(hasTooltip && { cursor: 'pointer' }),
'&:last-child': {
borderInlineEnd: 'none',
},
...(shouldOverflow && {
zIndex: theme.zIndex.tooltip - 2,
whiteSpace: 'pre-line',
height: 'fit-content',
minWidth: 'fit-content',
}),
},
}),
});
'&:hover': {
background: colors.bgHoverColor,
'.table-cell-actions': {
display: 'flex',
},
...(shouldOverflow && {
zIndex: theme.zIndex.tooltip - 2,
whiteSpace: 'pre-line',
height: 'fit-content',
minWidth: 'fit-content',
}),
},
}),
};
};
@@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect } fr
import { Column, DataGridProps, SortColumn } from 'react-data-grid';
import { varPreLine } from 'uwrap';
import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data';
import { Field, fieldReducers, FieldType, formattedValueToString, LinkModel, reduceField } from '@grafana/data';
import { useTheme2 } from '../../../themes/ThemeContext';
import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types';
@@ -17,6 +17,7 @@ import {
getColumnTypes,
GetMaxWrapCellOptions,
getMaxWrapCell,
getCellLinks,
} from './utils';
// Helper function to get displayed value
@@ -597,3 +598,10 @@ export function useColumnResize(
return dataGridResizeHandler;
}
export function useSingleLink(field: Field, rowIdx: number): LinkModel | undefined {
const linksCount = field.config.links?.length ?? 0;
const actionsCount = field.config.actions?.length ?? 0;
const shouldShowLink = linksCount === 1 && actionsCount === 0;
return useMemo(() => (shouldShowLink ? (getCellLinks(field, rowIdx) ?? []) : [])[0], [field, shouldShowLink, rowIdx]);
}
@@ -10,7 +10,6 @@ import {
TimeRange,
FieldConfigSource,
ActionModel,
InterpolateFunction,
FieldType,
DataFrameWithValue,
SelectableValue,
@@ -30,12 +29,9 @@ export type TableColumnResizeActionCallback = (fieldDisplayName: string, width:
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
export type FooterItem = Array<KeyValue<string>> | string | undefined;
export type GetActionsFunction = (
frame: DataFrame,
field: Field,
rowIndex: number,
replaceVariables?: InterpolateFunction
) => ActionModel[];
export type GetActionsFunction = (frame: DataFrame, field: Field, rowIndex: number) => ActionModel[];
export type GetActionsFunctionLocal = (field: Field, rowIndex: number) => ActionModel[];
export type TableFieldOptionsType = Omit<TableFieldOptions, 'cellOptions'> & {
cellOptions: TableCellOptions;
@@ -142,7 +138,6 @@ export interface BaseTableProps {
initialRowIndex?: number;
fieldConfig?: FieldConfigSource;
getActions?: GetActionsFunction;
replaceVariables?: InterpolateFunction;
// Used solely for testing as RTL can't correctly render the table otherwise
enableVirtualization?: boolean;
}
@@ -151,7 +146,6 @@ export interface BaseTableProps {
export interface TableNGProps extends BaseTableProps {}
export interface TableCellRendererProps {
actions?: ActionModel[];
rowIdx: number;
frame: DataFrame;
timeRange?: TimeRange;
@@ -165,6 +159,7 @@ export interface TableCellRendererProps {
cellInspect: boolean;
showFilters: boolean;
justifyContent: Property.JustifyContent;
getActions?: GetActionsFunctionLocal;
}
export type ContextMenuProps = {
@@ -205,7 +200,7 @@ export interface SparklineCellProps {
width: number;
}
export interface BarGaugeCellProps extends ActionCellProps {
export interface BarGaugeCellProps {
field: Field;
height: number;
rowIdx: number;
@@ -214,7 +209,7 @@ export interface BarGaugeCellProps extends ActionCellProps {
width: number;
}
export interface ImageCellProps extends ActionCellProps {
export interface ImageCellProps {
cellOptions: TableCellOptions;
field: Field;
height: number;
@@ -223,7 +218,7 @@ export interface ImageCellProps extends ActionCellProps {
rowIdx: number;
}
export interface JSONCellProps extends ActionCellProps {
export interface JSONCellProps {
justifyContent: Property.JustifyContent;
value: TableCellValue;
field: Field;
@@ -241,24 +236,26 @@ export interface GeoCellProps {
height: number;
}
export interface ActionCellProps {
actions?: ActionModel[];
}
export interface CellColors {
textColor?: string;
bgColor?: string;
bgHoverColor?: string;
}
export interface AutoCellProps extends ActionCellProps {
value: TableCellValue;
export interface AutoCellProps {
field: Field;
value: TableCellValue;
justifyContent: Property.JustifyContent;
rowIdx: number;
cellOptions: TableCellOptions;
}
export interface ActionCellProps {
field: Field;
rowIdx: number;
getActions: GetActionsFunctionLocal;
}
// Comparator for sorting table values
export type Comparator = (a: TableCellValue, b: TableCellValue) => number;
@@ -172,6 +172,7 @@ const DEFAULT_CELL_OPTIONS = { type: TableCellDisplayMode.Auto } as const;
/**
* @internal
* Returns the cell options for a field, migrating from legacy displayMode if necessary.
* TODO: remove live migration in favor of doing it in dashboard or panel migrator
*/
export function getCellOptions(field: Field): TableCellOptions {
if (field.config.custom?.displayMode) {
@@ -613,3 +614,12 @@ export function getApplyToRowBgFn(fields: Field[], theme: GrafanaTheme2): ((rowI
}
}
}
/** @internal */
export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDisplayMode) {
return (
cellType !== TableCellDisplayMode.DataLinks &&
cellType !== TableCellDisplayMode.Actions &&
(field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1
);
}
@@ -57,7 +57,7 @@ export interface TableCellProps extends CellProps<any> {
onCellFilterAdded?: TableFilterActionCallback;
innerWidth: number;
frame: DataFrame;
actions?: ActionModel[];
actions?: ActionModel[]; // unused in NG
setInspectCell?: TableInspectCellCallback;
}
@@ -196,6 +196,8 @@ export function getCellComponent(displayMode: TableCellDisplayMode, field: Field
return DataLinksCell;
case TableCellDisplayMode.Actions:
return ActionsCell;
case TableCellDisplayMode.Pill:
return DefaultCell; // Legacy table doesn't support pill cells, fallback to default
}
if (field.type === FieldType.geo) {
@@ -764,10 +766,16 @@ export function guessLongestField(fieldConfig: FieldConfigSource, data: DataFram
return longestField;
}
export type DataLinksActionsTooltipCoords = {
export interface DataLinksActionsTooltipState {
coords: DataLinksActionsTooltipCoords;
links?: LinkModel[];
actions?: ActionModel[];
}
export interface DataLinksActionsTooltipCoords {
clientX: number;
clientY: number;
};
}
export const getDataLinksActionsTooltipUtils = (links: LinkModel[], actions?: ActionModel[]) => {
const hasMultipleLinksOrActions = links.length > 1 || Boolean(actions?.length);
+150 -76
View File
@@ -245,78 +245,136 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
defer s.reloadMutex.RUnlock()
s.log.Debug("Getting user info")
toCheck := make([]*UserInfoJson, 0, 2)
if tokenData := s.extractFromToken(token); tokenData != nil {
toCheck = append(toCheck, tokenData)
// 1. Collect user info data from various sources
dataSources := s.collectUserInfoData(ctx, client, token)
// 2. Build user info from collected data
userInfo, externalOrgs, err := s.buildUserInfo(dataSources)
if err != nil {
return nil, err
}
// 3. Post-process user info
err = s.postProcessUserInfo(ctx, client, userInfo, externalOrgs)
if err != nil {
return nil, err
}
// 4. Validate user access
err = s.validateUserAccess(ctx, client, userInfo)
if err != nil {
return nil, err
}
s.log.Debug("User info result", "result", userInfo)
return userInfo, nil
}
// collectUserInfoData gathers user information from ID token, API, and access token
func (s *SocialGenericOAuth) collectUserInfoData(ctx context.Context, client *http.Client, token *oauth2.Token) []*UserInfoJson {
dataSources := make([]*UserInfoJson, 0, 3)
if idTokenData := s.extractFromIDToken(token); idTokenData != nil {
dataSources = append(dataSources, idTokenData)
}
if apiData := s.extractFromAPI(ctx, client); apiData != nil {
toCheck = append(toCheck, apiData)
dataSources = append(dataSources, apiData)
}
if accessTokenData := s.extractFromAccessToken(token); accessTokenData != nil {
dataSources = append(dataSources, accessTokenData)
}
return dataSources
}
// buildUserInfo constructs BasicUserInfo from collected data sources
func (s *SocialGenericOAuth) buildUserInfo(dataSources []*UserInfoJson) (*social.BasicUserInfo, []string, error) {
userInfo := &social.BasicUserInfo{}
var externalOrgs []string
for _, data := range toCheck {
for _, data := range dataSources {
s.log.Debug("Processing external user info", "source", data.source, "data", data)
if userInfo.Id == "" {
userInfo.Id = data.Sub
s.extractBasicUserFields(userInfo, data)
if err := s.extractRoleAndOrgs(userInfo, &externalOrgs, data); err != nil {
return nil, nil, err
}
if userInfo.Name == "" {
userInfo.Name = s.extractUserName(data)
}
s.extractUserGroups(userInfo, data)
}
if userInfo.Login == "" {
userInfo.Login = s.extractLogin(data)
}
return userInfo, externalOrgs, nil
}
if userInfo.Email == "" {
userInfo.Email = s.extractEmail(data)
if userInfo.Email != "" {
s.log.Debug("Set user info email from extracted email", "email", userInfo.Email)
}
}
// extractBasicUserFields extracts basic user fields (ID, Name, Login, Email) from data
func (s *SocialGenericOAuth) extractBasicUserFields(userInfo *social.BasicUserInfo, data *UserInfoJson) {
if userInfo.Id == "" {
userInfo.Id = data.Sub
}
if userInfo.Role == "" && !s.info.SkipOrgRoleSync {
role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{})
if err != nil {
s.log.Warn("Failed to extract role", "err", err)
} else {
userInfo.Role = role
if s.info.AllowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin
}
}
}
if userInfo.Name == "" {
userInfo.Name = s.extractUserName(data)
}
if len(externalOrgs) == 0 && !s.info.SkipOrgRoleSync {
var err error
externalOrgs, err = s.extractOrgs(data.rawJSON)
if err != nil {
s.log.Warn("Failed to extract orgs", "err", err)
return nil, err
}
}
if userInfo.Login == "" {
userInfo.Login = s.extractLogin(data)
}
if len(userInfo.Groups) == 0 {
groups, err := s.extractGroups(data)
if err != nil {
s.log.Warn("Failed to extract groups", "err", err)
} else if len(groups) > 0 {
s.log.Debug("Setting user info groups from extracted groups")
userInfo.Groups = groups
if userInfo.Email == "" {
userInfo.Email = s.extractEmail(data)
if userInfo.Email != "" {
s.log.Debug("Set user info email from extracted email", "email", userInfo.Email)
}
}
}
// extractRoleAndOrgs extracts role and organization information from data
func (s *SocialGenericOAuth) extractRoleAndOrgs(userInfo *social.BasicUserInfo, externalOrgs *[]string, data *UserInfoJson) error {
if userInfo.Role == "" && !s.info.SkipOrgRoleSync {
role, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, []string{})
if err != nil {
s.log.Warn("Failed to extract role", "err", err)
} else {
userInfo.Role = role
if s.info.AllowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin
}
}
}
if len(*externalOrgs) == 0 && !s.info.SkipOrgRoleSync {
orgs, err := s.extractOrgs(data.rawJSON)
if err != nil {
s.log.Warn("Failed to extract orgs", "err", err)
return err
}
*externalOrgs = orgs
}
return nil
}
// extractUserGroups extracts group information from data
func (s *SocialGenericOAuth) extractUserGroups(userInfo *social.BasicUserInfo, data *UserInfoJson) {
if len(userInfo.Groups) == 0 {
groups, err := s.extractGroups(data)
if err != nil {
s.log.Warn("Failed to extract groups", "err", err)
} else if len(groups) > 0 {
s.log.Debug("Setting user info groups from extracted groups")
userInfo.Groups = groups
}
}
}
// postProcessUserInfo handles post-processing of user info (org roles, private email, etc.)
func (s *SocialGenericOAuth) postProcessUserInfo(ctx context.Context, client *http.Client, userInfo *social.BasicUserInfo, externalOrgs []string) error {
if !s.info.SkipOrgRoleSync {
userInfo.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, externalOrgs, userInfo.Role)
if s.info.RoleAttributeStrict && len(userInfo.OrgRoles) == 0 {
// If no roles are found and role_attribute_strict is set, return an error.
// The s.info.RoleAttributeStrict is necessary, because there is a case when len(userInfo.OrgRoles) == 0,
// but strict role mapping is not enabled (when getAllOrgs fails).
return nil, errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data")
return errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data")
}
}
@@ -325,11 +383,11 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
}
if s.canFetchPrivateEmail(userInfo) {
var err error
userInfo.Email, err = s.fetchPrivateEmail(ctx, client)
email, err := s.fetchPrivateEmail(ctx, client)
if err != nil {
return nil, err
return err
}
userInfo.Email = email
s.log.Debug("Setting email from fetched private email", "email", userInfo.Email)
}
@@ -338,28 +396,32 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
userInfo.Login = userInfo.Email
}
return nil
}
// validateUserAccess validates user access based on team, organization, and group membership
func (s *SocialGenericOAuth) validateUserAccess(ctx context.Context, client *http.Client, userInfo *social.BasicUserInfo) error {
if !s.isTeamMember(ctx, client) {
return nil, &SocialError{"User not a member of one of the required teams"}
return &SocialError{"User not a member of one of the required teams"}
}
if !s.isOrganizationMember(ctx, client) {
return nil, &SocialError{"User not a member of one of the required organizations"}
return &SocialError{"User not a member of one of the required organizations"}
}
if !s.isGroupMember(userInfo.Groups) {
return nil, errMissingGroupMembership
return errMissingGroupMembership
}
s.log.Debug("User info result", "result", userInfo)
return userInfo, nil
return nil
}
func (s *SocialGenericOAuth) canFetchPrivateEmail(userinfo *social.BasicUserInfo) bool {
return s.info.ApiUrl != "" && userinfo.Email == ""
}
func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson {
s.log.Debug("Extracting user info from OAuth token")
func (s *SocialGenericOAuth) extractFromIDToken(token *oauth2.Token) *UserInfoJson {
s.log.Debug("Extracting user info from OAuth ID token")
idTokenAttribute := "id_token"
if s.idTokenAttributeName != "" {
@@ -373,21 +435,44 @@ func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson
return nil
}
rawJSON, err := s.retrieveRawIDToken(idToken)
rawJSON, err := s.retrieveRawJWTPayload(idToken)
if err != nil {
s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", token))
s.log.Warn("Error retrieving id_token payload", "error", err, "token", fmt.Sprintf("%+v", token))
return nil
}
return s.parseUserInfoFromJSON(rawJSON, "id_token")
}
func (s *SocialGenericOAuth) extractFromAccessToken(token *oauth2.Token) *UserInfoJson {
s.log.Debug("Extracting user info from OAuth access token")
accessToken := token.AccessToken
if accessToken == "" {
s.log.Debug("No access token found")
return nil
}
rawJSON, err := s.retrieveRawJWTPayload(accessToken)
if err != nil {
s.log.Warn("Error retrieving access token payload", "error", err)
return nil
}
return s.parseUserInfoFromJSON(rawJSON, "access_token")
}
// parseUserInfoFromJSON is a helper method to parse UserInfoJson from raw JSON and source
func (s *SocialGenericOAuth) parseUserInfoFromJSON(rawJSON []byte, source string) *UserInfoJson {
var data UserInfoJson
if err := json.Unmarshal(rawJSON, &data); err != nil {
s.log.Error("Error decoding id_token JSON", "raw_json", string(rawJSON), "error", err)
s.log.Error("Error decoding user info JSON", "raw_json", string(rawJSON), "error", err, "source", source)
return nil
}
data.rawJSON = rawJSON
data.source = "token"
s.log.Debug("Received id_token", "raw_json", string(data.rawJSON), "data", data.String())
data.source = source
s.log.Debug("Parsed user info from JSON", "raw_json", string(rawJSON), "data", data.String(), "source", source)
return &data
}
@@ -404,18 +489,7 @@ func (s *SocialGenericOAuth) extractFromAPI(ctx context.Context, client *http.Cl
return nil
}
rawJSON := rawUserInfoResponse.Body
var data UserInfoJson
if err := json.Unmarshal(rawJSON, &data); err != nil {
s.log.Error("Error decoding user info response", "raw_json", rawJSON, "error", err)
return nil
}
data.rawJSON = rawJSON
data.source = "API"
s.log.Debug("Received user info response from API", "raw_json", string(rawJSON), "data", data.String())
return &data
return s.parseUserInfoFromJSON(rawUserInfoResponse.Body, "API")
}
func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string {
@@ -31,6 +31,7 @@ func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) {
AllowAssignGrafanaAdmin bool
ResponseBody any
OAuth2Extra any
AccessToken string
Setup func(*orgtest.FakeOrgService)
RoleAttributePath string
RoleAttributeStrict bool
@@ -440,6 +441,62 @@ func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) {
ExpectedEmail: "john.doe@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleViewer},
},
// Access Token Test Cases
{
Name: "Given a valid access token with role, no ID token, no API response, use access token",
ResponseBody: map[string]any{},
OAuth2Extra: map[string]any{},
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiRWRpdG9yIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20ifQ.oVEMSJVqBwrGXOcwGgXL_8J-CZhgFVPjXXSqzPJQ5JU", // { "role": "Editor", "email": "access.token@example.com" }
RoleAttributePath: "role",
ExpectedEmail: "access.token@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
},
{
Name: "Given a valid access token with org roles, no ID token, no API response, use access token",
ResponseBody: map[string]any{},
OAuth2Extra: map[string]any{},
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiVmlld2VyIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20iLCJpbmZvIjp7InJvbGVzIjpbImFjY2Vzcy1kZXYiLCJhY2Nlc3Mtb3BzIl19fQ.g8-mNJQDL9CJWgRTFdKBRRKbsHZfFhJrzPYQGXfxGIE", // { "role": "Viewer", "email": "access.token@example.com", "info": { "roles": [ "access-dev", "access-ops" ] }}
RoleAttributePath: "role",
OrgAttributePath: "info.roles",
OrgMapping: []string{"access-dev:org_dev:Admin", "access-ops:org_engineering:Editor"},
ExpectedEmail: "access.token@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{4: org.RoleAdmin, 5: org.RoleEditor},
},
{
Name: "Given a valid access token and ID token, prefer ID token",
ResponseBody: map[string]any{},
OAuth2Extra: map[string]any{
// { "role": "Admin", "email": "id.token@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6ImlkLnRva2VuQGV4YW1wbGUuY29tIn0.T8wcoOOPQ_av9VsOFoYJZGNFGJgG0d3LPDvtxvgODkU",
},
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiRWRpdG9yIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20ifQ.oVEMSJVqBwrGXOcwGgXL_8J-CZhgFVPjXXSqzPJQ5JU", // { "role": "Editor", "email": "access.token@example.com" }
RoleAttributePath: "role",
ExpectedEmail: "id.token@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
{
Name: "Given a valid access token with no email, ID token with no role, API response with no data, merge",
ResponseBody: map[string]any{},
OAuth2Extra: map[string]any{
// { "email": "id.token@example.com" }
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImlkLnRva2VuQGV4YW1wbGUuY29tIn0.k5GwPcZvGe2BE_jgwN0ntz0nz4KlYhEd0hRRLApkTJ4",
},
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiRWRpdG9yIn0.gfnKWZKNFNqrILhHFzabBVEWnJJIZBmQSBwLPCHhLUY", // { "role": "Editor" }
RoleAttributePath: "role",
ExpectedEmail: "id.token@example.com",
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleEditor},
},
{
Name: "Given a valid access token with GrafanaAdmin role and AssignGrafanaAdmin enabled",
AllowAssignGrafanaAdmin: true,
ResponseBody: map[string]any{},
OAuth2Extra: map[string]any{},
AccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiR3JhZmFuYUFkbWluIiwiZW1haWwiOiJhY2Nlc3MudG9rZW5AZXhhbXBsZS5jb20ifQ.fJPjMgZW9bOYXOLgOUekNQmNrVbUNhU1iqQJwqFWzUY", // { "role": "GrafanaAdmin", "email": "access.token@example.com" }
RoleAttributePath: "role",
ExpectedEmail: "access.token@example.com",
ExpectedGrafanaAdmin: trueBoolPtr(),
ExpectedOrgRoles: map[int64]org.RoleType{2: org.RoleAdmin},
},
}
cfg := &setting.Cfg{
@@ -479,8 +536,9 @@ func TestUserInfoSearchesForEmailAndOrgRoles(t *testing.T) {
require.NoError(t, err)
}))
provider.info.ApiUrl = ts.URL
staticToken := oauth2.Token{
AccessToken: "",
AccessToken: tc.AccessToken,
TokenType: "",
RefreshToken: "",
Expiry: time.Now(),
@@ -853,7 +911,7 @@ func TestPayloadCompression(t *testing.T) {
}
token := staticToken.WithExtra(test.OAuth2Extra)
userInfo := provider.extractFromToken(token)
userInfo := provider.extractFromIDToken(token)
if test.ExpectedEmail == "" {
require.Nil(t, userInfo, "Testing case %q", test.Name)
+1 -1
View File
@@ -275,7 +275,7 @@ func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client
return nil, nil
}
rawJSON, err := s.retrieveRawIDToken(idToken)
rawJSON, err := s.retrieveRawJWTPayload(idToken)
if err != nil {
s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken))
return nil, nil
+1 -1
View File
@@ -236,7 +236,7 @@ func (s *SocialGoogle) extractFromToken(_ context.Context, _ *http.Client, token
return nil, nil
}
rawJSON, err := s.retrieveRawIDToken(idToken)
rawJSON, err := s.retrieveRawJWTPayload(idToken)
if err != nil {
s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken))
return nil, nil
+5 -5
View File
@@ -196,21 +196,21 @@ func (s *SocialBase) isGroupMember(groups []string) bool {
return false
}
func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) {
tokenString, ok := idToken.(string)
func (s *SocialBase) retrieveRawJWTPayload(token any) ([]byte, error) {
tokenString, ok := token.(string)
if !ok {
return nil, fmt.Errorf("id_token is not a string: %v", idToken)
return nil, fmt.Errorf("token is not a string: %v", token)
}
jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$")
matched := jwtRegexp.FindStringSubmatch(tokenString)
if matched == nil {
return nil, fmt.Errorf("id_token is not in JWT format: %s", tokenString)
return nil, fmt.Errorf("token is not in JWT format: %s", tokenString)
}
rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2])
if err != nil {
return nil, fmt.Errorf("error base64 decoding id_token: %w", err)
return nil, fmt.Errorf("error base64 decoding token payload: %w", err)
}
headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1])
-3
View File
@@ -114,9 +114,6 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
if !s.cfg.JWTAuth.SkipOrgRoleSync {
role, grafanaAdmin := s.extractRoleAndAdmin(claims)
if err != nil {
s.log.Warn("Failed to extract role", "err", err)
}
if s.cfg.JWTAuth.AllowAssignGrafanaAdmin {
id.IsGrafanaAdmin = &grafanaAdmin
+6 -4
View File
@@ -861,7 +861,7 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
}
// TestFoldersCreateAPIEndpointK8S is the counterpart of pkg/api/folder_test.go TestFoldersCreateAPIEndpoint
func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -902,7 +902,7 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
description: "folder creation fails without permissions to create a folder",
input: folderWithoutParentInput,
expectedCode: http.StatusForbidden,
expectedMessage: dashboards.ErrFolderAccessDenied.Error(),
expectedMessage: fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", "folders:create"),
permissions: []resourcepermissions.SetResourcePermissionCommand{},
},
{
@@ -1022,7 +1022,7 @@ func testDescription(description string, expectedErr error) string {
}
// There are no counterpart of TestFoldersGetAPIEndpointK8S in pkg/api/folder_test.go
func TestFoldersGetAPIEndpointK8S(t *testing.T) {
func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -1062,6 +1062,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) {
expectedOutput: []dtos.FolderSearchHit{
{UID: "foo", Title: "Folder 1"},
{UID: "qux", Title: "Folder 3"},
{UID: folder.SharedWithMeFolder.UID, Title: folder.SharedWithMeFolder.Title},
},
permissions: folderReadAndCreatePermission,
},
@@ -1107,7 +1108,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) {
}
// test on all dualwriter modes
for mode := 1; mode <= 4; mode++ {
for mode := 0; mode <= 4; mode++ {
for _, tc := range tcs {
t.Run(fmt.Sprintf("Mode: %d, %s", mode, tc.description), func(t *testing.T) {
modeDw := grafanarest.DualWriterMode(mode)
@@ -1123,6 +1124,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) {
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
featuremgmt.FlagUnifiedStorageSearch,
featuremgmt.FlagKubernetesClientDashboardsFolders,
},
})
@@ -210,7 +210,7 @@ export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) {
const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({
folderUid: parentFolder?.uid,
action: 'create',
title: parentFolder?.title,
title: '', // Empty title for new folders
});
if (!initialValues) {
@@ -18,6 +18,9 @@ import { addQuery } from 'app/core/utils/query';
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { ExpressionTypeDropdown } from 'app/features/expressions/components/ExpressionTypeDropdown';
import { ExpressionQueryType } from 'app/features/expressions/types';
import { getDefaults } from 'app/features/expressions/utils/expressionTypes';
import { GroupActionComponents } from 'app/features/query/components/QueryActionComponent';
import { QueryEditorRows } from 'app/features/query/components/QueryEditorRows';
import { QueryGroupTopSection } from 'app/features/query/components/QueryGroup';
@@ -286,9 +289,15 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
return (dsSettings.meta.backend || dsSettings.meta.alerting || dsSettings.meta.mixed) === true;
}
public onAddExpressionClick = () => {
public onAddExpressionOfType = (type: ExpressionQueryType) => {
const queries = this.getQueries();
this.onQueriesChange(addQuery(queries, expressionDatasource.newQuery()));
// Create base expression query with the specified type
const baseQuery = expressionDatasource.newQuery();
const queryWithType = { ...baseQuery, type };
// Apply defaults specific to the expression type
const queryWithDefaults = getDefaults(queryWithType);
this.onQueriesChange(addQuery(queries, queryWithDefaults));
};
public renderExtraActions() {
@@ -316,6 +325,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
if (!datasource || !dsSettings || !data) {
return null;
}
const showAddButton = !isSharedDashboardQuery(dsSettings.name);
const onSelectQueryFromLibrary = async (query: DataQuery) => {
// ensure all queries explicitly define a datasource
@@ -394,16 +404,11 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
</>
)}
{config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && (
<Button
icon="plus"
onClick={model.onAddExpressionClick}
variant="secondary"
data-testid={selectors.components.QueryTab.addExpression}
>
<span>
<ExpressionTypeDropdown handleOnSelect={model.onAddExpressionOfType}>
<Button icon="plus" variant="secondary" data-testid={selectors.components.QueryTab.addExpression}>
<Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.expression">Expression&nbsp;</Trans>
</span>
</Button>
</Button>
</ExpressionTypeDropdown>
)}
{model.renderExtraActions()}
</Stack>
@@ -1,10 +1,12 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useRef } from 'react';
import { DataSourceApi, QueryEditorProps, SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
import { InlineField, Select } from '@grafana/ui';
import { DataSourceApi, GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { Button, IconButton, InlineField, PopoverContent, useStyles2 } from '@grafana/ui';
import { ClassicConditions } from './components/ClassicConditions';
import { ExpressionTypeDropdown } from './components/ExpressionTypeDropdown';
import { Math } from './components/Math';
import { Reduce } from './components/Reduce';
import { Resample } from './components/Resample';
@@ -20,6 +22,24 @@ const labelWidth = 15;
type NonClassicExpressionType = Exclude<ExpressionQueryType, ExpressionQueryType.classic>;
type ExpressionTypeConfigStorage = Partial<Record<NonClassicExpressionType, string>>;
// Help text for each expression type - can be expanded with more detailed content
const getExpressionHelpText = (type: ExpressionQueryType): PopoverContent | string => {
const description = expressionTypes.find(({ value }) => value === type)?.description;
switch (type) {
case ExpressionQueryType.sql:
return (
<Trans i18nKey="expressions.expression-query-editor.helper-text-sql">
Run MySQL-dialect SQL against the tables returned from your data sources. Data source queries (ie "A", "B")
are available as tables and referenced by query-name. Fields are available as columns, as returned from the
data source.
</Trans>
);
default:
return description ?? '';
}
};
function useExpressionsCache() {
const expressionCache = useRef<ExpressionTypeConfigStorage>({});
@@ -62,14 +82,16 @@ export function ExpressionQueryEditor(props: Props) {
const { query, queries, onRunQuery, onChange, app } = props;
const { getCachedExpression, setCachedExpression } = useExpressionsCache();
const styles = useStyles2(getStyles);
useEffect(() => {
setCachedExpression(query.type, query.expression);
}, [query.expression, query.type, setCachedExpression]);
const onSelectExpressionType = useCallback(
(item: SelectableValue<ExpressionQueryType>) => {
const cachedExpression = getCachedExpression(item.value!);
const defaults = getDefaults({ ...query, type: item.value! });
(value: ExpressionQueryType) => {
const cachedExpression = getCachedExpression(value!);
const defaults = getDefaults({ ...query, type: value! });
onChange({ ...defaults, expression: cachedExpression ?? defaults.expression });
},
@@ -100,17 +122,35 @@ export function ExpressionQueryEditor(props: Props) {
}
};
const selected = expressionTypes.find((o) => o.value === query.type);
const helperText = getExpressionHelpText(query.type);
return (
<div>
<InlineField
label={t('expressions.expression-query-editor.label-operation', 'Operation')}
labelWidth={labelWidth}
>
<Select options={expressionTypes} value={selected} onChange={onSelectExpressionType} width={25} />
</InlineField>
<div className={styles.operationRow}>
<InlineField
label={t('expressions.expression-query-editor.label-operation', 'Operation')}
labelWidth={labelWidth}
>
<ExpressionTypeDropdown handleOnSelect={onSelectExpressionType}>
<Button fill="outline" icon="angle-down" iconPlacement="right" variant="secondary">
{expressionTypes.find(({ value }) => value === query.type)?.label}
</Button>
</ExpressionTypeDropdown>
</InlineField>
{helperText && <IconButton className={styles.infoIcon} name="info-circle" tooltip={helperText} />}
</div>
{renderExpressionType()}
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
operationRow: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
infoIcon: css({
marginBottom: theme.spacing(0.5), // Align with the select field
}),
});
@@ -0,0 +1,100 @@
import { css } from '@emotion/css';
import { ReactElement, useCallback, useMemo, memo } from 'react';
import { FeatureState, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Dropdown, FeatureBadge, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui';
import { ExpressionQueryType, expressionTypes } from 'app/features/expressions/types';
const EXPRESSION_ICON_MAP = {
[ExpressionQueryType.math]: 'calculator-alt',
[ExpressionQueryType.reduce]: 'compress-arrows',
[ExpressionQueryType.resample]: 'sync',
[ExpressionQueryType.classic]: 'cog',
[ExpressionQueryType.threshold]: 'sliders-v-alt',
[ExpressionQueryType.sql]: 'database',
} as const satisfies Record<ExpressionQueryType, string>;
interface ExpressionTypeDropdownProps {
children: ReactElement;
handleOnSelect: (value: ExpressionQueryType) => void;
}
interface ExpressionMenuItemProps {
item: SelectableValue<ExpressionQueryType>;
onSelect: (value: ExpressionQueryType) => void;
}
const ExpressionMenuItem = memo<ExpressionMenuItemProps>(({ item, onSelect }) => {
const { value, label, description } = item;
const styles = useStyles2(getStyles);
const handleClick = useCallback(() => onSelect(value!), [value, onSelect]);
return (
<Menu.Item
component={() => (
<div className={styles.expressionTypeItem} role="menuitem">
<div className={styles.expressionTypeItemContent} data-testid={`expression-type-${value}`}>
<Icon className={styles.icon} name={EXPRESSION_ICON_MAP[value!]} aria-hidden="true" />
{label}
{value === ExpressionQueryType.sql && <FeatureBadge featureState={FeatureState.new} />}
</div>
<Tooltip placement="right" content={description!}>
<Icon className={styles.infoIcon} name="info-circle" />
</Tooltip>
</div>
)}
key={value}
label=""
onClick={handleClick}
/>
);
});
ExpressionMenuItem.displayName = 'ExpressionMenuItem';
export const ExpressionTypeDropdown = memo<ExpressionTypeDropdownProps>(({ handleOnSelect, children }) => {
const menuItems = useMemo(
() => expressionTypes.map((item) => <ExpressionMenuItem key={item.value} item={item} onSelect={handleOnSelect} />),
[handleOnSelect]
);
const menuOverlay = useMemo(() => <Menu role="menu">{menuItems}</Menu>, [menuItems]);
return (
<Dropdown placement="bottom-start" overlay={menuOverlay}>
{children}
</Dropdown>
);
});
ExpressionTypeDropdown.displayName = 'ExpressionTypeDropdown';
const getStyles = (theme: GrafanaTheme2) => {
return {
expressionTypeItem: css({
width: '100%',
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
expressionTypeItemContent: css({
flexGrow: 1,
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
icon: css({
color: theme.colors.text.secondary,
flexShrink: 0,
}),
infoIcon: css({
opacity: 0.7,
color: theme.colors.text.secondary,
flexShrink: 0,
}),
};
};
@@ -30,12 +30,10 @@ interface Props {
export const SqlExpr = ({ onChange, refIds, query, alerting = false }: Props) => {
const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]);
const initialQuery = `-- Run MySQL-dialect SQL against the tables returned from your data sources.
-- Data source queries (ie "${vars[0]}") are available as tables and referenced by query-name
-- Fields are available as columns, as returned from the data source.
SELECT *
FROM ${vars[0]}
LIMIT 10`;
const initialQuery = `SELECT *
FROM ${vars[0]}
LIMIT 10`;
const styles = useStyles2(getStyles);
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ height: 0 });
@@ -51,7 +51,7 @@ export const ConvertFieldTypeTransformerEditor = ({
const onSelectField = useCallback(
(idx: number) => (value: string | undefined) => {
const conversions = options.conversions;
const conversions = [...options.conversions];
conversions[idx] = { ...conversions[idx], targetField: value ?? '', dateFormat: undefined };
onChange({
...options,
@@ -36,55 +36,55 @@ describe('UrlAndAuthenticationSection', () => {
expect(onOptionsChangeMock).toHaveBeenCalled();
});
it('renders DRBP warning for InfluxDB OSS 1.x and InfluxQL', () => {
const props = {
...defaultProps,
options: {
...defaultProps.options,
jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.InfluxQL },
},
};
const productsRequiringDBRP = [
'InfluxDB OSS 1.x',
'InfluxDB OSS 2.x',
'InfluxDB Enterprise 1.x',
'InfluxDB Cloud (TSM)',
'InfluxDB Cloud Serverless',
];
render(<UrlAndAuthenticationSection {...props} />);
expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument();
describe('UrlAndAuthenticationSection', () => {
it.each(productsRequiringDBRP)('renders DBRP warning for %s and InfluxQL', (product) => {
const props = {
...defaultProps,
options: {
...defaultProps.options,
jsonData: {
product,
version: InfluxVersion.InfluxQL,
},
},
};
render(<UrlAndAuthenticationSection {...props} />);
expect(screen.getByText(/requires DBRP mapping/i)).toBeInTheDocument();
});
});
it('renders DRBP warning for InfluxDB OSS 2.x and InfluxQL', () => {
it('does not render DBRP warning for SQL', () => {
const props = {
...defaultProps,
options: {
...defaultProps.options,
jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.InfluxQL },
jsonData: { version: InfluxVersion.SQL },
},
};
render(<UrlAndAuthenticationSection {...props} />);
expect(screen.getByText(/requires DRBP mapping/i)).toBeInTheDocument();
expect(screen.queryByText(/requires DBRP mapping/i)).not.toBeInTheDocument();
});
it('does not render DRBP warning for InfluxDB OSS 1.x and Flux', () => {
it('does not render DBRP warning for Flux', () => {
const props = {
...defaultProps,
options: {
...defaultProps.options,
jsonData: { product: 'InfluxDB OSS 1.x', version: InfluxVersion.Flux },
jsonData: { version: InfluxVersion.Flux },
},
};
render(<UrlAndAuthenticationSection {...props} />);
expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument();
});
it('does not render DRBP warning for InfluxDB OSS 2.x and Flux', () => {
const props = {
...defaultProps,
options: {
...defaultProps.options,
jsonData: { product: 'InfluxDB OSS 2.x', version: InfluxVersion.Flux },
},
};
render(<UrlAndAuthenticationSection {...props} />);
expect(screen.queryByText(/requires DRBP mapping/i)).not.toBeInTheDocument();
expect(screen.queryByText(/requires DBRP mapping/i)).not.toBeInTheDocument();
});
});
@@ -38,10 +38,16 @@ export const UrlAndAuthenticationSection = (props: Props) => {
typeof v === 'string' && (v === InfluxVersion.Flux || v === InfluxVersion.InfluxQL || v === InfluxVersion.SQL);
// Database + Retention Policy (DBRP) mapping is required for InfluxDB OSS 1.x and 2.x when using InfluxQL
const requiresDrbpMapping =
const requiresDbrpMapping =
options.jsonData.product &&
options.jsonData.version === InfluxVersion.InfluxQL &&
['InfluxDB OSS 1.x', 'InfluxDB OSS 2.x'].includes(options.jsonData.product);
[
'InfluxDB OSS 1.x',
'InfluxDB OSS 2.x',
'InfluxDB Enterprise 1.x',
'InfluxDB Cloud (TSM)',
'InfluxDB Cloud Serverless',
].includes(options.jsonData.product);
const onProductChange = ({ value }: ComboboxOption) => {
trackInfluxDBConfigV2ProductSelected({ product: value });
@@ -113,8 +119,8 @@ export const UrlAndAuthenticationSection = (props: Props) => {
<Space v={2} />
{requiresDrbpMapping && (
<Alert severity="warning" title="InfluxQL requires DRBP mapping">
{requiresDbrpMapping && (
<Alert severity="warning" title="InfluxQL requires DBRP mapping">
InfluxDB OSS 1.x and 2.x users must configure a Database + Retention Policy (DBRP) mapping via the CLI or
API before data can be queried.{' '}
<TextLink href="https://docs.influxdata.com/influxdb/cloud/query-data/influxql/dbrp/" external>
@@ -30,8 +30,8 @@ export const CONFIG_SECTION_HEADERS_WITH_PDC = [
];
export const HTTP_MODES: ComboboxOption[] = [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'GET', value: 'GET' },
];
export const getInlineLabelStyles = (theme: GrafanaTheme2, transparent = false, width?: number | 'auto') => {
@@ -6,7 +6,7 @@
"dependencies": {
"@emotion/css": "11.13.5",
"@grafana/data": "12.1.0-pre",
"@grafana/lezer-logql": "0.2.7",
"@grafana/lezer-logql": "0.2.8",
"@grafana/llm": "0.22.1",
"@grafana/monaco-logql": "^0.0.8",
"@grafana/runtime": "12.1.0-pre",
@@ -10,6 +10,7 @@ import { AutoCellOptionsEditor } from './cells/AutoCellOptionsEditor';
import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor';
import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor';
import { ImageCellOptionsEditor } from './cells/ImageCellOptionsEditor';
import { PillCellOptionsEditor } from './cells/PillCellOptionsEditor';
import { SparklineCellOptionsEditor } from './cells/SparklineCellOptionsEditor';
// The props that any cell type editor are expected
@@ -77,6 +78,9 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
{cellType === TableCellDisplayMode.Image && (
<ImageCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
)}
{cellType === TableCellDisplayMode.Pill && (
<PillCellOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />
)}
</div>
);
};
@@ -91,6 +95,7 @@ let cellDisplayModeOptions: Array<SelectableValue<TableCellOptions>> = [
{ value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' },
{ value: { type: TableCellDisplayMode.Image }, label: 'Image' },
{ value: { type: TableCellDisplayMode.Actions }, label: 'Actions' },
{ value: { type: TableCellDisplayMode.Pill }, label: 'Pill' },
];
const getStyles = (theme: GrafanaTheme2) => ({
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import {
ActionModel,
@@ -57,6 +57,11 @@ export function TablePanel(props: Props) {
const enableSharedCrosshair = panelContext.sync && panelContext.sync() !== DashboardCursorSync.Off;
const _getActions = useCallback(
(frame: DataFrame, field: Field, rowIndex: number) => getCellActions(frame, field, rowIndex, replaceVariables),
[replaceVariables]
);
const tableElement = (
<TableNG
height={tableHeight}
@@ -75,8 +80,7 @@ export function TablePanel(props: Props) {
timeRange={timeRange}
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
fieldConfig={fieldConfig}
getActions={getCellActions}
replaceVariables={replaceVariables}
getActions={_getActions}
structureRev={data.structureRev}
/>
);
@@ -159,28 +163,39 @@ const getCellActions = (
field: Field,
rowIndex: number,
replaceVariables: InterpolateFunction | undefined
) => {
const actions: Array<ActionModel<Field>> = [];
const actionLookup = new Set<string>();
): Array<ActionModel<Field>> => {
const numActions = field.config.actions?.length ?? 0;
const actionsModel = getActions(
dataFrame,
field,
field.state!.scopedVars!,
replaceVariables ?? replaceVars,
field.config.actions ?? [],
{ valueRowIndex: rowIndex }
);
if (numActions > 0) {
const actions = getActions(
dataFrame,
field,
field.state!.scopedVars!,
replaceVariables ?? replaceVars,
field.config.actions ?? [],
{ valueRowIndex: rowIndex }
);
actionsModel.forEach((action) => {
const key = `${action.title}`;
if (!actionLookup.has(key)) {
actions.push(action);
actionLookup.add(key);
if (actions.length === 1) {
return actions;
} else {
const actionsOut: Array<ActionModel<Field>> = [];
const actionLookup = new Set<string>();
actions.forEach((action) => {
const key = action.title;
if (!actionLookup.has(key)) {
actionsOut.push(action);
actionLookup.add(key);
}
});
return actionsOut;
}
});
}
return actions;
return [];
};
const tableStyles = {
@@ -0,0 +1,66 @@
import { t } from '@grafana/i18n';
import { TablePillCellOptions } from '@grafana/schema';
import { Field, ColorPicker, RadioButtonGroup, Stack } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
const colorModeOptions: Array<{ value: 'auto' | 'fixed' | 'mapped'; label: string }> = [
{ value: 'auto', label: 'Auto' },
{ value: 'fixed', label: 'Fixed color' },
{ value: 'mapped', label: 'Value mapping' },
];
export const PillCellOptionsEditor = ({ cellOptions, onChange }: TableCellEditorProps<TablePillCellOptions>) => {
const colorMode = cellOptions.colorMode || 'auto';
const onColorModeChange = (mode: 'auto' | 'fixed' | 'mapped') => {
const updatedOptions = { ...cellOptions, colorMode: mode };
onChange(updatedOptions);
};
const onColorChange = (color: string) => {
const updatedOptions = { ...cellOptions, color };
onChange(updatedOptions);
};
return (
<Stack direction="column" gap={1}>
<Field
label={t('table.pill-cell-options-editor.label-color-mode', 'Color Mode')}
description={t(
'table.pill-cell-options-editor.description-color-mode',
'Choose how colors are assigned to pills'
)}
noMargin
>
<RadioButtonGroup value={colorMode} onChange={onColorModeChange} options={colorModeOptions} />
</Field>
{colorMode === 'fixed' && (
<Field
label={t('table.pill-cell-options-editor.label-fixed-color', 'Fixed Color')}
description={t(
'table.pill-cell-options-editor.description-fixed-color',
'All pills in this column will use this color'
)}
noMargin
>
<ColorPicker color={cellOptions.color || '#FF780A'} onChange={onColorChange} enableNamedColors={false} />
</Field>
)}
{colorMode === 'mapped' && (
<Field
label={t('table.pill-cell-options-editor.label-value-mappings-info', 'Value Mappings')}
description={t(
'table.pill-cell-options-editor.description-value-mappings-info',
'For Value Mappings either use the global table Value Mappings or the Field overrides Value Mappings. The default will fall back to the Color Scheme. '
)}
noMargin
>
<div>&nbsp;</div>
</Field>
)}
</Stack>
);
};
+37 -16
View File
@@ -4435,7 +4435,6 @@
}
},
"render-left-actions": {
"text-public": "Veřejná",
"tooltip-view-as-scene": "Zobrazit jako scénu"
}
},
@@ -5211,7 +5210,6 @@
"playlist-next": "Přejít na další nástěnku",
"playlist-previous": "Přejít na předchozí nástěnku",
"playlist-stop": "Zastavit playlist",
"public-dashboard": "Veřejná",
"refresh": "Obnovit nástěnku",
"save": "Uložit nástěnku",
"save-dashboard": {
@@ -5512,15 +5510,9 @@
"type": "Typ"
},
"dashboard-preview-banner": {
"not-saved": "Hodnota ještě není uložena v databázi Grafany",
"not-yet-saved": "Hodnota není uložena v databázi Grafany",
"open-pull-request-in-git-hub": "Otevřít pull request v GitHubu",
"title-dashboard-loaded-branch-git-hub": "Tato nástěnka je načtena z větve na GitHubu.",
"title-dashboard-loaded-external-repository": "Tato nástěnka je načtena z externího úložiště",
"title-dashboard-loaded-request-git-hub": "Tato nástěnka je načtena z pull request na GitHubu.",
"title-error-loading-dashboard": "Chyba při načítání nástěnky",
"value-not-saved": "Hodnota ještě není uložena v databázi Grafany",
"view-pull-request-in-git-hub": "Zobrazit pull request v GitHubu"
"title-error-loading-dashboard": "Chyba při načítání nástěnky"
},
"dashboard-scene": {
"text": {
@@ -7259,6 +7251,7 @@
"when": "KDY"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Činnost"
},
"math": {
@@ -8551,7 +8544,7 @@
"usage-count_other": "Použito na {{count}} nástěnkách"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Hledat podle názvu nebo popisu"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Načítání panelu knihovny…"
@@ -8696,12 +8689,18 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_few": "",
"indexed-label_many": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_few": "",
"parsedl-label_many": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_few": "",
"structured-metadata_many": "",
"structured-metadata_other": ""
}
}
},
@@ -8758,6 +8757,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8771,9 +8771,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10369,6 +10371,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Tato funkce je v současné době v aktivním vývoji. Pro nejlepší zážitek a nejnovější vylepšení doporučujeme použít <2>noční sestavení</2> Grafany."
@@ -11834,6 +11847,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Öffentlich",
"tooltip-view-as-scene": "Als Szene anzeigen"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Zum nächsten Dashboard",
"playlist-previous": "Zum vorherigen Dashboard",
"playlist-stop": "Wiedergabeliste stoppen",
"public-dashboard": "Öffentlich",
"refresh": "Dashboard aktualisieren",
"save": "Dashboard speichern",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Typ"
},
"dashboard-preview-banner": {
"not-saved": "Der Wert ist noch nicht in der Grafana-Datenbank gespeichert",
"not-yet-saved": "Der Wert wird nicht in der Grafana-Datenbank gespeichert",
"open-pull-request-in-git-hub": "Pull-Anfrage in GitHub öffnen",
"title-dashboard-loaded-branch-git-hub": "Dieses Dashboard wird von einem Branch in GitHub geladen.",
"title-dashboard-loaded-external-repository": "Dieses Dashboard wird von einem externen Repository geladen",
"title-dashboard-loaded-request-git-hub": "Dieses Dashboard wird von einer Pull-Anfrage in GitHub geladen.",
"title-error-loading-dashboard": "Fehler beim Laden des Dashboards",
"value-not-saved": "Der Wert ist noch nicht in der Grafana-Datenbank gespeichert",
"view-pull-request-in-git-hub": "Pull-Anfrage in GitHub anzeigen"
"title-error-loading-dashboard": "Fehler beim Laden des Dashboards"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "WANN"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Operation"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Verwendet bei {{count}} Dashboards"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Suche nach Name oder Beschreibung"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Bibliotheks-Panel wird geladen ..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Diese Funktion befindet sich momentan in der aktiven Entwicklung. Für die bestmögliche Nutzererfahrung und die neuesten Verbesserungen empfehlen wir die Nutzung von <2>Nightly Build</2> von Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+9
View File
@@ -7209,6 +7209,7 @@
"when": "WHEN"
},
"expression-query-editor": {
"helper-text-sql": "Run MySQL-dialect SQL against the tables returned from your data sources. Data source queries (ie \"A\", \"B\") are available as tables and referenced by query-name. Fields are available as columns, as returned from the data source.",
"label-operation": "Operation"
},
"math": {
@@ -11768,6 +11769,14 @@
"name-show-table-footer": "Show table footer",
"name-show-table-header": "Show table header",
"name-wrap-header-text": "Wrap header text",
"pill-cell-options-editor": {
"description-color-mode": "Choose how colors are assigned to pills",
"description-fixed-color": "All pills in this column will use this color",
"description-value-mappings-info": "For Value Mappings either use the global table Value Mappings or the Field overrides Value Mappings. The default will fall back to the Color Scheme. ",
"label-color-mode": "Color Mode",
"label-fixed-color": "Fixed Color",
"label-value-mappings-info": "Value Mappings"
},
"placeholder-column-width": "auto",
"placeholder-fields": "All Numeric Fields"
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Pública",
"tooltip-view-as-scene": "Ver como escena"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Ir al siguiente panel de control",
"playlist-previous": "Ir al panel de control anterior",
"playlist-stop": "Detener la lista de reproducción",
"public-dashboard": "Pública",
"refresh": "Actualizar panel de control",
"save": "Guardar panel de control",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Tipo"
},
"dashboard-preview-banner": {
"not-saved": "El valor aún no se ha guardado en la base de datos de Grafana",
"not-yet-saved": "El valor no se ha guardado en la base de datos de Grafana",
"open-pull-request-in-git-hub": "Abrir solicitud de extracción en GitHub",
"title-dashboard-loaded-branch-git-hub": "Este panel de control se carga desde una rama en GitHub.",
"title-dashboard-loaded-external-repository": "Este panel de control se carga desde un repositorio externo",
"title-dashboard-loaded-request-git-hub": "Este panel de control se carga desde una solicitud de extracción en GitHub.",
"title-error-loading-dashboard": "Error al cargar el panel de control",
"value-not-saved": "El valor aún no se ha guardado en la base de datos de Grafana",
"view-pull-request-in-git-hub": "Ver solicitud de extracción en GitHub"
"title-error-loading-dashboard": "Error al cargar el panel de control"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "CUÁNDO"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Operación"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Utilizado en {{count}} dashboards"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Buscar por nombre o descripción"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Cargando panel de la librería..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Esta función se encuentra actualmente en desarrollo activo. Para obtener la mejor experiencia y las últimas mejoras, recomendamos utilizar la <2>compilación nocturna</2> de Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Public",
"tooltip-view-as-scene": "Afficher en tant que scène"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Accéder au tableau de bord suivant",
"playlist-previous": "Accéder au tableau de bord précédent",
"playlist-stop": "Arrêter la liste de lecture",
"public-dashboard": "Public",
"refresh": "Actualiser le tableau de bord",
"save": "Enregistrer le tableau de bord",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Type"
},
"dashboard-preview-banner": {
"not-saved": "La valeur n'est pas encore enregistrée dans la base de données Grafana",
"not-yet-saved": "La valeur n'est pas enregistrée dans la base de données Grafana",
"open-pull-request-in-git-hub": "Ouvrir la demande de fusion dans GitHub",
"title-dashboard-loaded-branch-git-hub": "Ce tableau de bord est chargé à partir d'une branche dans GitHub.",
"title-dashboard-loaded-external-repository": "Ce tableau de bord est chargé à partir d'un référentiel externe",
"title-dashboard-loaded-request-git-hub": "Ce tableau de bord est chargé à partir d'une demande de fusion dans GitHub.",
"title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord",
"value-not-saved": "La valeur n'est pas encore enregistrée dans la base de données Grafana",
"view-pull-request-in-git-hub": "Afficher la demande de fusion dans GitHub"
"title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "QUAND"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Opération"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Utilisé sur {{count}} tableaux de bord"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Rechercher par nom ou par description"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Chargement du panneau de la bibliothèque..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Cette fonctionnalité est actuellement en cours de développement. Pour une expérience optimale et les dernières améliorations, nous vous recommandons d’utiliser la <2>compilation nocturne</2> de Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Nyilvános",
"tooltip-view-as-scene": "Megtekintés jelenetként"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Ugrás a következő irányítópulthoz",
"playlist-previous": "Ugrás az előző irányítópulthoz",
"playlist-stop": "Lejátszási lista leállítása",
"public-dashboard": "Nyilvános",
"refresh": "Irányítópult frissítése",
"save": "Irányítópult mentése",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Típus"
},
"dashboard-preview-banner": {
"not-saved": "Az érték még nincs mentve a Grafana-adatbázisban",
"not-yet-saved": "Az érték nincs mentve a Grafana-adatbázisban",
"open-pull-request-in-git-hub": "Összefésülési kérelem megnyitása a GitHubon",
"title-dashboard-loaded-branch-git-hub": "Ez az irányítópult a GitHub egyik ágából töltődik be.",
"title-dashboard-loaded-external-repository": "Ez az irányítópult külső adattárból töltődik be",
"title-dashboard-loaded-request-git-hub": "Ez az irányítópult a GitHub egyik összefésülési kérelméből töltődik be.",
"title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor",
"value-not-saved": "Az érték még nincs mentve a Grafana-adatbázisban",
"view-pull-request-in-git-hub": "Összefésülési kérelem megtekintése a GitHubon"
"title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "WHEN"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Művelet"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "{{count}} irányítópulton használatos"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Keresés név vagy leírás alapján"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Könyvtárpanel betöltése…"
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Ez a funkció jelenleg aktív fejlesztés alatt áll. A legjobb élmény és a legújabb fejlesztések érdekében javasoljuk a Grafana <2>éjszakai buildjének</2> használatát."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+28 -16
View File
@@ -4381,7 +4381,6 @@
}
},
"render-left-actions": {
"text-public": "Publik",
"tooltip-view-as-scene": "Tampilkan sebagai Scene"
}
},
@@ -5154,7 +5153,6 @@
"playlist-next": "Buka dasbor berikutnya",
"playlist-previous": "Buka dasbor sebelumnya",
"playlist-stop": "Hentikan daftar putar",
"public-dashboard": "Publik",
"refresh": "Muat ulang dasbor",
"save": "Simpan dasbor",
"save-dashboard": {
@@ -5455,15 +5453,9 @@
"type": "Jenis"
},
"dashboard-preview-banner": {
"not-saved": "Nilai belum disimpan dalam database Grafana",
"not-yet-saved": "Nilai tidak disimpan dalam database Grafana",
"open-pull-request-in-git-hub": "Buka permintaan penggabungan di GitHub",
"title-dashboard-loaded-branch-git-hub": "Dasbor ini dimuat dari cabang di GitHub.",
"title-dashboard-loaded-external-repository": "Dasbor ini dimuat dari repositori eksternal",
"title-dashboard-loaded-request-git-hub": "Dasbor ini dimuat dari permintaan penggabungan di GitHub.",
"title-error-loading-dashboard": "Kesalahan saat memuat dasbor",
"value-not-saved": "Nilai belum disimpan dalam database Grafana",
"view-pull-request-in-git-hub": "Lihat permintaan penggabungan di GitHub"
"title-error-loading-dashboard": "Kesalahan saat memuat dasbor"
},
"dashboard-scene": {
"text": {
@@ -7196,6 +7188,7 @@
"when": "KAPAN"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Pengoperasian"
},
"math": {
@@ -8479,7 +8472,7 @@
"usage-count_other": "Digunakan pada {{count}} dasbor"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Cari berdasarkan nama atau deskripsi"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Memuat panel pustaka..."
@@ -8618,12 +8611,9 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_other": "",
"parsedl-label_other": "",
"structured-metadata_other": ""
}
}
},
@@ -8680,6 +8670,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8693,9 +8684,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10282,6 +10275,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Fitur ini saat ini sedang dalam pengembangan aktif. Untuk pengalaman terbaik dan peningkatan terbaru, kami merekomendasikan Anda untuk menggunakan <2>versi nightly </2> Grafana."
@@ -11726,6 +11730,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Pubblico",
"tooltip-view-as-scene": "Visualizza come Scena"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Vai al dashboard successivo",
"playlist-previous": "Vai al dashboard precedente",
"playlist-stop": "Interrompi playlist",
"public-dashboard": "Pubblico",
"refresh": "Aggiorna dashboard",
"save": "Salva dashboard",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Tipo"
},
"dashboard-preview-banner": {
"not-saved": "Il valore non è ancora stato salvato nel database di Grafana",
"not-yet-saved": "Il valore non è stato salvato nel database di Grafana",
"open-pull-request-in-git-hub": "Apri richiesta pull in GitHub",
"title-dashboard-loaded-branch-git-hub": "Questo dashboard viene caricato da un ramo in GitHub.",
"title-dashboard-loaded-external-repository": "Questo dashboard viene caricato da un repository esterno",
"title-dashboard-loaded-request-git-hub": "Questo dashboard viene caricato da una richiesta pull in GitHub.",
"title-error-loading-dashboard": "Errore durante il caricamento del dashboard",
"value-not-saved": "Il valore non è ancora stato salvato nel database di Grafana",
"view-pull-request-in-git-hub": "Visualizza richiesta pull in GitHub"
"title-error-loading-dashboard": "Errore durante il caricamento del dashboard"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "QUANDO"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Operazione"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Utilizzato su {{count}} dashboard"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Cerca per nome o descrizione"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Caricamento del pannello della libreria in corso..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Questa funzione è attualmente in fase di sviluppo attivo. Per un'esperienza ottimale e gli ultimi miglioramenti, consigliamo di utilizzare la <2>nightly build</2> di Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+28 -16
View File
@@ -4381,7 +4381,6 @@
}
},
"render-left-actions": {
"text-public": "パブリック",
"tooltip-view-as-scene": "シーンとして表示"
}
},
@@ -5154,7 +5153,6 @@
"playlist-next": "次のダッシュボードに移動",
"playlist-previous": "前のダッシュボードに戻る",
"playlist-stop": "プレイリストを停止",
"public-dashboard": "パブリック",
"refresh": "ダッシュボードを更新",
"save": "ダッシュボードを保存",
"save-dashboard": {
@@ -5455,15 +5453,9 @@
"type": "タイプ"
},
"dashboard-preview-banner": {
"not-saved": "値はまだGrafanaデータベースに保存されていません",
"not-yet-saved": "値はGrafanaデータベースに保存されていません",
"open-pull-request-in-git-hub": "GitHubでプルリクエストを開く",
"title-dashboard-loaded-branch-git-hub": "このダッシュボードはGitHubのブランチから読み込まれます。",
"title-dashboard-loaded-external-repository": "このダッシュボードは外部リポジトリから読み込まれます",
"title-dashboard-loaded-request-git-hub": "このダッシュボードは、GitHubのプルリクエストから読み込まれます。",
"title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました",
"value-not-saved": "値はまだGrafanaデータベースに保存されていません",
"view-pull-request-in-git-hub": "GitHubでプルリクエストを表示する"
"title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました"
},
"dashboard-scene": {
"text": {
@@ -7196,6 +7188,7 @@
"when": "条件"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "操作"
},
"math": {
@@ -8479,7 +8472,7 @@
"usage-count_other": "{{count}}件のダッシュボードで使用中"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "名前と詳細で検索"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "ライブラリパネル読み込み中..."
@@ -8618,12 +8611,9 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_other": "",
"parsedl-label_other": "",
"structured-metadata_other": ""
}
}
},
@@ -8680,6 +8670,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8693,9 +8684,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10282,6 +10275,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "この機能は現在開発中です。最高の体験と最新の改善を利用するには、Grafanaの<2>ナイトリービルド</2>の使用をお勧めします。"
@@ -11726,6 +11730,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+28 -16
View File
@@ -4381,7 +4381,6 @@
}
},
"render-left-actions": {
"text-public": "공개",
"tooltip-view-as-scene": "씬으로 보기"
}
},
@@ -5154,7 +5153,6 @@
"playlist-next": "다음 대시보드로 이동",
"playlist-previous": "이전 대시보드로 이동",
"playlist-stop": "플레이리스트 중지",
"public-dashboard": "공개",
"refresh": "대시보드 새로 고침",
"save": "대시보드 저장",
"save-dashboard": {
@@ -5455,15 +5453,9 @@
"type": "유형"
},
"dashboard-preview-banner": {
"not-saved": "값이 아직 Grafana 데이터베이스에 저장되지 않았습니다.",
"not-yet-saved": "값이 Grafana 데이터베이스에 저장되지 않았습니다.",
"open-pull-request-in-git-hub": "GitHub에서 풀 요청 열기",
"title-dashboard-loaded-branch-git-hub": "이 대시보드는 GitHub의 브랜치에서 로딩됩니다.",
"title-dashboard-loaded-external-repository": "이 대시보드는 외부 리포지토리에서 로딩됩니다.",
"title-dashboard-loaded-request-git-hub": "이 대시보드는 GitHub의 풀 요청에서 로딩됩니다.",
"title-error-loading-dashboard": "대시보드 로딩 중 오류 발생",
"value-not-saved": "값이 아직 Grafana 데이터베이스에 저장되지 않았습니다.",
"view-pull-request-in-git-hub": "GitHub에서 풀 요청 보기"
"title-error-loading-dashboard": "대시보드 로딩 중 오류 발생"
},
"dashboard-scene": {
"text": {
@@ -7196,6 +7188,7 @@
"when": "다음과 같은 때"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "연산"
},
"math": {
@@ -8479,7 +8472,7 @@
"usage-count_other": "{{count}}개의 대시보드에서 사용됨"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "이름 또는 설명으로 검색"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "라이브러리 패널 로딩 중..."
@@ -8618,12 +8611,9 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_other": "",
"parsedl-label_other": "",
"structured-metadata_other": ""
}
}
},
@@ -8680,6 +8670,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8693,9 +8684,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10282,6 +10275,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "이 기능은 현재 적극적으로 개발 중입니다. 최상의 경험과 최신 개선 사항 적용을 위해 Grafana의 <2>야간 빌드</2>를 사용하는 것이 좋습니다."
@@ -11726,6 +11730,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Openbaar",
"tooltip-view-as-scene": "Weergeven als scène"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Naar het volgende dashboard",
"playlist-previous": "Naar het vorige dashboard",
"playlist-stop": "Afspeellijst stoppen",
"public-dashboard": "Openbaar",
"refresh": "Dashboard vernieuwen",
"save": "Dashboard opslaan",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Type"
},
"dashboard-preview-banner": {
"not-saved": "De waarde is nog niet opgeslagen in de Grafana-database",
"not-yet-saved": "De waarde is niet opgeslagen in de Grafana-database",
"open-pull-request-in-git-hub": "Pull-verzoek openen in GitHub",
"title-dashboard-loaded-branch-git-hub": "Dit dashboard wordt geladen vanuit een filiaal in GitHub.",
"title-dashboard-loaded-external-repository": "Dit dashboard wordt geladen vanuit een externe repository",
"title-dashboard-loaded-request-git-hub": "Dit dashboard wordt geladen vanuit een pull-verzoek in GitHub.",
"title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard",
"value-not-saved": "De waarde is nog niet opgeslagen in de Grafana-database",
"view-pull-request-in-git-hub": "Pull-verzoek bekijken in GitHub"
"title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "WANNEER"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Werking"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Gebruikt op {{count}} dashboards"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Zoeken op naam of beschrijving"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Bibliotheekpaneel laden..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Er wordt momenteel actief gewerkt aan de ontwikkeling van deze functie. Voor de beste ervaring en de nieuwste verbeteringen raden we je aan om de <2>nachtelijke versie</2> van Grafana te gebruiken."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+37 -16
View File
@@ -4435,7 +4435,6 @@
}
},
"render-left-actions": {
"text-public": "Publiczny",
"tooltip-view-as-scene": "Wyświetl jako scenę"
}
},
@@ -5211,7 +5210,6 @@
"playlist-next": "Przejdź do następnego pulpitu",
"playlist-previous": "Przejdź do poprzedniego pulpitu",
"playlist-stop": "Zatrzymaj autoodtwarzanie",
"public-dashboard": "Publiczny",
"refresh": "Odśwież pulpit",
"save": "Zapisz pulpit",
"save-dashboard": {
@@ -5512,15 +5510,9 @@
"type": "Typ"
},
"dashboard-preview-banner": {
"not-saved": "Wartość nie została jeszcze zapisana w bazie danych Grafana",
"not-yet-saved": "Wartość nie została zapisana w bazie danych Grafana",
"open-pull-request-in-git-hub": "Otwórz pull request w GitHub",
"title-dashboard-loaded-branch-git-hub": "Ten pulpit nawigacyjny jest ładowany z gałęzi w GitHub.",
"title-dashboard-loaded-external-repository": "Ten pulpit nawigacyjny jest ładowany z zewnętrznego repozytorium",
"title-dashboard-loaded-request-git-hub": "Ten pulpit nawigacyjny jest ładowany z pull request w GitHub.",
"title-error-loading-dashboard": "Błąd wczytywania pulpitu",
"value-not-saved": "Wartość nie została jeszcze zapisana w bazie danych Grafana",
"view-pull-request-in-git-hub": "Wyświetl pull request w GitHub"
"title-error-loading-dashboard": "Błąd wczytywania pulpitu"
},
"dashboard-scene": {
"text": {
@@ -7259,6 +7251,7 @@
"when": "KIEDY"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Operacja"
},
"math": {
@@ -8551,7 +8544,7 @@
"usage-count_other": "Używane na {{count}} pulpitu"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Szukaj według nazwy lub opisu"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Wczytywanie panelu biblioteki…"
@@ -8696,12 +8689,18 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_few": "",
"indexed-label_many": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_few": "",
"parsedl-label_many": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_few": "",
"structured-metadata_many": "",
"structured-metadata_other": ""
}
}
},
@@ -8758,6 +8757,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8771,9 +8771,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10369,6 +10371,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Ta funkcja jest obecnie aktywnie rozwijana. Aby uzyskać najlepsze wrażenia i najnowsze ulepszenia, zalecamy korzystanie z <2>nocnej kompilacji</2> aplikacji Grafana."
@@ -11834,6 +11847,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Pública",
"tooltip-view-as-scene": "Visualizar como cena"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Ir para o próximo painel de controle",
"playlist-previous": "Ir para o painel de controle anterior",
"playlist-stop": "Parar lista de reprodução",
"public-dashboard": "Público",
"refresh": "Atualizar painel de controle",
"save": "Salvar painel de controle",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Tipo"
},
"dashboard-preview-banner": {
"not-saved": "O valor ainda não está salvo no banco de dados da Grafana",
"not-yet-saved": "O valor não está salvo no banco de dados da Grafana",
"open-pull-request-in-git-hub": "Abrir solicitação de pull no GitHub",
"title-dashboard-loaded-branch-git-hub": "Este painel de controle é carregado a partir de um branch no GitHub.",
"title-dashboard-loaded-external-repository": "Este painel de controle é carregado a partir de um repositório externo",
"title-dashboard-loaded-request-git-hub": "Este painel de controle é carregado a partir de uma solicitação de pull no GitHub.",
"title-error-loading-dashboard": "Erro ao carregar o painel de controle",
"value-not-saved": "O valor ainda não está salvo no banco de dados da Grafana",
"view-pull-request-in-git-hub": "Ver solicitação de pull no GitHub"
"title-error-loading-dashboard": "Erro ao carregar o painel de controle"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "QUANDO"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Operação"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Usado em {{count}} painéis"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Pesquisar por nome ou descrição"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Carregando painel da biblioteca…"
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Este recurso está em desenvolvimento ativo no momento. Para ter uma melhor experiência e conferir as melhorias mais recentes, recomendamos usar a <2>compilação noturna</2> da Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Público",
"tooltip-view-as-scene": "Ver como cena"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Ir para o próximo painel de controlo",
"playlist-previous": "Ir para o painel de controlo anterior",
"playlist-stop": "Parar a lista de reprodução",
"public-dashboard": "Público",
"refresh": "Atualizar o painel de controlo",
"save": "Guardar o painel de controlo",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Tipo"
},
"dashboard-preview-banner": {
"not-saved": "O valor ainda não está guardado na base de dados da Grafana",
"not-yet-saved": "O valor não está guardado na base de dados da Grafana",
"open-pull-request-in-git-hub": "Abrir pedido pull no GitHub",
"title-dashboard-loaded-branch-git-hub": "Este painel de controlo é carregado a partir de um ramo no GitHub.",
"title-dashboard-loaded-external-repository": "Este painel de controlo é carregado a partir de um repositório externo",
"title-dashboard-loaded-request-git-hub": "Este painel de controlo é carregado a partir de um pedido pull no GitHub.",
"title-error-loading-dashboard": "Erro ao carregar o painel de controlo",
"value-not-saved": "O valor ainda não está guardado na base de dados da Grafana",
"view-pull-request-in-git-hub": "Visualizar pedido pull no GitHub"
"title-error-loading-dashboard": "Erro ao carregar o painel de controlo"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "QUANDO"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Funcionamento"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Utilizado em {{count}} painéis de controlo"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Pesquisar por nome ou descrição"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "A carregar painel de biblioteca..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Esta funcionalidade está atualmente em desenvolvimento ativo. Para a melhor experiência e as melhorias mais recentes, recomendamos a utilização da <2>compilação noturna</2> da Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+37 -16
View File
@@ -4435,7 +4435,6 @@
}
},
"render-left-actions": {
"text-public": "Общедоступный",
"tooltip-view-as-scene": "Просмотр в виде сцены"
}
},
@@ -5211,7 +5210,6 @@
"playlist-next": "Перейти к следующему дашборду",
"playlist-previous": "Перейти к предыдущему дашборду",
"playlist-stop": "Остановить плейлист",
"public-dashboard": "Общедоступный",
"refresh": "Обновить дашборд",
"save": "Сохранить дашборд",
"save-dashboard": {
@@ -5512,15 +5510,9 @@
"type": "Тип"
},
"dashboard-preview-banner": {
"not-saved": "Значение еще не сохранено в базе данных Grafana",
"not-yet-saved": "Значение не сохранено в базе данных Grafana",
"open-pull-request-in-git-hub": "Открыть запрос на включение изменений в GitHub",
"title-dashboard-loaded-branch-git-hub": "Дашборд загружается из ветви в GitHub.",
"title-dashboard-loaded-external-repository": "Дашборд загружается из внешнего репозитория",
"title-dashboard-loaded-request-git-hub": "Дашборд загружается из запроса на включение изменений в GitHub.",
"title-error-loading-dashboard": "Ошибка при загрузке дашборда",
"value-not-saved": "Значение еще не сохранено в базе данных Grafana",
"view-pull-request-in-git-hub": "Просмотр запроса на включение изменений в GitHub"
"title-error-loading-dashboard": "Ошибка при загрузке дашборда"
},
"dashboard-scene": {
"text": {
@@ -7259,6 +7251,7 @@
"when": "КОГДА"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Операция"
},
"math": {
@@ -8551,7 +8544,7 @@
"usage-count_other": "Используется на {{count}} дашборда"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Поиск по названию или описанию"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Загрузка панели библиотеки..."
@@ -8696,12 +8689,18 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_few": "",
"indexed-label_many": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_few": "",
"parsedl-label_many": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_few": "",
"structured-metadata_many": "",
"structured-metadata_other": ""
}
}
},
@@ -8758,6 +8757,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8771,9 +8771,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10369,6 +10371,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "В настоящее время эта функция находится в стадии активной разработки. Чтобы обеспечить максимальное удобство пользования и получить доступ к последним улучшениям, рекомендуем использовать <2>ночную сборку</2> Grafana."
@@ -11834,6 +11847,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Offentlig",
"tooltip-view-as-scene": "Visa som scen"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Gå till nästa instrumentpanel",
"playlist-previous": "Gå till föregående instrumentpanel",
"playlist-stop": "Stoppa spellista",
"public-dashboard": "Offentlig",
"refresh": "Uppdatera instrumentpanel",
"save": "Spara instrumentpanel",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Typ"
},
"dashboard-preview-banner": {
"not-saved": "Värdet har ännu inte sparats i Grafana-databasen",
"not-yet-saved": "Värdet sparades inte i Grafana-databasen",
"open-pull-request-in-git-hub": "Öppna pull-begäran i GitHub",
"title-dashboard-loaded-branch-git-hub": "Den här instrumentpanelen laddas från en gren i GitHub.",
"title-dashboard-loaded-external-repository": "Den här instrumentpanelen laddas från ett externt arkiv",
"title-dashboard-loaded-request-git-hub": "Den här instrumentpanelen laddas från en pull-begäran i GitHub.",
"title-error-loading-dashboard": "Fel vid laddning av instrumentpanel",
"value-not-saved": "Värdet har ännu inte sparats i Grafana-databasen",
"view-pull-request-in-git-hub": "Visa pull-begäran i GitHub"
"title-error-loading-dashboard": "Fel vid laddning av instrumentpanel"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "NÄR"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Drift"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "Används på {{count}} instrumentpaneler"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Sök efter namn eller beskrivning"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Laddar bibliotekspanel …"
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Den här funktionen är för närvarande under aktiv utveckling. För den bästa upplevelsen och de senaste förbättringarna rekommenderar vi att du använder <2> nightly-versionen</2> av Grafana."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+31 -16
View File
@@ -4399,7 +4399,6 @@
}
},
"render-left-actions": {
"text-public": "Herkese açık",
"tooltip-view-as-scene": "Sahne olarak görüntüle"
}
},
@@ -5173,7 +5172,6 @@
"playlist-next": "Sonraki panoya git",
"playlist-previous": "Önceki panoya git",
"playlist-stop": "Oynatma listesini durdur",
"public-dashboard": "Herkese açık",
"refresh": "Panoyu yenile",
"save": "Panoyu kaydet",
"save-dashboard": {
@@ -5474,15 +5472,9 @@
"type": "Tür"
},
"dashboard-preview-banner": {
"not-saved": "Değer henüz Grafana veri tabanına kaydedilmedi",
"not-yet-saved": "Değer Grafana veri tabanına kaydedilmedi",
"open-pull-request-in-git-hub": "GitHub'da çekme isteği aç",
"title-dashboard-loaded-branch-git-hub": "Bu pano, GitHub'daki bir daldan yüklendi.",
"title-dashboard-loaded-external-repository": "Bu pano, haricî bir depodan yüklendi.",
"title-dashboard-loaded-request-git-hub": "Bu pano, GitHub'daki bir çekme isteğinden yüklendi.",
"title-error-loading-dashboard": "Pano yüklenirken hata oluştu",
"value-not-saved": "Değer henüz Grafana veri tabanına kaydedilmedi",
"view-pull-request-in-git-hub": "GitHub'da çekme isteğini görüntüle"
"title-error-loading-dashboard": "Pano yüklenirken hata oluştu"
},
"dashboard-scene": {
"text": {
@@ -7217,6 +7209,7 @@
"when": "NE ZAMAN"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "Çalıştırma"
},
"math": {
@@ -8503,7 +8496,7 @@
"usage-count_other": "{{count}} panoda kullanılıyor"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "Ada veya açıklamaya göre ara"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "Kütüphane paneli yükleniyor..."
@@ -8644,12 +8637,12 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_one": "",
"indexed-label_other": "",
"parsedl-label_one": "",
"parsedl-label_other": "",
"structured-metadata_one": "",
"structured-metadata_other": ""
}
}
},
@@ -8706,6 +8699,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8719,9 +8713,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10311,6 +10307,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "Bu özellik şu anda aktif geliştirme aşamasındadır. En iyi deneyim ve en yeni geliştirmeler için Grafana'nın <2>gecelik derleme</2> sürümünü kullanmanızı öneririz."
@@ -11762,6 +11769,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+28 -16
View File
@@ -4381,7 +4381,6 @@
}
},
"render-left-actions": {
"text-public": "公开",
"tooltip-view-as-scene": "作为场景查看"
}
},
@@ -5154,7 +5153,6 @@
"playlist-next": "前往下一个仪表板",
"playlist-previous": "前往上一个仪表板",
"playlist-stop": "停止播放列表",
"public-dashboard": "公开",
"refresh": "刷新仪表板",
"save": "保存仪表板",
"save-dashboard": {
@@ -5455,15 +5453,9 @@
"type": "类型"
},
"dashboard-preview-banner": {
"not-saved": "该值尚未保存在 Grafana 数据库中",
"not-yet-saved": "该值未保存在 Grafana 数据库中",
"open-pull-request-in-git-hub": "在 GitHub 中打开拉取请求",
"title-dashboard-loaded-branch-git-hub": "此数据面板从 GitHub 的分支加载。",
"title-dashboard-loaded-external-repository": "此数据面板从外部存储库加载",
"title-dashboard-loaded-request-git-hub": "此数据面板从 GitHub 的拉取请求加载。",
"title-error-loading-dashboard": "加载数据面板时出错",
"value-not-saved": "该值尚未保存在 Grafana 数据库中",
"view-pull-request-in-git-hub": "在 GitHub 中查看拉取请求"
"title-error-loading-dashboard": "加载数据面板时出错"
},
"dashboard-scene": {
"text": {
@@ -7196,6 +7188,7 @@
"when": "满足以下条件时"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "运算"
},
"math": {
@@ -8479,7 +8472,7 @@
"usage-count_other": "用于 {{count}} 个数据面板"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "按名称或描述搜索"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "正在加载库面板..."
@@ -8618,12 +8611,9 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_other": "",
"parsedl-label_other": "",
"structured-metadata_other": ""
}
}
},
@@ -8680,6 +8670,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8693,9 +8684,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10282,6 +10275,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "此功能目前正在积极开发中。为了获得最佳体验和最新改进,我们建议您使用 Grafana 的<2>夜间版本</2>。"
@@ -11726,6 +11730,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+28 -16
View File
@@ -4381,7 +4381,6 @@
}
},
"render-left-actions": {
"text-public": "公共",
"tooltip-view-as-scene": "以場景形式檢視"
}
},
@@ -5154,7 +5153,6 @@
"playlist-next": "前往下一個儀表板",
"playlist-previous": "前往上一個儀表板",
"playlist-stop": "停止播放清單",
"public-dashboard": "公共",
"refresh": "重新整理儀表板",
"save": "儲存儀表板",
"save-dashboard": {
@@ -5455,15 +5453,9 @@
"type": "類型"
},
"dashboard-preview-banner": {
"not-saved": "該數值尚未儲存在 Grafana 資料庫中",
"not-yet-saved": "該數值未儲存在 Grafana 資料庫中",
"open-pull-request-in-git-hub": "在 GitHub 中開啟拉取請求",
"title-dashboard-loaded-branch-git-hub": "此儀表板是從 GitHub 的分支載入。",
"title-dashboard-loaded-external-repository": "此儀表板是從外部存放庫載入",
"title-dashboard-loaded-request-git-hub": "此儀表板是從 GitHub 的拉取請求載入。",
"title-error-loading-dashboard": "載入控制面板發生錯誤",
"value-not-saved": "該數值尚未儲存在 Grafana 資料庫中",
"view-pull-request-in-git-hub": "在 GitHub 中檢視拉取請求"
"title-error-loading-dashboard": "載入控制面板發生錯誤"
},
"dashboard-scene": {
"text": {
@@ -7196,6 +7188,7 @@
"when": "當"
},
"expression-query-editor": {
"helper-text-sql": "",
"label-operation": "操作"
},
"math": {
@@ -8479,7 +8472,7 @@
"usage-count_other": "用於 {{count}} 個儀表板"
},
"library-panels-search": {
"placeholder-search-by-name-or-description": "按名稱或描述搜尋"
"placeholder-search-by-name-or-description": ""
},
"loading-indicator": {
"loading-library-panel": "正在載入資料庫面板…"
@@ -8618,12 +8611,9 @@
"fields": {
"type": {
"loki": {
"indexed-label": "",
"indexed-label-plural": "",
"parsed-label-plural": "",
"parsedl-label": "",
"structured-metadata": "",
"structured-metadata-plural": ""
"indexed-label_other": "",
"parsedl-label_other": "",
"structured-metadata_other": ""
}
}
},
@@ -8680,6 +8670,7 @@
"close": "",
"copy-shortlink": "",
"copy-to-clipboard": "",
"displayed-fields-section": "",
"fields": {
"adhoc-statistics": "",
"copy-value-to-clipboard": "",
@@ -8693,9 +8684,11 @@
"fields-section": "",
"hide-log-line": "",
"links-section": "",
"log-line-field": "",
"log-line-section": "",
"no-details": "",
"pin-line": "",
"remove-displayed-field": "",
"search": {
"no-results": ""
},
@@ -10282,6 +10275,17 @@
"label-workflow": ""
}
},
"provisioned-resource-preview-banner": {
"preview-banner": {
"not-saved": "",
"open-pull-request-in-git-hub": "",
"view-pull-request-in-git-hub": ""
},
"title-dashboard-loaded-branch-git-hub": "",
"title-dashboard-loaded-pull-request-git-hub": "",
"title-folder-created-branch-git-hub": "",
"title-folder-created-pull-request-git-hub": ""
},
"provisioning": {
"banner": {
"message": "此功能目前正在積極開發中。為了獲得最佳體驗和最新改進版本,建議使用 Grafana 的<2>夜間版本</2>。"
@@ -11726,6 +11730,14 @@
"name-show-table-footer": "",
"name-show-table-header": "",
"name-wrap-header-text": "",
"pill-cell-options-editor": {
"description-color-mode": "",
"description-fixed-color": "",
"description-value-mappings-info": "",
"label-color-mode": "",
"label-fixed-color": "",
"label-value-mappings-info": ""
},
"placeholder-column-width": "",
"placeholder-fields": ""
},
+333 -322
View File
@@ -2720,7 +2720,7 @@ __metadata:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:12.1.0-pre"
"@grafana/e2e-selectors": "npm:12.1.0-pre"
"@grafana/lezer-logql": "npm:0.2.7"
"@grafana/lezer-logql": "npm:0.2.8"
"@grafana/llm": "npm:0.22.1"
"@grafana/monaco-logql": "npm:^0.0.8"
"@grafana/plugin-configs": "npm:12.1.0-pre"
@@ -3283,12 +3283,12 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/lezer-logql@npm:0.2.7":
version: 0.2.7
resolution: "@grafana/lezer-logql@npm:0.2.7"
"@grafana/lezer-logql@npm:0.2.8":
version: 0.2.8
resolution: "@grafana/lezer-logql@npm:0.2.8"
peerDependencies:
"@lezer/lr": ^1.0.0
checksum: 10/606a9dc77b3b3751e1f325d6b1a8994b1bafef7fe0f6f3980ee7d184244b373f828960f46a748746e765615352ed8928d10f22ff06cede588fcc9d32e70a68d8
checksum: 10/56b31f9479037201b07d27602c023a2c2373758f232b02a4e5c501dbfabe6f708ddd2a4f8d059b5aad576297137bc2836ed3f520eceee6ffcd3001ad53c73f5c
languageName: node
linkType: hard
@@ -3714,7 +3714,7 @@ __metadata:
"@testing-library/jest-dom": "npm:6.6.3"
"@testing-library/react": "npm:16.2.0"
"@testing-library/user-event": "npm:14.6.1"
"@types/chance": "npm:1.1.6"
"@types/chance": "npm:^1.1.7"
"@types/common-tags": "npm:^1.8.0"
"@types/d3": "npm:7.4.3"
"@types/hoist-non-react-statics": "npm:3.3.6"
@@ -3738,7 +3738,7 @@ __metadata:
"@types/tinycolor2": "npm:1.4.6"
"@types/uuid": "npm:10.0.0"
calculate-size: "npm:1.1.1"
chance: "npm:1.1.12"
chance: "npm:^1.1.13"
classnames: "npm:2.5.1"
common-tags: "npm:1.8.2"
core-js: "npm:3.40.0"
@@ -8902,10 +8902,10 @@ __metadata:
languageName: node
linkType: hard
"@types/chance@npm:1.1.6, @types/chance@npm:^1.1.3":
version: 1.1.6
resolution: "@types/chance@npm:1.1.6"
checksum: 10/f4366f1b3144d143af3e6f0fad2ed1db7b9bdfa7d82d40944e9619d57fe7e6b60e8c1452f47a8ededa6b2188932879518628ecd9aac81c40384ded39c26338ba
"@types/chance@npm:^1.1.7":
version: 1.1.7
resolution: "@types/chance@npm:1.1.7"
checksum: 10/5b3bf4ef0b7a2f6554f7767d7f081b4f613b45e74e74450d4da9c10cd12162420fc6ff5bb0abea143d6f7c43b62776b7a0e75d242cbe25c729fe020a1735e16f
languageName: node
linkType: hard
@@ -11726,21 +11726,21 @@ __metadata:
languageName: node
linkType: hard
"autoprefixer@npm:10.4.20":
version: 10.4.20
resolution: "autoprefixer@npm:10.4.20"
"autoprefixer@npm:10.4.21":
version: 10.4.21
resolution: "autoprefixer@npm:10.4.21"
dependencies:
browserslist: "npm:^4.23.3"
caniuse-lite: "npm:^1.0.30001646"
browserslist: "npm:^4.24.4"
caniuse-lite: "npm:^1.0.30001702"
fraction.js: "npm:^4.3.7"
normalize-range: "npm:^0.1.2"
picocolors: "npm:^1.0.1"
picocolors: "npm:^1.1.1"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.1.0
bin:
autoprefixer: bin/autoprefixer
checksum: 10/d3c4b562fc4af2393623a0207cc336f5b9f94c4264ae1c316376904c279702ce2b12dc3f27205f491195d1e29bb52ffc269970ceb0f271f035fadee128a273f7
checksum: 10/5d7aeee78ef362a6838e12312908516a8ac5364414175273e5cff83bbff67612755b93d567f3aa01ce318342df48aeab4b291847b5800c780e58c458f61a98a6
languageName: node
linkType: hard
@@ -12287,17 +12287,17 @@ __metadata:
languageName: node
linkType: hard
"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3":
version: 4.25.0
resolution: "browserslist@npm:4.25.0"
"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3, browserslist@npm:^4.24.4, browserslist@npm:^4.24.5":
version: 4.25.1
resolution: "browserslist@npm:4.25.1"
dependencies:
caniuse-lite: "npm:^1.0.30001718"
electron-to-chromium: "npm:^1.5.160"
caniuse-lite: "npm:^1.0.30001726"
electron-to-chromium: "npm:^1.5.173"
node-releases: "npm:^2.0.19"
update-browserslist-db: "npm:^1.1.3"
bin:
browserslist: cli.js
checksum: 10/4a5442b1a0d09c4c64454f184b8fed17d8c3e202034bf39de28f74497d7bd28dddee121b2bab4e34825fe0ed4c166d84e32a39f576c76fce73c1f8f05e4b6ee6
checksum: 10/bfb5511b425886279bbe2ea44d10e340c8aea85866c9d45083c13491d049b6362e254018c0afbf56d41ceeb64f994957ea8ae98dbba74ef1e54ef901c8732987
languageName: node
linkType: hard
@@ -12567,10 +12567,10 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001718":
version: 1.0.30001723
resolution: "caniuse-lite@npm:1.0.30001723"
checksum: 10/edab89e84a2b257cf640f0bac1f25f92c699ade86143b2affc73403468f894023416a9f4a99e5345c933956990b005a2facfb87ac4517c8ccb588819bb62453b
"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001726":
version: 1.0.30001727
resolution: "caniuse-lite@npm:1.0.30001727"
checksum: 10/6155a4141332c337d6317325bea58a09036a65f45bd9bd834ec38978b40c27d214baa04d25b21a5661664f3fbd00cb830e2bdb7eee8df09970bdd98a71f4dabf
languageName: node
linkType: hard
@@ -12677,10 +12677,10 @@ __metadata:
languageName: node
linkType: hard
"chance@npm:1.1.12, chance@npm:^1.0.10":
version: 1.1.12
resolution: "chance@npm:1.1.12"
checksum: 10/8700d5a66e27b47f4bdcf68f48489e1a490f39cd8bc8e39cac67c089b3f8d04b4bbc6710db0e85e802ad698cf3440d92784861208d2c248820355404b85b3f30
"chance@npm:^1.1.13":
version: 1.1.13
resolution: "chance@npm:1.1.13"
checksum: 10/968e31ce9b8ce554c8a84fb66d85f6d06a33517bf21355ab760e347a0aaaece0652a23c791406826d4efe619b1aa4e7b66fb42acbd07859ba36ec2498a167b8c
languageName: node
linkType: hard
@@ -12849,15 +12849,15 @@ __metadata:
languageName: node
linkType: hard
"chrome-remote-interface@npm:0.33.2":
version: 0.33.2
resolution: "chrome-remote-interface@npm:0.33.2"
"chrome-remote-interface@npm:0.33.3":
version: 0.33.3
resolution: "chrome-remote-interface@npm:0.33.3"
dependencies:
commander: "npm:2.11.x"
ws: "npm:^7.2.0"
bin:
chrome-remote-interface: bin/client.js
checksum: 10/fa82c76c5af629f5fbccb22c383604650f8679571385b8610783fde6f006b899c48e1522be551fd9670de59c1a2d93beb152a55651f24ba9c91ca0f8df3f581c
checksum: 10/65d07afc8f97fad6326bd94f0c4ef004d76b16841015c72d09aefbfa4df62b47407067a01c886535a706c36b23070b40fc3edb96444665438efa1d4f65cc3db8
languageName: node
linkType: hard
@@ -13933,14 +13933,14 @@ __metadata:
languageName: node
linkType: hard
"css-minimizer-webpack-plugin@npm:7.0.0":
version: 7.0.0
resolution: "css-minimizer-webpack-plugin@npm:7.0.0"
"css-minimizer-webpack-plugin@npm:7.0.2":
version: 7.0.2
resolution: "css-minimizer-webpack-plugin@npm:7.0.2"
dependencies:
"@jridgewell/trace-mapping": "npm:^0.3.25"
cssnano: "npm:^7.0.1"
cssnano: "npm:^7.0.4"
jest-worker: "npm:^29.7.0"
postcss: "npm:^8.4.38"
postcss: "npm:^8.4.40"
schema-utils: "npm:^4.2.0"
serialize-javascript: "npm:^6.0.2"
peerDependencies:
@@ -13958,7 +13958,7 @@ __metadata:
optional: true
lightningcss:
optional: true
checksum: 10/47d8f8a38c97496759f1676b5344231d48bfb205cc272e163a113d4cd40daddd17f8eb719d1f1071cf3ec4d62fd83537801cba598d7c653657aa97f1461e4a8b
checksum: 10/80ada5f059900a3b474e33ff0f10c0e5f5a37b7525fa7ef30812b6049bcbc28fbda1d703d8f76724a97449114306de5cd7a7af15003a16784e6c9cc374dfedc8
languageName: node
linkType: hard
@@ -14098,64 +14098,64 @@ __metadata:
languageName: node
linkType: hard
"cssnano-preset-default@npm:^7.0.6":
version: 7.0.6
resolution: "cssnano-preset-default@npm:7.0.6"
"cssnano-preset-default@npm:^7.0.7":
version: 7.0.7
resolution: "cssnano-preset-default@npm:7.0.7"
dependencies:
browserslist: "npm:^4.23.3"
browserslist: "npm:^4.24.5"
css-declaration-sorter: "npm:^7.2.0"
cssnano-utils: "npm:^5.0.0"
postcss-calc: "npm:^10.0.2"
postcss-colormin: "npm:^7.0.2"
postcss-convert-values: "npm:^7.0.4"
postcss-discard-comments: "npm:^7.0.3"
postcss-discard-duplicates: "npm:^7.0.1"
postcss-discard-empty: "npm:^7.0.0"
postcss-discard-overridden: "npm:^7.0.0"
postcss-merge-longhand: "npm:^7.0.4"
postcss-merge-rules: "npm:^7.0.4"
postcss-minify-font-values: "npm:^7.0.0"
postcss-minify-gradients: "npm:^7.0.0"
postcss-minify-params: "npm:^7.0.2"
postcss-minify-selectors: "npm:^7.0.4"
postcss-normalize-charset: "npm:^7.0.0"
postcss-normalize-display-values: "npm:^7.0.0"
postcss-normalize-positions: "npm:^7.0.0"
postcss-normalize-repeat-style: "npm:^7.0.0"
postcss-normalize-string: "npm:^7.0.0"
postcss-normalize-timing-functions: "npm:^7.0.0"
postcss-normalize-unicode: "npm:^7.0.2"
postcss-normalize-url: "npm:^7.0.0"
postcss-normalize-whitespace: "npm:^7.0.0"
postcss-ordered-values: "npm:^7.0.1"
postcss-reduce-initial: "npm:^7.0.2"
postcss-reduce-transforms: "npm:^7.0.0"
postcss-svgo: "npm:^7.0.1"
postcss-unique-selectors: "npm:^7.0.3"
cssnano-utils: "npm:^5.0.1"
postcss-calc: "npm:^10.1.1"
postcss-colormin: "npm:^7.0.3"
postcss-convert-values: "npm:^7.0.5"
postcss-discard-comments: "npm:^7.0.4"
postcss-discard-duplicates: "npm:^7.0.2"
postcss-discard-empty: "npm:^7.0.1"
postcss-discard-overridden: "npm:^7.0.1"
postcss-merge-longhand: "npm:^7.0.5"
postcss-merge-rules: "npm:^7.0.5"
postcss-minify-font-values: "npm:^7.0.1"
postcss-minify-gradients: "npm:^7.0.1"
postcss-minify-params: "npm:^7.0.3"
postcss-minify-selectors: "npm:^7.0.5"
postcss-normalize-charset: "npm:^7.0.1"
postcss-normalize-display-values: "npm:^7.0.1"
postcss-normalize-positions: "npm:^7.0.1"
postcss-normalize-repeat-style: "npm:^7.0.1"
postcss-normalize-string: "npm:^7.0.1"
postcss-normalize-timing-functions: "npm:^7.0.1"
postcss-normalize-unicode: "npm:^7.0.3"
postcss-normalize-url: "npm:^7.0.1"
postcss-normalize-whitespace: "npm:^7.0.1"
postcss-ordered-values: "npm:^7.0.2"
postcss-reduce-initial: "npm:^7.0.3"
postcss-reduce-transforms: "npm:^7.0.1"
postcss-svgo: "npm:^7.0.2"
postcss-unique-selectors: "npm:^7.0.4"
peerDependencies:
postcss: ^8.4.31
checksum: 10/686e7652d01ad4337dbad17b22fdb9cf132cf4664fddd05194da13a1f44f1177697745bbc6da73083941356280e89fe2cceacb6f422cc4522d70ff51db83cd63
postcss: ^8.4.32
checksum: 10/1ca9b739531acc2dff66347cc1b6195da0549058b5b00b9d36c3f241535ad67476218f61201cfb15e8b460357ec42414aa53bf78a7f01ee26ac26b7852e6c244
languageName: node
linkType: hard
"cssnano-utils@npm:^5.0.0":
version: 5.0.0
resolution: "cssnano-utils@npm:5.0.0"
"cssnano-utils@npm:^5.0.1":
version: 5.0.1
resolution: "cssnano-utils@npm:5.0.1"
peerDependencies:
postcss: ^8.4.31
checksum: 10/89ed5b8ca554697b4ae285e0d3e134fccc9a0471adda57c8fba17a2bace2f062b9fcf7aeaf66fbd7fabddca8a15a6b1e5ccb70a2783421ae1ac164f779d9f24e
postcss: ^8.4.32
checksum: 10/cdf37315d3cf9726e10ce842b18e148e4df1d1d18d292540e724d5a96994901abc631c8894328c39ab70c864449a8a83f8fc117114fdcbade204e5e65898af90
languageName: node
linkType: hard
"cssnano@npm:^7.0.1":
version: 7.0.6
resolution: "cssnano@npm:7.0.6"
"cssnano@npm:^7.0.4":
version: 7.0.7
resolution: "cssnano@npm:7.0.7"
dependencies:
cssnano-preset-default: "npm:^7.0.6"
lilconfig: "npm:^3.1.2"
cssnano-preset-default: "npm:^7.0.7"
lilconfig: "npm:^3.1.3"
peerDependencies:
postcss: ^8.4.31
checksum: 10/12b1e1f2b52ff2ba0ecb470e51f8fb3298d976bf91a51c7d2854793ea1e2af5d3c40385a85ad82d2117c84b9528d08f4bfecbb14949c6014d953dae34260952b
postcss: ^8.4.32
checksum: 10/c5b3123757834537f818e0f3eb6b20da51a194fefed599632f7ddd600c9e25d38abe38a22582a579660a49368a146c294e2096b2837cbeeda51ddfc85b108601
languageName: node
linkType: hard
@@ -15384,10 +15384,10 @@ __metadata:
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.5.160":
version: 1.5.167
resolution: "electron-to-chromium@npm:1.5.167"
checksum: 10/078093a38e7295e575f381943f62914f49b53dd73506af2ce3e59332835c42b487ad02ff1207dfdcb33a5886d74a98e352c04431c0537366d9999a79c7d15c94
"electron-to-chromium@npm:^1.5.173":
version: 1.5.180
resolution: "electron-to-chromium@npm:1.5.180"
checksum: 10/8d7f68650427f6bcb107ee1dcbe18f68b5c582601653095bec653fa898bd8427be4b1836581ce74405dca7cb36ebfc265a85c186e750b4de6e6f3ac4cfeef71b
languageName: node
linkType: hard
@@ -18122,7 +18122,7 @@ __metadata:
"@grafana/flamegraph": "workspace:*"
"@grafana/google-sdk": "npm:0.3.2"
"@grafana/i18n": "workspace:*"
"@grafana/lezer-logql": "npm:0.2.7"
"@grafana/lezer-logql": "npm:0.2.8"
"@grafana/llm": "npm:0.22.1"
"@grafana/monaco-logql": "npm:^0.0.8"
"@grafana/o11y-ds-frontend": "workspace:*"
@@ -18178,7 +18178,7 @@ __metadata:
"@testing-library/user-event": "npm:14.6.1"
"@types/babel__core": "npm:^7"
"@types/babel__preset-env": "npm:^7"
"@types/chance": "npm:^1.1.3"
"@types/chance": "npm:^1.1.7"
"@types/common-tags": "npm:^1.8.0"
"@types/confusing-browser-globals": "npm:^1"
"@types/d3": "npm:7.4.3"
@@ -18238,15 +18238,15 @@ __metadata:
"@visx/tooltip": "npm:3.12.0"
"@welldone-software/why-did-you-render": "npm:8.0.3"
ansicolor: "npm:2.0.3"
autoprefixer: "npm:10.4.20"
autoprefixer: "npm:10.4.21"
babel-loader: "npm:9.2.1"
baron: "npm:3.0.3"
blob-polyfill: "npm:9.0.20240710"
brace: "npm:0.11.1"
browserslist: "npm:^4.21.4"
centrifuge: "npm:5.3.5"
chance: "npm:^1.0.10"
chrome-remote-interface: "npm:0.33.2"
chance: "npm:^1.1.13"
chrome-remote-interface: "npm:0.33.3"
classnames: "npm:2.5.1"
codeowners: "npm:^5.1.1"
combokeys: "npm:^3.0.0"
@@ -18258,7 +18258,7 @@ __metadata:
crashme: "npm:0.0.15"
croner: "npm:^9.0.0"
css-loader: "npm:7.1.2"
css-minimizer-webpack-plugin: "npm:7.0.0"
css-minimizer-webpack-plugin: "npm:7.0.2"
cypress: "npm:14.3.2"
cypress-file-upload: "npm:5.0.8"
cypress-recurse: "npm:^1.35.3"
@@ -21787,7 +21787,7 @@ __metadata:
languageName: node
linkType: hard
"lilconfig@npm:^3.1.2, lilconfig@npm:^3.1.3":
"lilconfig@npm:^3.1.3":
version: 3.1.3
resolution: "lilconfig@npm:3.1.3"
checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3
@@ -23295,12 +23295,12 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^3.3.8":
version: 3.3.8
resolution: "nanoid@npm:3.3.8"
"nanoid@npm:^3.3.11, nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
bin:
nanoid: bin/nanoid.cjs
checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40
checksum: 10/73b5afe5975a307aaa3c95dfe3334c52cdf9ae71518176895229b8d65ab0d1c0417dd081426134eb7571c055720428ea5d57c645138161e7d10df80815527c48
languageName: node
linkType: hard
@@ -25105,7 +25105,7 @@ __metadata:
languageName: node
linkType: hard
"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.1":
"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045
@@ -25303,79 +25303,79 @@ __metadata:
languageName: node
linkType: hard
"postcss-calc@npm:^10.0.2":
version: 10.0.2
resolution: "postcss-calc@npm:10.0.2"
"postcss-calc@npm:^10.1.1":
version: 10.1.1
resolution: "postcss-calc@npm:10.1.1"
dependencies:
postcss-selector-parser: "npm:^6.1.2"
postcss-selector-parser: "npm:^7.0.0"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.38
checksum: 10/12d497e632b4a12f7d33507ed6f74db2dd01f9b9cc1f9986271af16b118d25f959dc255777a91d742e0431f400a90b8540d00533fc0513f34c1840a491cf2bee
checksum: 10/16a25ec594cfbbda439fd2939820f78ed4e7b8b5ab458aed7283b05fffabe68e1d4e1f4821fac798095f10539371676cd690bd27927adefab1911ff69b33d62c
languageName: node
linkType: hard
"postcss-colormin@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-colormin@npm:7.0.2"
"postcss-colormin@npm:^7.0.3":
version: 7.0.3
resolution: "postcss-colormin@npm:7.0.3"
dependencies:
browserslist: "npm:^4.23.3"
browserslist: "npm:^4.24.5"
caniuse-api: "npm:^3.0.0"
colord: "npm:^2.9.3"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/cb83d95d21668c770e5268f50ec6f8cd5d991d65123bafd3aa4a697580609c62d0078e704c4b7820db57638bf386084b253885b1e86263f580e8a393a687e973
postcss: ^8.4.32
checksum: 10/b9016d205eaf61a25efb187264a2ce35cb59aa1734b946268abcd747b5796e0d855c081b460ead4042a17c6806e011b57ee543b9e1f6312620f8daf661a7e40c
languageName: node
linkType: hard
"postcss-convert-values@npm:^7.0.4":
version: 7.0.4
resolution: "postcss-convert-values@npm:7.0.4"
"postcss-convert-values@npm:^7.0.5":
version: 7.0.5
resolution: "postcss-convert-values@npm:7.0.5"
dependencies:
browserslist: "npm:^4.23.3"
browserslist: "npm:^4.24.5"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/077481cc98514965acf335cdacae4f604be86f4153ed3bcfdd2c4c54058182d0b472f859931d55d9aeb01600f08fff6a88a21539adbb6169019fda8b22f064ef
postcss: ^8.4.32
checksum: 10/67920f9ba823a6f6aa3b46c3a098c2d4a7a2a32349971cfa6ce986e08e7cbae6badeb23de680d36d1439e7d3f2cdbf26f5ee080a66f2823931c1d3f8146bc2a6
languageName: node
linkType: hard
"postcss-discard-comments@npm:^7.0.3":
version: 7.0.3
resolution: "postcss-discard-comments@npm:7.0.3"
"postcss-discard-comments@npm:^7.0.4":
version: 7.0.4
resolution: "postcss-discard-comments@npm:7.0.4"
dependencies:
postcss-selector-parser: "npm:^6.1.2"
postcss-selector-parser: "npm:^7.1.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/f7c994df0d2de75d876f0db7ebd5b63718ef7ee5336a35f5f753f8ea115ecf8be26d5d2ad8800e833f18b33da6e018af82de7b5f0aa69e6338d3e0aff46348d4
postcss: ^8.4.32
checksum: 10/a09ac248bfbd6f2baa72b84873a876f4113df0fb5e9dd10808f6bbb310473fcd7905cc4639dbfd3ad8a5444053d42f7bb644a6934e95305820bdedc731d3c80a
languageName: node
linkType: hard
"postcss-discard-duplicates@npm:^7.0.1":
"postcss-discard-duplicates@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-discard-duplicates@npm:7.0.2"
peerDependencies:
postcss: ^8.4.32
checksum: 10/2da841b5c0117528e56e1ccda28924339c03fdb93dab61b767cebb9a9e4a2a077498d00e0c97c9ec36a534f98d6f358e6236f30913c184f90d51f6d302f4f0f6
languageName: node
linkType: hard
"postcss-discard-empty@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-discard-duplicates@npm:7.0.1"
resolution: "postcss-discard-empty@npm:7.0.1"
peerDependencies:
postcss: ^8.4.31
checksum: 10/0c757bb542caf017740157a2e29186ae83085bb42cd8e5ea3649fa039cc3d505ccaca739b1aed6c89e1f0a7f18440f77c3f49e4b99f45efd767c863d6647af94
postcss: ^8.4.32
checksum: 10/39977000657e78202da891ae6300593e40e1c8a756f1d9707087390e47a410739c394c35e902130556efb5808e6701b3b34b89facf7a9e56533d617dd9597049
languageName: node
linkType: hard
"postcss-discard-empty@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-discard-empty@npm:7.0.0"
"postcss-discard-overridden@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-discard-overridden@npm:7.0.1"
peerDependencies:
postcss: ^8.4.31
checksum: 10/0c5cea198057727765855dbb43b5f16bd4d7da8c783fea8d18ad445ad3457681a7bc1696fda6bf16313e6fadaf86d519470aff68f02378b8b413e60023b70d57
languageName: node
linkType: hard
"postcss-discard-overridden@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-discard-overridden@npm:7.0.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/e41c448305f96a93ec97a4a8ce2932a123283898041ff38ed2f7a35fcb76d937f448c2c8efb7d74d53d38b4ebf9163ae12935297bb99baec2f6751776b0ea29b
postcss: ^8.4.32
checksum: 10/a0e67314b696591396e6bb371cdd57537e06f63e9fa0d742fe678decf600bed0cdcfa481487bce91b3732bdd7c46338f9102ccc8180c41032811e99962883715
languageName: node
linkType: hard
@@ -25406,78 +25406,78 @@ __metadata:
languageName: node
linkType: hard
"postcss-merge-longhand@npm:^7.0.4":
version: 7.0.4
resolution: "postcss-merge-longhand@npm:7.0.4"
"postcss-merge-longhand@npm:^7.0.5":
version: 7.0.5
resolution: "postcss-merge-longhand@npm:7.0.5"
dependencies:
postcss-value-parser: "npm:^4.2.0"
stylehacks: "npm:^7.0.4"
stylehacks: "npm:^7.0.5"
peerDependencies:
postcss: ^8.4.31
checksum: 10/b94b98a9b21bc8671aa0fba96491e8e2deea57c9bbfe9a74305400a36035b63764d8bfbdc6bda047887b665b92b91361a8bbc1cb9c14f80b3792feef19881005
postcss: ^8.4.32
checksum: 10/3378fc3a196082dfdb9acff94efbfa0de95ed86bf87f485285e775fd3c21218e5a243e363ad80b96237edb454776f7c1deea28c37afb8b96ddfaf5cfe8bd606b
languageName: node
linkType: hard
"postcss-merge-rules@npm:^7.0.4":
version: 7.0.4
resolution: "postcss-merge-rules@npm:7.0.4"
"postcss-merge-rules@npm:^7.0.5":
version: 7.0.5
resolution: "postcss-merge-rules@npm:7.0.5"
dependencies:
browserslist: "npm:^4.23.3"
browserslist: "npm:^4.24.5"
caniuse-api: "npm:^3.0.0"
cssnano-utils: "npm:^5.0.0"
postcss-selector-parser: "npm:^6.1.2"
cssnano-utils: "npm:^5.0.1"
postcss-selector-parser: "npm:^7.1.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/f67a4f6e814c5e7ce990a3c3d699e1c1dba7e79c5cb3a11795534d47b0fa257d27465e248546b45104d8278dfcbd07d9fbceb8046fcdfac86fe6340ca3c85f9a
postcss: ^8.4.32
checksum: 10/fa490791ea5e907e4498701593252ce33df468a821e5f3acf5f126f73c8262189c13ca7a0c1645ae3d66a46a03cf930048e10d808182a3e9bec78af30a02893a
languageName: node
linkType: hard
"postcss-minify-font-values@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-minify-font-values@npm:7.0.0"
"postcss-minify-font-values@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-minify-font-values@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/8578c1d1d4d65ca34db5ac0cccc7b73500040e52a3abb8abc7e5b6e47e5f72c88bfe5f3b19847556a2a68082245009d693a7c098b8bc58e7f9640abba4e80194
postcss: ^8.4.32
checksum: 10/6578a1fd293e202e738ce38d91d71c08ba970f4a998edff48022cb21ec23ef26bf7d284ddb41d6e51bf20b5b5676fe142de1bd092a76d2ef982d5ee1d6b00190
languageName: node
linkType: hard
"postcss-minify-gradients@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-minify-gradients@npm:7.0.0"
"postcss-minify-gradients@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-minify-gradients@npm:7.0.1"
dependencies:
colord: "npm:^2.9.3"
cssnano-utils: "npm:^5.0.0"
cssnano-utils: "npm:^5.0.1"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/9649e255ad954e67e0d7c2111b0f1681a93e8cba7179a547491eacf135d64596dfee9774b589d7a46ee3ace673a026113e56e734d6ab19297367f11dd3104c0e
postcss: ^8.4.32
checksum: 10/4aa782331c5d1826e549b3940eefb54e2d51f5c5a2c5f5537384bfe6eac45bfe7ba4535c03cd1642d8a27ab088f56c3682b55f5dd2c3f7969b715692e0c1102b
languageName: node
linkType: hard
"postcss-minify-params@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-minify-params@npm:7.0.2"
"postcss-minify-params@npm:^7.0.3":
version: 7.0.3
resolution: "postcss-minify-params@npm:7.0.3"
dependencies:
browserslist: "npm:^4.23.3"
cssnano-utils: "npm:^5.0.0"
browserslist: "npm:^4.24.5"
cssnano-utils: "npm:^5.0.1"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/26b6ce4db3cdefcceb7a00b64dfbd27dee4194b55708937dddd5c4000c1f02013dc0659e62e799dc1ce1f1a697961cec55a2a746a4f59d54ccae4b68adf41768
postcss: ^8.4.32
checksum: 10/97de22d6ba0310685d33b530dbfeefa930f7ac48effe623fc8a4a59d2b98bed221d0d2edad4f2e1f4590322240d0e1e94bdb162069c40b5d7ae00c58637c90c9
languageName: node
linkType: hard
"postcss-minify-selectors@npm:^7.0.4":
version: 7.0.4
resolution: "postcss-minify-selectors@npm:7.0.4"
"postcss-minify-selectors@npm:^7.0.5":
version: 7.0.5
resolution: "postcss-minify-selectors@npm:7.0.5"
dependencies:
cssesc: "npm:^3.0.0"
postcss-selector-parser: "npm:^6.1.2"
postcss-selector-parser: "npm:^7.1.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/54c74dcb098819417e95ec2b5ecdd33a2c6fdccea2346e110037c762d37644e11f83d67e6b0c93405f2b7cc28880ca0a07ad4d6618330436f7b8b84d719b85fb
postcss: ^8.4.32
checksum: 10/12580d9a17c146c9e9bb604b4887085d897554317590cee91e0f28e2a4757c18e09299365a44eae25e848e65d53b845928dfa56a9d0199d0e159d525732fbf89
languageName: node
linkType: hard
@@ -25525,136 +25525,136 @@ __metadata:
languageName: node
linkType: hard
"postcss-normalize-charset@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-charset@npm:7.0.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/a41043fb81a1d5b3b05e8b317de7fe123854a4535f9ce2904a16196a32b3565d2fd6ac59a9842e337cf1bb298dcc108cbdbc6a5d4a500aec3520d759e951a8de
languageName: node
linkType: hard
"postcss-normalize-display-values@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-display-values@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/55bbfb4dac3bf9bcc2aed30057c0bc968927b5337b372ee2dd825d6ec626c18d1481b0e8dd928d4cab70c3e8a2e6708d6115b14bebd34fe4462eb15aacff35f4
languageName: node
linkType: hard
"postcss-normalize-positions@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-positions@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/a6b982e567ddf1ad4120aaf898056f2fdbe5f6cae1d475fef22cb1f025c9bfe37df5511a4353b9f13d01feae8b1d9638c1deb70537058312262647052d004f64
languageName: node
linkType: hard
"postcss-normalize-repeat-style@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-repeat-style@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/f8ef8cf5ac6232f1d0615a97f21ea464a6930484b58421c87e0f9e626b1bb52916592f25e4f9874f424b1529807b170d8805d45878aa8293ea0608dd753230c8
languageName: node
linkType: hard
"postcss-normalize-string@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-string@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/23ea7dd7b28880dfafd0880ab782d65186ab94a4cf789b8723f9666020c7f7c8b97546e0dc46d08da3f71a873bb6db41cd69a4cafb4fde4a85f97ef83ee38bae
languageName: node
linkType: hard
"postcss-normalize-timing-functions@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-timing-functions@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/f85870b3c8132b530fb8e5c8474f1eea1d0ef69a374d5867d0300f7501803bffa55f7fad34f662d88a747ce73d552ec0f818722d2d5157cf8e5dc45a98fa552b
languageName: node
linkType: hard
"postcss-normalize-unicode@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-normalize-unicode@npm:7.0.2"
dependencies:
browserslist: "npm:^4.23.3"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/cb342f7507f28c8e9c500a2d6369c6b04a85f6c6f93aaa1ab6768d0e097453480834d3f7c5fad503f9fb9e178d9011df50ceaeebe2ac68d5daaa7c8a63ad3b3f
languageName: node
linkType: hard
"postcss-normalize-url@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-url@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/c5edca0646a13d76c5347fffaaa828184e035486d7eeb2a8b31781d30de6a90f7ad3f0cffe59e8fd4c31f1525fdb85b45777745685603ac533a151c42691f601
languageName: node
linkType: hard
"postcss-normalize-whitespace@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-normalize-whitespace@npm:7.0.0"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/c409362e3256ed66629fc48c63e834c9bfb598ca20587adb620bbc04fdccef4cd0d08b1f485eb8290d6a30e8dd836fecb0def38c3a49fe8503e2579e60f5bccf
languageName: node
linkType: hard
"postcss-ordered-values@npm:^7.0.1":
"postcss-normalize-charset@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-ordered-values@npm:7.0.1"
dependencies:
cssnano-utils: "npm:^5.0.0"
postcss-value-parser: "npm:^4.2.0"
resolution: "postcss-normalize-charset@npm:7.0.1"
peerDependencies:
postcss: ^8.4.31
checksum: 10/048082c09eee021d97def02eb8fc03fb0414402b1f6925af29a862f537b66b43d7a8e8d94c552ca67cd6172230873260f4ad44f1d5bac81c553afb054d80e6a8
postcss: ^8.4.32
checksum: 10/bcec822491e3421b009c688473433164b5c80bbef48af4e47f704bee68f0b7ba2009aaf46788e698dd233d5f4e1cf444a4f59a901623c73f8458c2227b15db57
languageName: node
linkType: hard
"postcss-reduce-initial@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-reduce-initial@npm:7.0.2"
"postcss-normalize-display-values@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-display-values@npm:7.0.1"
dependencies:
browserslist: "npm:^4.23.3"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/53f341c17a5487639e6f7c917ad695e059bf4aff66b3c971e008163f774337444753310def9f38dd26066ea96b136422592fc74077c38c40b3bfdfaa338d5b58
languageName: node
linkType: hard
"postcss-normalize-positions@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-positions@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/72b23ab87c97c155d2ec475fba8a8b968f7c7b42d055a79b267449d570c328d5ea4cb0002428cf26e9daa70c58655e0b931d2a5801cc407554d3f03a21ac041b
languageName: node
linkType: hard
"postcss-normalize-repeat-style@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-repeat-style@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/db677bceec8c00a1860b64932b99af937e7674b3e5c5ac333c95efb090e9abd747eca4ad51855f0fe73fbe544c3d21e58d06b39e03fd525945309743e31ec235
languageName: node
linkType: hard
"postcss-normalize-string@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-string@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/48df2eaca6f5365af31ad46fd60a32dc7b714cc5ec8ba80980e65855ddc47c03ac82077ce7ca04c90898f73d173410d1d6a104754ff487e7e5a59e3eae8325b3
languageName: node
linkType: hard
"postcss-normalize-timing-functions@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-timing-functions@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/31fb88489244334295918fa7d6af2d76c310a83abd20be0a7f1c408c54ac0c0f81b0ae7877698bf66de1f76495766e159c8871387407dfcafa0cb1a53f5f0460
languageName: node
linkType: hard
"postcss-normalize-unicode@npm:^7.0.3":
version: 7.0.3
resolution: "postcss-normalize-unicode@npm:7.0.3"
dependencies:
browserslist: "npm:^4.24.5"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/fc10205655f77d6467da811fbd26aa607c519cbf162ae2ba40821cf64227233445490881119c820c6988c0943cb2f4dc755abe94cb30637001ca35cce5d07b61
languageName: node
linkType: hard
"postcss-normalize-url@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-url@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/975dd0d1b55b637d45756ec57e554b2134f77368dd3ae09be9fa6636f2f41e72422505409d7fca75c635b9b1b8ec8ec2607d84c6c85497bbfd4e7748a2992882
languageName: node
linkType: hard
"postcss-normalize-whitespace@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-normalize-whitespace@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/05a0fa74f4c8e93243053b9cc865cbddddb309b2ccb08271ca9c38ea7ece2ff43d5faa12cce87f06e40cbcf22c94443c9fa2b74ed0c6b94d72a9e67ea0381626
languageName: node
linkType: hard
"postcss-ordered-values@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-ordered-values@npm:7.0.2"
dependencies:
cssnano-utils: "npm:^5.0.1"
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.32
checksum: 10/be8fb13639fb0e1ffd7d4e9bb4824d3a283c8a63a8b0dd1a654435fff1e019007c79be877940bb101bb9ebd8ba3ac18bcffd144e939890bedeb40044dcc2b9cc
languageName: node
linkType: hard
"postcss-reduce-initial@npm:^7.0.3":
version: 7.0.3
resolution: "postcss-reduce-initial@npm:7.0.3"
dependencies:
browserslist: "npm:^4.24.5"
caniuse-api: "npm:^3.0.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/5a8260cbf7fa6ea12908debe23e191bb45109b29048d15e63c60df42c4ed62c860273ce9b37172d5f31c4bdb965e984962e4e6f506939a1fc49202dd7bf520c5
postcss: ^8.4.32
checksum: 10/8fd9ff4b49a2f7e1b7c51b7da637578e32a178363e3e932c80565241454dca306658dacd390ad3d73647d55dace8be8fe29278668afa32fd9d872ee7026bdbf7
languageName: node
linkType: hard
"postcss-reduce-transforms@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-reduce-transforms@npm:7.0.0"
"postcss-reduce-transforms@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-reduce-transforms@npm:7.0.1"
dependencies:
postcss-value-parser: "npm:^4.2.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/1c369a1be820a80e8bf06376476190fe2ae5a0b5a7459257d7d9b5bc0c9aed79f46026e8558fca088f7a814e632c678f67749b246901a3839f2d50b7b9ec2d41
postcss: ^8.4.32
checksum: 10/a22d07559859b9d4313d579104a25aa254695bc37dec5134de1064d1bd52b9d1f33f050fbf330170ef1105ede9aad7741bbcf9cad2221a6a5c8d529fd3cf0259
languageName: node
linkType: hard
@@ -25705,36 +25705,36 @@ __metadata:
languageName: node
linkType: hard
"postcss-selector-parser@npm:^7.0.0":
version: 7.0.0
resolution: "postcss-selector-parser@npm:7.0.0"
"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0":
version: 7.1.0
resolution: "postcss-selector-parser@npm:7.1.0"
dependencies:
cssesc: "npm:^3.0.0"
util-deprecate: "npm:^1.0.2"
checksum: 10/0e92be7281e2b440a8be8cf207de40a24ca7bc765577916499614d5a47827a3e658206728cc559db96803e554270516104aad919a04f91bfa8914ccef1ba14ca
checksum: 10/2caf09e66e2be81d45538f8afdc5439298c89bea71e9943b364e69dce9443d9c5ab33f4dd8b237f1ed7d2f38530338dcc189c1219d888159e6afb5b0afe58b19
languageName: node
linkType: hard
"postcss-svgo@npm:^7.0.1":
version: 7.0.1
resolution: "postcss-svgo@npm:7.0.1"
"postcss-svgo@npm:^7.0.2":
version: 7.0.2
resolution: "postcss-svgo@npm:7.0.2"
dependencies:
postcss-value-parser: "npm:^4.2.0"
svgo: "npm:^3.3.2"
peerDependencies:
postcss: ^8.4.31
checksum: 10/4196d9b7ec37ea7c427b6d3d40fa75bdae6d1fdf5a814481202138fb9b074ecc1e442b8e0202aa8c76eaaff747e2f6bfec968cfe7bc774d8a58faf8bd945ff4e
postcss: ^8.4.32
checksum: 10/8615877dffbac2bb2b971fb0e8c882ebff479c2529a0fc20937d09623fcaf35a2d934c4046188bae2534729aba1de5a1ba227630aaf96a800b6f2acdbfbf1d32
languageName: node
linkType: hard
"postcss-unique-selectors@npm:^7.0.3":
version: 7.0.3
resolution: "postcss-unique-selectors@npm:7.0.3"
"postcss-unique-selectors@npm:^7.0.4":
version: 7.0.4
resolution: "postcss-unique-selectors@npm:7.0.4"
dependencies:
postcss-selector-parser: "npm:^6.1.2"
postcss-selector-parser: "npm:^7.1.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/c38ca6b5f539cae1e0e8ef0efa338f91e4e054dbd9c619e26708d787e94ce788739bbe782103f2cf35c38819233897901038292255a1726905bd04433ac9e5f2
postcss: ^8.4.32
checksum: 10/b880f96fdb20037b16ae21b48f5240a4cf8585bf3133c7894dd869711b14f3a1a82bbdecd36adc78f8c34553a46fc2199ed3e92d5031b0267ff6f43894fc00f7
languageName: node
linkType: hard
@@ -25745,7 +25745,7 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.1":
"postcss@npm:8.5.1":
version: 8.5.1
resolution: "postcss@npm:8.5.1"
dependencies:
@@ -25756,6 +25756,17 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:^8.4.33, postcss@npm:^8.4.40, postcss@npm:^8.5.1":
version: 8.5.6
resolution: "postcss@npm:8.5.6"
dependencies:
nanoid: "npm:^3.3.11"
picocolors: "npm:^1.1.1"
source-map-js: "npm:^1.2.1"
checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86
languageName: node
linkType: hard
"prefix-style@npm:2.0.1":
version: 2.0.1
resolution: "prefix-style@npm:2.0.1"
@@ -30048,15 +30059,15 @@ __metadata:
languageName: node
linkType: hard
"stylehacks@npm:^7.0.4":
version: 7.0.4
resolution: "stylehacks@npm:7.0.4"
"stylehacks@npm:^7.0.5":
version: 7.0.5
resolution: "stylehacks@npm:7.0.5"
dependencies:
browserslist: "npm:^4.23.3"
postcss-selector-parser: "npm:^6.1.2"
browserslist: "npm:^4.24.5"
postcss-selector-parser: "npm:^7.1.0"
peerDependencies:
postcss: ^8.4.31
checksum: 10/fc9d6b1e0b996d139a77f391df6db49ee1ab7e8fdeb32a8fa6b4c11512e72eb072470c32080171e46ebe123c9c96d763b9e4421b09c9c428985077940b6ba085
postcss: ^8.4.32
checksum: 10/798ac0f92ff4489c251550d64b903f1aa8b5946e5b09b33ebf68290b5a345257cecf98c989526a5d462b560081194fead38c4f804ec016ceb8b1b3f17ec74fc5
languageName: node
linkType: hard