Merge remote-tracking branch 'origin/main' into resource-store

This commit is contained in:
Ryan McKinley 2024-06-21 17:38:20 +03:00
commit 37edc450a7
77 changed files with 1533 additions and 1829 deletions

View File

@ -143,6 +143,14 @@ exports[`better eslint`] = {
"packages/grafana-data/src/panel/registryFactories.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-data/src/table/amendTimeSeries.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"packages/grafana-data/src/themes/colorManipulator.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -2838,6 +2846,10 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
],
"public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
@ -4607,14 +4619,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
],
"public/app/features/live/data/amendTimeSeries.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"public/app/features/logs/components/InfiniteScroll.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],

4
.github/CODEOWNERS vendored
View File

@ -409,12 +409,12 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/geo/ @grafana/dataviz-squad
/public/app/features/visualization/data-hover/ @grafana/dataviz-squad
/public/app/features/commandPalette/ @grafana/grafana-frontend-platform
/public/app/features/connections/ @grafana/plugins-platform-frontend @mikkancso
/public/app/features/connections/ @grafana/plugins-platform-frontend
/public/app/features/correlations/ @grafana/explore-squad
/public/app/features/dashboard/ @grafana/dashboards-squad
/public/app/features/dashboard/components/TransformationsEditor/ @grafana/dataviz-squad
/public/app/features/dashboard-scene/ @grafana/dashboards-squad
/public/app/features/datasources/ @grafana/plugins-platform-frontend @mikkancso
/public/app/features/datasources/ @grafana/plugins-platform-frontend
/public/app/features/dimensions/ @grafana/dataviz-squad
/public/app/features/dataframe-import/ @grafana/dataviz-squad
/public/app/features/explore/ @grafana/explore-squad

View File

@ -22,14 +22,40 @@ Grafana uses the [i18next](https://www.i18next.com/) framework for managing tran
```jsx
import { Trans } from 'app/core/internationalization';
const SearchTitle = ({ term }) => <Trans i18nKey="search-page.results-title">Results for {{ term }}</Trans>;
```
Prefer using `<Trans />` for JSX children, and `t()` for props and other JavaScript usage.
There may be cases where you need to interpolate variables inside other components in the translation.
If the nested component is displaying the variable only (e.g. to add emphasis or color), the best solution is to create a new wrapping component:
```jsx
import { Trans } from 'app/core/internationalization';
import { Text } from '@grafana/ui';
const SearchTerm = ({ term }) => <Text color="success">{term}</Text>;
const SearchTitle = ({ term }) => (
<Trans i18nKey="search-page.results-title">
Results for <em>{{ term }}</em>
Results for <SearchTerm term={term} />
</Trans>
);
```
Prefer using `<Trans />` for JSX children, and `t()` for props and other JavaScript usage.
However there are also cases where the nested component might be displaying additional text which also needs to be translated. In this case, you can use the `values` prop to explicitly pass variables to the translation, and reference them as templated strings in the markup. For example:
```jsx
import { Trans } from 'app/core/internationalization';
import { Text } from '@grafana/ui';
const SearchTitle = ({ term }) => (
<Trans i18nKey="search-page.results-title" values={{ myVariable: term }}>
Results for <Text color="success">{'{{ myVariable }}'} and this translated text is also in green</Text>
</Trans>
);
```
When translating in `grafana-ui`, use a relative path to import `<Trans />` and `t()` from `src/utils/i18n`.

View File

@ -33,81 +33,84 @@ The following tables list permissions associated with basic and fixed roles.
## Fixed role definitions
| Fixed role | Permissions | Description |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fixed:alerting.instances:writer` | All permissions from `fixed:alerting.instances:reader` and<br> `alert.instances:create`<br>`alert.instances:write` for organization scope <br> `alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) |
| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope <br> `alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) |
| `fixed:alerting.notifications:writer` | All permissions from `fixed:alerting.notifications:reader` and<br>`alert.notifications:write`for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) |
| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) |
| `fixed:alerting.rules:writer` | All permissions from `fixed:alerting.rules:reader` and <br> `alert.rule:create` <br> `alert.rule:write` <br> `alert.rule:delete` <br> `alert.silences:create` <br> `alert.silences:write` for scope `folders:*` <br> `alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and manage rule-specific silences |
| `fixed:alerting.rules:reader` | `alert.rule:read`, `alert.silences:read` for scope `folders:*` <br> `alert.rules.external:read` for scope `datasources:*` <br> `alert.notifications.time-intervals:read` <br> `alert.notifications.receivers:list` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and read rule-specific silences |
| `fixed:alerting:writer` | All permissions from `fixed:alerting.rules:writer` <br>`fixed:alerting.instances:writer`<br>`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) |
| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader` <br>`fixed:alerting.instances:reader`<br>`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) |
| `fixed:alerting.provisioning.secrets:reader` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) |
| `fixed:alerting.provisioning:writer` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) |
| `fixed:alerting.provisioning.status:writer` | `alert.provisioning.provenance:write` | Set provenance status to alert rules, notification policies, contact points, etc. Should be used together with regular writer roles. [\*](#alerting-roles) |
| `fixed:annotations.dashboard:writer` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. |
| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. |
| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader` <br>`annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. |
| `fixed:apikeys:reader` | `apikeys:read` for scope `apikeys:*` | Read all api keys. |
| `fixed:apikeys:writer` | All permissions from `fixed:apikeys:reader` and <br> `apikeys:create` <br> `apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. |
| `fixed:authentication.config:writer` | `settings:read` for scope `settings:auth.saml:*` <br> `settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. |
| `fixed:dashboards:creator` | `dashboards:create`<br>`folders:read` | Create dashboards. |
| `fixed:dashboards.insights:reader` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. |
| `fixed:dashboards.permissions:reader` | `dashboards.permissions:read` | Read all dashboard permissions. |
| `fixed:dashboards.permissions:writer` | All permissions from `fixed:dashboards.permissions:reader` and <br>`dashboards.permissions:write` | Read and update all dashboard permissions. |
| `fixed:dashboards.public:writer` | `dashboards.public:write` | Create, update, delete or pause a public dashboard. |
| `fixed:dashboards:reader` | `dashboards:read` | Read all dashboards. |
| `fixed:dashboards:writer` | All permissions from `fixed:dashboards:reader` and <br>`dashboards:write`<br>`dashboards:edit`<br>`dashboards:delete`<br>`dashboards:create`<br>`dashboards.permissions:read`<br>`dashboards.permissions:write` | Read, create, update, and delete all dashboards. |
| `fixed:datasources.caching:reader` | `datasources.caching:read` | Read data source query caching settings. |
| `fixed:datasources.caching:writer` | `datasources.caching:read`<br>`datasources.caching:write` | Enable, disable, or update query caching settings. |
| `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. |
| `fixed:datasources.id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. |
| `fixed:datasources.insights:reader` | `datasources.insights:read` | Read data source insights data. |
| `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. |
| `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and <br>`datasources.permissions:write` | Create, read, or delete permissions of a data source. |
| `fixed:datasources:creator` | `datasources:create` | Create data sources. |
| `fixed:datasources:reader` | `datasources:read`<br>`datasources:query` | Read and query data sources. |
| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and <br>`datasources:create`<br>`datasources:write`<br>`datasources:delete` | Read, query, create, delete, or update a data source. |
| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. |
| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and <br>`folders.permissions:write` | Read and update all folder permissions. |
| `fixed:folders:creator` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. |
| `fixed:folders:reader` | `folders:read`<br>`dashboards:read` | Read all folders and dashboards. |
| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and <br>`folders:read`<br>`folders:write`<br>`folders:create`<br>`folders:delete`<br>`folders.permissions:read`<br>`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. |
| `fixed:ldap:reader` | `ldap.user:read`<br>`ldap.status:read` | Read the LDAP configuration and LDAP status information. |
| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and <br>`ldap.user:sync`<br>`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. |
| `fixed:library.panels:creator` | `library.panels:create`<br>`folders:read` | Create library panel at the root level. |
| `fixed:library.panels:reader` | `library.panels:read` | Read all library panels. |
| `fixed:library.panels:general.reader` | `library.panels:read` | Read all library panels at the root level. |
| `fixed:library.panels:writer` | All permissions from `fixed:library.panels:reader` plus<br>`library.panels:create`<br>`library.panels:delete`<br>`library.panels:write` | Create, read, write or delete all library panels and their permissions. |
| `fixed:library.panels:general.writer` | All permissions from `fixed:library.panels:general.reader` plus<br>`library.panels:create`<br>`library.panels:delete`<br>`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. |
| `fixed:licensing:reader` | `licensing:read`<br>`licensing.reports:read` | Read licensing information and licensing reports. |
| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and <br>`licensing:write`<br>`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. |
| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. |
| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and <br>`org.users:add`<br>`org.users:remove`<br>`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. |
| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs:create`<br>`orgs:delete`<br>`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. |
| `fixed:organization:reader` | `orgs:read`<br>`orgs.quotas:read` | Read an organization and its quotas. |
| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs.preferences:read`<br>`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. |
| `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). |
| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. |
| `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. |
| `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. |
| `fixed:reports:reader` | `reports:read`<br>`reports:send`<br>`reports.settings:read` | Read all reports and shared report settings. |
| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and <br>`reports:create`<br>`reports:write`<br>`reports:delete`<br>`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. |
| `fixed:roles:reader` | `roles:read`<br>`teams.roles:read`<br>`users.roles:read`<br>`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. |
| `fixed:roles:writer` | All permissions from `fixed:roles:reader` and <br>`roles:write`<br>`roles:delete`<br>`teams.roles:add`<br>`teams.roles:remove`<br>`users.roles:add`<br>`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. |
| `fixed:roles:resetter` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. |
| `fixed:serviceaccounts:reader` | `serviceaccounts:read` | Read Grafana service accounts. |
| `fixed:serviceaccounts:creator` | `serviceaccounts:create` | Create Grafana service accounts. |
| `fixed:serviceaccounts:writer` | `serviceaccounts:read`<br>`serviceaccounts:create`<br>`serviceaccounts:write`<br>`serviceaccounts:delete`<br>`serviceaccounts.permissions:read`<br>`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. |
| `fixed:settings:reader` | `settings:read` | Read Grafana instance settings. |
| `fixed:settings:writer` | All permissions from `fixed:settings:reader` and<br>`settings:write` | Read and update Grafana instance settings. |
| `fixed:stats:reader` | `server.stats:read` | Read Grafana instance statistics. |
| `fixed:teams:reader` | `teams:read` | List all teams. |
| `fixed:teams:creator` | `teams:create`<br>`org.users:read` | Create a team and list organization users (required to manage the created team). |
| `fixed:teams:writer` | `teams:create`<br>`teams:delete`<br>`teams:read`<br>`teams:write`<br>`teams.permissions:read`<br>`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. |
| `fixed:users:reader` | `users:read`<br>`users.quotas:read`<br>`users.authtoken:read`<br>` | Read all users and their information, such as team memberships, authentication tokens, and quotas. |
| `fixed:users:writer` | All permissions from `fixed:users:reader` and <br>`users:write`<br>`users:create`<br>`users:delete`<br>`users:enable`<br>`users:disable`<br>`users.password:write`<br>`users.permissions:write`<br>`users:logout`<br>`users.authtoken:write`<br>`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a users authentication token, or update quotas for all users. |
The following table has the existing built-in fixed role definitions. Other fixed roles might be added by plugins installed in Grafana.
The UUID presented here can be used as an identifier for [Terraform provisioning](../rbac-terraform-provisioning).
| Fixed role | UUID | Permissions | Description |
| -------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fixed:alerting:reader` | `fixed_O2oP1_uBFozI2i93klAkcvEWR30` | All permissions from `fixed:alerting.rules:reader` <br>`fixed:alerting.instances:reader`<br>`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) |
| `fixed:alerting:writer` | `fixed_-PAZgSJsDlRD8NUg-PFSeH_BkJY` | All permissions from `fixed:alerting.rules:writer` <br>`fixed:alerting.instances:writer`<br>`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) |
| `fixed:alerting.instances:reader` | `fixed_ut5fVS-Ulh_ejFoskFhJT_rYg0Y` | `alert.instances:read` for organization scope <br> `alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) |
| `fixed:alerting.instances:writer` | `fixed_pKOBJE346uyqMLdgWbk1NsQfEl0` | All permissions from `fixed:alerting.instances:reader` and<br> `alert.instances:create`<br>`alert.instances:write` for organization scope <br> `alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) |
| `fixed:alerting.notifications:reader` | `fixed_hmBn0lX5h1RZXB9Vaot420EEdA0` | `alert.notifications:read` for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) |
| `fixed:alerting.notifications:writer` | `fixed_XplK6HPNxf9AP5IGTdB5Iun4tJc` | All permissions from `fixed:alerting.notifications:reader` and<br>`alert.notifications:write`for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) |
| `fixed:alerting.provisioning:writer` | `fixed_y7pFjdEkxpx5ETdcxPvp0AgRuUo` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) |
| `fixed:alerting.provisioning.secrets:reader` | `fixed_9fmzXXZZG-Od0Amy2ofEG8Uk--c` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) |
| `fixed:alerting.provisioning.status:writer` | `fixed_eAxlzfkTuobvKEgXHveFMBZrOj8` | `alert.provisioning.provenance:write` | Set provenance status to alert rules, notification policies, contact points, etc. Should be used together with regular writer roles. [\*](#alerting-roles) |
| `fixed:alerting.rules:reader` | `fixed_fRGKL_vAqUsmUWq5EYKnOha9DcA` | `alert.rule:read`, `alert.silences:read` for scope `folders:*` <br> `alert.rules.external:read` for scope `datasources:*` <br> `alert.notifications.time-intervals:read` <br> `alert.notifications.receivers:list` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and read rule-specific silences |
| `fixed:alerting.rules:writer` | `fixed_YJJGwAalUwDZPrXSyFH8GfYBXAc` | All permissions from `fixed:alerting.rules:reader` and <br> `alert.rule:create` <br> `alert.rule:write` <br> `alert.rule:delete` <br> `alert.silences:create` <br> `alert.silences:write` for scope `folders:*` <br> `alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and manage rule-specific silences |
| `fixed:annotations:reader` | `fixed_hpZnoizrfAJsrceNcNQqWYV-xNU` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. |
| `fixed:annotations:writer` | `fixed_ZVW-Aa9Tzle6J4s2aUFcq1StKWE` | All permissions from `fixed:annotations:reader` <br>`annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. |
| `fixed:annotations.dashboard:writer` | `fixed_8A775xenXeKaJk4Cr7bchP9yXOA` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. |
| `fixed:apikeys:reader` | `fixed_kYZ7UEkwEvGmCCjTrq07cFAVFws` | `apikeys:read` for scope `apikeys:*` | Read all api keys. |
| `fixed:apikeys:writer` | `fixed_anTrcpRkm21NBO1Q2CsX8y0fiCQ` | All permissions from `fixed:apikeys:reader` and <br> `apikeys:create` <br> `apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. |
| `fixed:authentication.config:writer` | `fixed_0rYhZ2Qnzs8AdB1nX7gexk3fHDw` | `settings:read` for scope `settings:auth.saml:*` <br> `settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. |
| `fixed:dashboards:creator` | `fixed_ZorKUcEPCM01A1fPakEzGBUyU64` | `dashboards:create`<br>`folders:read` | Create dashboards. |
| `fixed:dashboards:reader` | `fixed_Sgr67JTOhjQGFlzYRahOe45TdWM` | `dashboards:read` | Read all dashboards. |
| `fixed:dashboards:writer` | `fixed_OK2YOQGIoI1G031hVzJB6rAJQAs` | All permissions from `fixed:dashboards:reader` and <br>`dashboards:write`<br>`dashboards:edit`<br>`dashboards:delete`<br>`dashboards:create`<br>`dashboards.permissions:read`<br>`dashboards.permissions:write` | Read, create, update, and delete all dashboards. |
| `fixed:dashboards.insights:reader` | `fixed_JlBJ2_gizP8zhgaeGE2rjyZe2Rs` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. |
| `fixed:dashboards.permissions:reader` | `fixed_f17oxuXW_58LL8mYJsm4T_mCeIw` | `dashboards.permissions:read` | Read all dashboard permissions. |
| `fixed:dashboards.permissions:writer` | `fixed_CcznxhWX_Yqn8uWMXMQ-b5iFW9k` | All permissions from `fixed:dashboards.permissions:reader` and <br>`dashboards.permissions:write` | Read and update all dashboard permissions. |
| `fixed:dashboards.public:writer` | `fixed_f_GHHRBciaqESXfGz2oCcooqHxs` | `dashboards.public:write` | Create, update, delete or pause a public dashboard. |
| `fixed:datasources:creator` | `fixed_XX8jHREgUt-wo1A-rPXIiFlX6Zw` | `datasources:create` | Create data sources. |
| `fixed:datasources:explorer` | `fixed_qDzW9mzx9yM91T5Bi8dHUM2muTw` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. |
| `fixed:datasources:reader` | `fixed_C2x8IxkiBc1KZVjyYH775T9jNMQ` | `datasources:read`<br>`datasources:query` | Read and query data sources. |
| `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and <br>`datasources:create`<br>`datasources:write`<br>`datasources:delete` | Read, query, create, delete, or update a data source. |
| `fixed:datasources.caching:reader` | `fixed_D2ddpGxJYlw0mbsTS1ek9fj0kj4` | `datasources.caching:read` | Read data source query caching settings. |
| `fixed:datasources.caching:writer` | `fixed_JtFjHr7jd7hSqUYcktKvRvIOGRE` | `datasources.caching:read`<br>`datasources.caching:write` | Enable, disable, or update query caching settings. |
| `fixed:datasources.id:reader` | `fixed_entg--fHmDqWY2-69N0ocawK0Os` | `datasources.id:read` | Read the ID of a data source based on its name. |
| `fixed:datasources.insights:reader` | `fixed_EBZ3NwlfecNPp2p0XcZRC1nfEYk` | `datasources.insights:read` | Read data source insights data. |
| `fixed:datasources.permissions:reader` | `fixed_ErYA-cTN3yn4h4GxaVPcawRhiOY` | `datasources.permissions:read` | Read data source permissions. |
| `fixed:datasources.permissions:writer` | `fixed_aiQh9YDfLOKjQhYasF9_SFUjQiw` | All permissions from `fixed:datasources.permissions:reader` and <br>`datasources.permissions:write` | Create, read, or delete permissions of a data source. |
| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. |
| `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read`<br>`dashboards:read` | Read all folders and dashboards. |
| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and <br>`folders:read`<br>`folders:write`<br>`folders:create`<br>`folders:delete`<br>`folders.permissions:read`<br>`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. |
| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. |
| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and <br>`folders.permissions:write` | Read and update all folder permissions. |
| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`<br>`ldap.status:read` | Read the LDAP configuration and LDAP status information. |
| `fixed:ldap:writer` | `fixed_p6AvnU4GCQyIh7-hbwI-bk3GYnU` | All permissions from `fixed:ldap:reader` and <br>`ldap.user:sync`<br>`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. |
| `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`<br>`folders:read` | Create library panel at the root level. |
| `fixed:library.panels:general.reader` | `fixed_ct0DghiBWR_2BiQm3EvNPDVmpio` | `library.panels:read` | Read all library panels at the root level. |
| `fixed:library.panels:general.writer` | `fixed_DgprkmqfN_1EhZ2v1_d1fYG8LzI` | All permissions from `fixed:library.panels:general.reader` plus<br>`library.panels:create`<br>`library.panels:delete`<br>`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. |
| `fixed:library.panels:reader` | `fixed_tvTr9CnZ6La5vvUO_U_X1LPnhUs` | `library.panels:read` | Read all library panels. |
| `fixed:library.panels:writer` | `fixed_JTljAr21LWLTXCkgfBC4H0lhBC8` | All permissions from `fixed:library.panels:reader` plus<br>`library.panels:create`<br>`library.panels:delete`<br>`library.panels:write` | Create, read, write or delete all library panels and their permissions. |
| `fixed:licensing:reader` | `fixed_OADpuXvNEylO2Kelu3GIuBXEAYE` | `licensing:read`<br>`licensing.reports:read` | Read licensing information and licensing reports. |
| `fixed:licensing:writer` | `fixed_gzbz3rJpQMdaKHt-E4q0PVaKMoE` | All permissions from `fixed:licensing:viewer` and <br>`licensing:write`<br>`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. |
| `fixed:org.users:reader` | `fixed_oCqNwlVHLOpw7-jAlwp4HzYqwGY` | `org.users:read` | Read users within a single organization. |
| `fixed:org.users:writer` | `fixed_VERj5nayasjgf_Yh0sWqqCkxWlw` | All permissions from `fixed:org.users:reader` and <br>`org.users:add`<br>`org.users:remove`<br>`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. |
| `fixed:organization:maintainer` | `fixed_CMm-uuBaPUBf4r8XG3jIvxo55bg` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs:create`<br>`orgs:delete`<br>`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. |
| `fixed:organization:reader` | `fixed_0SZPJlTHdNEe8zO91zv7Zwiwa2w` | `orgs:read`<br>`orgs.quotas:read` | Read an organization and its quotas. |
| `fixed:organization:writer` | `fixed_Y4jGqDd8w1yCrPwlik8z5Iu8-3M` | All permissions from `fixed:organization:reader` and <br> `orgs:write`<br>`orgs.preferences:read`<br>`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. |
| `fixed:plugins:maintainer` | `fixed_yEOKidBcWgbm74x-nTa3lW5lOyY` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. |
| `fixed:plugins:writer` | `fixed_MRYpGk7kpNNwt2VoVOXFiPnQziE` | `plugins:write` | Enable and disable plugins and edit plugins' settings. |
| `fixed:plugins.app:reader` | `fixed_AcZRiNYx7NueYkUqzw1o2OGGUAA` | `plugins.app:access` | Access application plugins (still enforcing the organization role). |
| `fixed:provisioning:writer` | `fixed_bgk1FCyR6OEDwhgirZlQgu5LlCA` | `provisioning:reload` | Reload provisioning. |
| `fixed:reports:reader` | `fixed_72_8LU_0ukfm6BdblOw8Z9q-GQ8` | `reports:read`<br>`reports:send`<br>`reports.settings:read` | Read all reports and shared report settings. |
| `fixed:reports:writer` | `fixed_jBW3_7g1EWOjGVBYeVRwtFxhUNw` | All permissions from `fixed:reports:reader` and <br>`reports:create`<br>`reports:write`<br>`reports:delete`<br>`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. |
| `fixed:roles:reader` | `fixed_GkfG-1NSwEGb4hpK3-E3qHyNltc` | `roles:read`<br>`teams.roles:read`<br>`users.roles:read`<br>`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. |
| `fixed:roles:resetter` | `fixed_WgPpC3qJRmVpVTJavFNwfS5RuzQ` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. |
| `fixed:roles:writer` | `fixed_W5aFaw8isAM27x_eWfElBhZ0iOc` | All permissions from `fixed:roles:reader` and <br>`roles:write`<br>`roles:delete`<br>`teams.roles:add`<br>`teams.roles:remove`<br>`users.roles:add`<br>`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. |
| `fixed:serviceaccounts:creator` | `fixed_Ikw60fckA0MyiiZ73BawSfOULy4` | `serviceaccounts:create` | Create Grafana service accounts. |
| `fixed:serviceaccounts:reader` | `fixed_QFjJAZ88iawMLInYOxPA1DB1w6I` | `serviceaccounts:read` | Read Grafana service accounts. |
| `fixed:serviceaccounts:writer` | `fixed_iBvUNUEZBZ7PUW0vdkN5iojc2sk` | `serviceaccounts:read`<br>`serviceaccounts:create`<br>`serviceaccounts:write`<br>`serviceaccounts:delete`<br>`serviceaccounts.permissions:read`<br>`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. |
| `fixed:settings:reader` | `fixed_0LaUt1x6PP8hsZzEBhqPQZFUd8Q` | `settings:read` | Read Grafana instance settings. |
| `fixed:settings:writer` | `fixed_joIHDgMrGg790hMhUufVzcU4j44` | All permissions from `fixed:settings:reader` and<br>`settings:write` | Read and update Grafana instance settings. |
| `fixed:stats:reader` | `fixed_OnRCXxZVINWpcKvTF5A1gecJ7pA` | `server.stats:read` | Read Grafana instance statistics. |
| `fixed:teams:creator` | `fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI` | `teams:create`<br>`org.users:read` | Create a team and list organization users (required to manage the created team). |
| `fixed:teams:reader` | `fixed_3SNL15gkRtJ7XeEKpMVJyQjYbjg` | `teams:read` | List all teams. |
| `fixed:teams:writer` | `fixed_xw1T0579h620MOYi4L96GUs7fZY` | `teams:create`<br>`teams:delete`<br>`teams:read`<br>`teams:write`<br>`teams.permissions:read`<br>`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. |
| `fixed:users:reader` | `fixed_buZastUG3reWyQpPemcWjGqPAd0` | `users:read`<br>`users.quotas:read`<br>`users.authtoken:read`<br>` | Read all users and their information, such as team memberships, authentication tokens, and quotas. |
| `fixed:users:writer` | `fixed_wjzgHHo_Ux25DJuELn_oiAdB_yM` | All permissions from `fixed:users:reader` and <br>`users:write`<br>`users:create`<br>`users:delete`<br>`users:enable`<br>`users:disable`<br>`users.password:write`<br>`users.permissions:write`<br>`users:logout`<br>`users.authtoken:write`<br>`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a users authentication token, or update quotas for all users. |
### Alerting roles

View File

@ -79,7 +79,7 @@ For every dashboard and data source, you can access usage information.
To see dashboard usage information, click the dashboard insights icon in the header.
{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights-11.2.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}}
{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights-11.2.png" alt="Dashboard insights icon" >}}
Dashboard insights show the following information:

View File

@ -21,12 +21,12 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/
annotations-api:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/developers/http_api/annotations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/developers/http_api/annotations/
destination: /docs/grafana-cloud/developer-resources/api-reference/http-api/annotations/
---
# Annotate visualizations
@ -70,34 +70,44 @@ Watch the following video for a quick tutorial on creating annotations:
To add an annotation, complete the following steps:
1. In the dashboard click the panel to which you're adding the annotation. A context menu will appear.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click the panel to which you're adding the annotation.
A context menu will appear.
1. In the context menu, click **Add annotation**.
![Add annotation context menu](/static/img/docs/time-series-panel/time-series-annotations-context-menu.png)
1. Add an annotation description and tags (optional).
1. Click **Save**.
1. Click **Save dashboard**.
1. Click **Exit edit**.
Alternatively, to add an annotation, press Ctrl/Cmd and click the panel, and the **Add annotation** popover will appear.
Alternatively, to add an annotation, press Ctrl/Cmd and click the panel, and the **Add annotation** context menu will appear.
### Add a region annotation
1. In the dashboard press Ctrl/Cmd and click and drag on the panel.
1. Click **Edit** in the top-right corner of the dashboard.
1. Press Ctrl/Cmd and click and drag on the panel.
![Add annotation popover](/static/img/docs/time-series-panel/time-series-annotations-add-region-annotation.gif)
1. Add an annotation description and tags (optional).
1. Click **Save**.
1. Click **Save dashboard**.
1. Click **Exit edit**.
### Edit an annotation
1. In the dashboard, hover over an annotation indicator on the Time series panel.
<!--![Add annotation popover](/static/img/docs/time-series-panel/time-series-annotations-edit-annotation.gif)-->
1. Click on the pencil icon in the annotation tooltip.
1. Modify the description and/or tags.
1. Click save.
1. Click **Edit** in the top-right corner of the dashboard.
1. Hover over the annotation indicator on the panel.
1. Click the pencil icon in the annotation tooltip.
1. Modify the description and tags.
1. Click **Save dashboard**.
1. Click **Exit edit**.
### Delete an annotation
1. In the dashboard hover over an annotation indicator on a panel.
<!--![Add annotation popover](/static/img/docs/time-series-panel/time-series-annotations-edit-annotation.gif)-->
1. Click on the trash icon in the annotation tooltip.
1. Click **Edit** in the top-right corner of the dashboard.
1. Hover over the annotation indicator on the panel.
1. Click the trash icon in the annotation tooltip.
1. Click **Save dashboard**.
1. Click **Exit edit**.
## Fetch annotations through dashboard settings
@ -109,10 +119,11 @@ Check out the video below for a quick tutorial.
### Add new annotation queries
To add a new annotation query to a dashboard, take the following steps:
To add a new annotation query to a dashboard, follow these steps:
1. Click the dashboard settings (gear) icon in the dashboard header to open the settings menu.
1. Select **Annotations**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. On the **Settings** page, go to the **Annotations** tab.
1. Click **Add annotation query**.
If you've added a query before, the **+ New query** button is displayed.
@ -140,6 +151,9 @@ To add a new annotation query to a dashboard, take the following steps:
The annotation query options are different for each data source. For information about annotations in a specific data source, refer to the specific [data source](ref:data-source) topic.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
## Built-in query
After you add an annotation, they will still be visible. This is due to the built-in annotation query that exists on all dashboards. This annotation query will fetch all annotation events that originate from the current dashboard, which are stored in Grafana, and show them on the panel where they were created. This includes alert state history annotations.
@ -150,8 +164,9 @@ To add annotations directly to the dashboard, this query must be enabled.
To confirm if the built-in query is enabled, take the following steps:
1. Click the dashboard settings (gear) icon in the dashboard header to open the dashboard settings menu.
1. Click **Annotations**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. On the **Settings** page, go to the **Annotations** tab.
1. Find the **Annotations & Alerts (Built-in)** query.
If it says **Disabled** before the name of the query, then you'll need to click the query name to open it and update the setting.
@ -162,6 +177,8 @@ You can stop annotations from being fetched and drawn by taking the following st
1. Click **Annotations**.
1. Find and click the **Annotations & Alerts (Built-in)** query to open it.
1. Click the **Enabled** toggle to turn it off.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
When you copy a dashboard using the **Save As** feature it will get a new dashboard id, so annotations created on the source dashboard will no longer be visible on the copy. You can still show them if you add a new **Annotation Query** and filter by tags. However, this only works if the annotations on the source dashboard had tags to filter by.

View File

@ -21,28 +21,28 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/#add-ad-hoc-filters
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/#add-ad-hoc-filters
destination: /docs/grafana-cloud/visualizations/dashboards/variables/add-template-variables/#add-ad-hoc-filters
manage-dashboard-links:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/manage-dashboard-links/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/manage-dashboard-links/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/manage-dashboard-links/
linking-overview:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/
template-and-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
---
# Dashboard URL variables
Grafana can apply variable values passed as query parameters in dashboard URLs.
For more information, refer to [Manage dashboard links](ref:manage-dashboard-links) and [Templates and variables][].
For more information, refer to [Manage dashboard links](ref:manage-dashboard-links) and [Templates and variables](ref:template-and-variables).
## Passing variables as query parameters

View File

@ -140,7 +140,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee
1. To add more panels to the dashboard, click **Back to dashboard**.
Then click **Add** in the dashboard header and select **Visualization** in the drop-down.
![Add drop-down](/media/docs/grafana/dashboards/screenshot-add-dropdown-10.0.png)
![Add drop-down](/media/docs/grafana/dashboards/screenshot-add-dropdown-11.2.png)
When you add additional panels to the dashboard, you're taken straight to the **Edit panel** view.
@ -203,7 +203,12 @@ You can place a panel on a dashboard in any location.
1. Click **Dashboards** in the main menu.
1. Navigate to the dashboard you want to work on.
1. Click **Edit** in the top-right corner.
1. Click the panel title and drag the panel to the new location.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.
## Resize a panel
@ -211,4 +216,9 @@ You can size a dashboard panel to suits your needs.
1. Click **Dashboards** in the main menu.
1. Navigate to the dashboard you want to work on.
1. Click **Edit** in the top-right corner.
1. To adjust the size of the panel, click and drag the lower-right corner of the panel.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.

View File

@ -48,7 +48,6 @@ To import a dashboard, follow these steps:
1. (Optional) Change the dashboard name, folder, or UID, and specify metric prefixes, if the dashboard uses any.
1. Select a data source, if required.
1. Click **Import**.
1. Save the dashboard.
## Discover dashboards on grafana.com

View File

@ -128,20 +128,20 @@ Add a link to a URL at the top of your current dashboard. You can link to any av
### Update a dashboard link
To change or update a dashboard link, follow this procedure.
To edit, duplicate, or delete dashboard link, follow these steps:
1. In the dashboard settings, on the **Links** tab, click the link that you want to edit.
1. Change the settings and then click **Save dashboard**.
1. In the dashboard you want to link, click **Edit**.
1. Click **Settings**.
1. Go to the **Links** tab.
1. Do one of the following:
- **Edit** - Click the name of the link and update the link settings.
- **Duplicate** - Click the copy link icon next to the link that you want to duplicate.
- **Delete** - Click the red **X** next to the link that you want to delete, and then **Delete**.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and then **Exit edit**.
## Duplicate a dashboard link
To duplicate a dashboard link, click the copy link icon next to the link that you want to duplicate.
### Delete a dashboard link
To delete a dashboard link, click the red **X** next to the link that you want to delete and then **Delete**.
## Panel links
Each panel can have its own set of links that are shown in the upper left of the panel after the panel title. You can link to any available URL, including dashboards, panels, or external sites. You can even control the time range to ensure the user is zoomed in on the right data in Grafana.

View File

@ -21,28 +21,38 @@ weight: 400
# Manage dashboard version history
Whenever you save a version of your dashboard, a copy of that version is saved so that previous versions of your dashboard are never lost. A list of these versions is available by entering the dashboard settings and then selecting "Versions" in the left side menu.
Whenever you save a version of your dashboard, a copy of that version is saved so that previous versions of your dashboard are never lost. You can see a list of dashboard versions in the **Versions** tab of the dashboard settings:
![Dashboards versions list](/media/docs/grafana/dashboards/screenshot-dashboard-versions-list.png)
![Dashboards versions list](/media/docs/grafana/dashboards/screenshot-dashboard-version-list-11.2.png)
The dashboard version history feature lets you compare and restore to previously saved dashboard versions.
## Comparing two dashboard versions
## Compare two dashboard versions
To compare two dashboard versions, select the two versions from the list that you wish to compare. Once selected, the "Compare versions" button will become clickable. Click the button to view the diff between the two versions.
To compare two dashboard versions, follow these steps:
![Dashboard versions selected](/media/docs/grafana/dashboards/screenshot-dashboard-versions-select.png)
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. Go to the **Versions** tab.
1. Select the two dashboard versions that you want to compare.
1. Click **Compare versions** to view the diff between the two versions.
1. Review the text descriptions of the differences between the versions.
1. (Optional) Expand the **View JSON Diff** section of the page to see the diff of the raw JSON that represents your dashboard.
1. When you've finished comparing versions, click **Back to dashboard** and **Exit edit**.
Upon clicking the button, you'll be brought to the diff view. By default, you'll see a textual summary of the changes, like in the image below.
When you're comparing versions, if one of the versions you've selected is the latest version, a button to restore the previous version is also displayed, so you can restore a version from the compare view:
![Dashboards versions diff](/media/docs/grafana/dashboards/screenshot-dashboard-versions-diff-basic.png)
![Dashboards versions diff](/media/docs/grafana/dashboards/screenshot-dashboard-compare-versions-restore-11.2.png)
If you want to view the diff of the raw JSON that represents your dashboard, you can do that as well by clicking the expand icon for the View JSON Diff section at the bottom.
## Restore a previously dashboard version
## Restoring to a previously saved dashboard version
To restore to a previously saved dashboard version, follow these steps:
If you need to restore to a previously saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the **Restore to version \<x\>** button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. Go to the **Versions** tab.
1. Click the **Restore** button next to the version.
![Restore dashboard version](/media/docs/grafana/dashboards/screenshot-dashboard-versions-restore.png)
When you restore a version, the dashboard is immediately saved and you're no longer in edit mode.
After restoring to a previous version, a new version will be created containing the same exact data as the previous version, only with a different version number. This is indicated in the "Notes column" for the row in the new dashboard version. This is done simply to ensure your previous dashboard versions are not affected by the change.
After you restore a previous version, a new version of the dashboard is created containing the same exact data as the previous version, but with a different version number. This is indicated in the **Notes column** in the **Versions** tab of the dashboard settings. This is done simply to ensure your previous dashboard versions are not affected by the change.

View File

@ -19,17 +19,22 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
json-fields:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/view-dashboard-json-model/#json-fields
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/view-dashboard-json-model/#json-fields
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/view-dashboard-json-model/#json-fields
data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/
dashboard-links:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/manage-dashboard-links/#dashboard-links
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/manage-dashboard-links/#dashboard-links
---
# Modify dashboard settings
@ -44,25 +49,29 @@ The dashboard settings page allows you to:
To access the dashboard setting page:
1. Open a dashboard in edit mode.
1. Click **Dashboard settings** (gear icon) located at the top of the page.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
## Modify dashboard time settings
Adjust dashboard time settings when you want to change the dashboard timezone, the local browser time, and specify auto-refresh time intervals.
1. On the **Dashboard settings** page, click **General**.
1. Navigate to the **Time Options** section.
1. On the **Settings** page, scroll down to the **Time Options** section of the **General** tab.
1. Specify time settings as follows.
- **Timezone:** Specify the local time zone of the service or system that you are monitoring. This can be helpful when monitoring a system or service that operates across several time zones.
- **Time zone:** Specify the local time zone of the service or system that you are monitoring. This can be helpful when monitoring a system or service that operates across several time zones.
- **Default:** Grafana uses the default selected time zone for the user profile, team, or organization. If no time zone is specified for the user profile, a team the user is a member of, or the organization, then Grafana uses the local browser time.
- **Local browser time:** The time zone configured for the viewing user browser is used. This is usually the same time zone as set on the computer.
- **Browser time:** The time zone configured for the viewing user browser is used. This is usually the same time zone as set on the computer.
- Standard [ISO 8601 time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), including UTC.
- **Auto-refresh:** Customize the options displayed for relative time and the auto-refresh options Entries are comma separated and accept any valid time unit.
- **Auto refresh:** Customize the options displayed for relative time and the auto-refresh options Entries are comma separated and accept any valid time unit.
- **Now delay:** Override the `now` time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
- **Hide time picker:** Select this option if you do not want Grafana to display the time picker.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.
## Add tags
You can add metadata to your dashboards using tags. Tags also give you the ability to filter the list of dashboards.
@ -71,13 +80,16 @@ Tags can be up to 50 characters long, including spaces.
To add tags to a dashboard, follow these steps:
1. On the **Dashboard settings** page, scroll down to the **Tags** section.
1. On the **Settings** page, scroll down to the **Tags** section of the **General** tab.
1. In the field, enter a new or existing tag.
If you're entering an existing tag, make sure that you spell it the same way or a new tag is created.
1. Click **Add** or press the Enter key.
1. Save the dashboard.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.
When you're on the **Dashboards** page, any tags you've entered show up under the **Tags** column.
@ -86,10 +98,14 @@ When you're on the **Dashboards** page, any tags you've entered show up under th
An annotation query is a query that queries for events. These events can be visualized in graphs across the dashboard as vertical lines along with a small
icon you can hover over to see the event information.
1. On the **Dashboard settings** page, click **Annotations**.
1. On the **Settings** page, go to the **Annotations** tab.
1. Click **Add annotation query**.
1. Enter a name and select a data source.
1. Complete the rest of the form to build a query and annotation.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.
The query editor UI changes based on the data source you select. Refer to the [Data source](ref:data-source) documentation for details on how to construct a query.
@ -101,33 +117,43 @@ the dashboard. These dropdowns make it easy to change the data being displayed i
For more information about variables, refer to [Variables](ref:variables).
1. On the **Dashboard settings** page, click **Variable** in the left side section menu and then the **Add variable** button.
1. In the **General** section, the name of the variable. This is the name that you will later use in queries.
1. Select a variable **Type**.
1. On the **Settings** page, go to the **Variables** tab.
1. Click **+ New variable**.
1. In the **Select variable type** drop-down, choose an option.
> **Note:** The variable type you select impacts which fields you populate on the page.
The variable type you select impacts which fields you populate on the page.
1. Define the variable and click **Update**.
1. In the **General** section, enter the name of the variable.
This is the name that you'll use later in queries.
1. Set the rest of the variable options.
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.
## Add a link
Dashboard links enable you to place links to other dashboards and web sites directly below the dashboard header. Links provide for easy navigation to other, related dashboards and content.
1. On the **Dashboard settings** page, click **Links** in the left side section menu and then the **Add link** button.
1. Enter title and in the **Type** field, select **Dashboard** or **Link**.
1. To add a dashboard link:
a. Add an optional tag. Tags are useful creating a dynamic dropdown of dashboards that all have a specific tag.
b. Select any of the dashboard link **Options**.
c. Click **Apply**.
1. To add a link:
a. Add a URL and tooltip text that appears when the user hovers over the link.
b. Select an icon that appears next to the link.
c. Select any of the dashboard link **Options**.
1. On the **Settings** page, click the **Links** tab.
1. Click **+ New link**.
1. Enter title for the link.
1. In the **Type** drop-down, select **Dashboards** or **Link**.
1. Set the rest of the link options.
For more detailed directions on creating links, refer to [Dashboard links](ref:dashboard-links)
1. Click **Save dashboard**.
1. (Optional) Enter a description of the changes you've made.
1. Click **Save**.
1. Click **Exit edit**.
## View dashboard JSON model
A dashboard in Grafana is represented by a JSON object, which stores metadata of its dashboard. Dashboard metadata includes dashboard properties, metadata from panels, template variables, panel queries, and so on.
To view a dashboard JSON model, on the **Dashboard settings** page, click **JSON**.
To view a dashboard JSON model, on the **Settings** page, click the **JSON Model** tab.
For more information about the JSON fields, refer to [JSON fields](ref:json-fields).

View File

@ -21,7 +21,7 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
---
# Dashboard JSON model
@ -30,9 +30,10 @@ A dashboard in Grafana is represented by a JSON object, which stores metadata of
To view the JSON of a dashboard:
1. Navigate to a dashboard.
1. In the top navigation menu, click the **Dashboard settings** (gear) icon.
1. Click **JSON Model**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. Go to the **JSON Model** tab.
1. When you've finished viewing the JSON, click **Back to dashboard** and **Exit edit**.
## JSON fields

View File

@ -15,7 +15,7 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/
destination: /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels/
custom-branding:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/configure-custom-branding/
@ -25,7 +25,7 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/#dashboard-insights
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/#dashboard-insights
destination: /docs/grafana-cloud/visualizations/dashboards/assess-dashboard-usage/
caching:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching
@ -56,7 +56,7 @@ You can see a list of all your public dashboards in one place by navigating to *
## Make a dashboard public
1. Click the sharing icon in the dashboard header.
1. Click **Share** in the top-right corner of the dashboard.
1. Click the **Public dashboard** tab.
1. Acknowledge the implications of making the dashboard public by selecting all the checkboxes.
1. Click **Generate public URL** to make the dashboard public and make your link live.
@ -66,7 +66,7 @@ Once you've made the dashboard public, a **Public** tag is displayed in the head
## Pause access
1. Click the sharing icon in the dashboard header.
1. Click **Share** in the top-right corner of the dashboard.
1. Click the **Public dashboard** tab.
1. Enable the **Pause sharing dashboard** toggle.
@ -74,7 +74,7 @@ The dashboard is no longer accessible, even with the link, until you make it sha
## Revoke access
1. Click the sharing icon in the dashboard header.
1. Click **Share** in the top-right corner of the dashboard.
1. Click the **Public dashboard** tab.
1. Click **Revoke public URL** to delete the public dashboard.
@ -94,7 +94,7 @@ Email sharing allows you to share your public dashboard with only specific peopl
### Invite a viewer
1. Click the sharing icon in the dashboard header.
1. Click **Share** in the top-right corner of the dashboard.
1. Click the **Public dashboard** tab.
1. Acknowledge the implications of making the dashboard public by selecting all the checkboxes.
1. Click **Generate public URL** to make the dashboard public and make your link live.
@ -111,7 +111,7 @@ If the viewer doesn't have an invitation or it's been revoked, you won't be noti
### Revoke access for a viewer
1. Click the sharing icon in the dashboard header.
1. Click **Share** in the top-right corner of the dashboard.
1. Click the **Public dashboard** tab.
1. Click **Revoke** on the viewer you'd like to revoke access for.
@ -119,7 +119,7 @@ Immediately, the viewer no longer has access to the public dashboard, nor can th
### Reinvite a viewer
1. Click the sharing icon in the dashboard header.
1. Click **Share** in the top-right corner of the dashboard.
1. Click the **Public dashboard** tab.
1. Click **Resend** on the viewer you'd like to re-share the public dashboard with.
@ -149,7 +149,7 @@ If a Grafana user has read access to the parent dashboard, they can view the pub
You can check usage analytics about your public dashboard by clicking the insights icon in the dashboard header:
{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}}
{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights-11.2.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}}
Learn more about the kind of information provided in the [dashboard insights documentation](ref:dashboard-insights-documentation).

View File

@ -49,32 +49,32 @@ refs:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/add-template-variables/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/add-template-variables/
inspect:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/inspect-variable/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/inspect-variable/
destination: /docs/grafana-cloud/visualizations/dashboards/variables/inspect-variable/
prometheus-query-variables:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/prometheus/template-variables/#use-**rate_interval
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/prometheus/template-variables/#use-**rate_interval
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/prometheus/template-variables/#use-**rate_interval
raw-variable-format:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#raw
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#raw
destination: /docs/grafana-cloud/visualizations/dashboards/variables/variable-syntax/#raw
data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/
raw-format:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#raw
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/variable-syntax/#raw
destination: /docs/grafana-cloud/visualizations/dashboards/variables/variable-syntax/#raw
add-a-data-source:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#add-a-data-source
@ -102,18 +102,20 @@ The following table lists the types of variables shipped with Grafana.
You must enter general options for any type of variable that you create.
1. Navigate to the dashboard you want to make a variable for and click the **Dashboard settings** (gear) icon at the top of the page.
1. On the **Variables** tab, click **New variable**.
1. Click **Edit** in the top-right corner of the dashboard.
1. Click **Settings**.
1. Go to the **Variables** tab.
1. Click **Add variable**.
1. Enter a **Name** for the variable.
1. In the **Type** list, select **Query**.
1. In the **Select variable type** drop-down, choose **Query**.
1. (Optional) In **Label**, enter the display name of the variable dropdown.
If you don't enter a display name, then the dropdown label is the variable name.
1. Choose a **Hide** option:
- **No selection (blank):** The variable dropdown displays the variable **Name** or **Label** value. This is the default.
- **Label:** The variable dropdown only displays the selected variable value and a down arrow.
- **Variable:** No variable dropdown is displayed on the dashboard.
1. Choose a **Show on dashboard** option:
- **Label and value** - The variable drop-down displays the variable **Name** or **Label** value. This is the default.
- **Value:** The variable drop-down only displays the selected variable value and a down arrow.
- **Nothing:** No variable drop-down is displayed on the dashboard.
## Add a query variable
@ -123,7 +125,9 @@ Query variables are generally only supported for strings. If your query returns
Query expressions can contain references to other variables and in effect create linked variables. Grafana detects this and automatically refreshes a variable when one of its linked variables change.
> **Note:** Query expressions are different for each data source. For more information, refer to the documentation for your [data source](ref:data-source).
{{< admonition type="note" >}}
Query expressions are different for each data source. For more information, refer to the documentation for your [data source](ref:data-source).
{{< /admonition >}}
1. [Enter general options](#enter-general-options).
1. In the **Data source** list, select the target data source for the query. For more information about data sources, refer to [Add a data source](ref:add-a-data-source).
@ -138,7 +142,8 @@ Query expressions can contain references to other variables and in effect create
1. In the **Sort** list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query will be used.
1. (Optional) Enter [Selection Options](#configure-variable-selection-options).
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
## Add a custom variable
@ -150,7 +155,8 @@ For example, if you have server names or region names that never change, then yo
1. In the **Values separated by comma** list, enter the values for this variable in a comma-separated list. You can include numbers, strings, or key/value pairs separated by a space and a colon. For example, `key1 : value1,key2 : value2`.
1. (Optional) Enter [Selection Options](#configure-variable-selection-options).
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
## Add a text box variable
@ -161,7 +167,8 @@ For more information about cardinality, refer to [What are cardinality spikes an
1. [Enter general options](#enter-general-options).
1. (Optional) In the **Default value** field, select the default value for the variable. If you do not enter anything in this field, then Grafana displays an empty text box for users to type text into.
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
## Add a constant variable
@ -174,7 +181,8 @@ Constant variables are useful when you have complex values that you need to incl
1. [Enter general options](#enter-general-options).
1. In the **Value** field, enter the variable value. You can enter letters, numbers, and symbols. You can even use wildcards if you use [raw format](ref:raw-format).
1. In **Preview of values**, Grafana displays the current variable value. Review it to ensure it matches what you expect.
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
## Add a data source variable
@ -188,7 +196,8 @@ _Data source_ variables enable you to quickly change the data source for an enti
1. (Optional) In **Instance name filter**, enter a regex filter for which data source instances to choose from in the variable value drop-down list. Leave this field empty to display all instances.
1. (Optional) Enter [Selection Options](#configure-variable-selection-options).
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
## Add an interval variable
@ -202,7 +211,8 @@ You can use an interval variable as a parameter to group by time (for InfluxDB),
- **Step count -** Select the number of times the current time range will be divided to calculate the value, similar to the **Max data points** query option. For example, if the current visible time range is 30 minutes, then the `auto` interval groups the data into 30 one-minute increments. The default value is 30 steps.
- **Min Interval -** The minimum threshold below which the step count intervals will not divide the time. To continue the 30 minute example, if the minimum interval is set to 2m, then Grafana would group the data into 15 two-minute increments.
1. In **Preview of values**, Grafana displays a list of the current variable values. Review them to ensure they match what you expect.
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
### Interval variable examples
@ -231,7 +241,8 @@ Ad hoc filter variables only work with Prometheus, Loki, InfluxDB, and Elasticse
You can also click **Open advanced data source picker** to see more options, including adding a data source (Admins only). For more information about data sources, refer to [Add a data source](ref:add-a-data-source).
1. Click **Add** to add the variable to the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
### Create ad hoc filters
@ -392,7 +403,8 @@ Extremely complex linked templated dashboards are possible, 5 or 10 levels deep.
### Grafana Play dashboard examples
The following Grafana Play dashboards contain fairly simple chained variables, only two layers deep. To view the variables and their settings, click **Dashboard settings** (gear icon) and then click **Variables**. Both examples are expanded in the following section.
The following Grafana Play dashboards contain fairly simple chained variables, only two layers deep. To view the variables and their settings, click **Edit**
and then **Settings**; then go to the **Variables** tab. Both examples are expanded in the following section.
- [Graphite Templated Nested](https://play.grafana.org/d/000000056/graphite-templated-nested?orgId=1&var-app=country&var-server=All&var-interval=1h)
- [InfluxDB Templated](https://play.grafana.org/d/e7bad3ef-db0c-4bbd-8245-b85c0b2ca2b9/influx-2-73a-hourly-electric-grid-monitor-for-us?orgId=1&refresh=1m)
@ -658,4 +670,6 @@ enp216s0f0np2 0000:d7:00_0_0000:d8:00_2
enp216s0f0np3 0000:d7:00_0_0000:d8:00_3
```
**Note:** Only `text` and `value` capture group names are supported.
{{< admonition type="note" >}}
Only `text` and `value` capture group names are supported.
{{< /admonition >}}

View File

@ -175,9 +175,12 @@ To add a panel in a new dashboard click **+ Add visualization** in the middle of
![Empty dashboard state](/media/docs/grafana/dashboards/empty-dashboard-10.2.png)
To add a panel to an existing dashboard, click **Add** in the dashboard header and select **Visualization** in the drop-down:
To add a panel to an existing dashboard, follow these steps:
![Add dropdown](/media/docs/grafana/dashboards/screenshot-add-dropdown-10.0.png)
1. Click **Edit** in the top-right corner of the dashboard.
1. Click the **Add** drop-down and select **Visualization**:
![Add dropdown](/media/docs/grafana/panels-visualizations/screenshot-add-dropdown-11.2.png)
## Panel configuration

View File

@ -41,10 +41,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/
configure-panel-options-documentation:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/configure-panel-options/
@ -90,20 +86,30 @@ For more information on the panel editor, refer to the [Panel editor documentati
This procedure uses dashboard variables and templates to allow you to enter trace IDs which can then be visualized. You'll use a variable called `traceId` and add it as a template query.
1. From your Grafana stack, create a new dashboard or go to an existing dashboard where you'd like to add traces visualizations.
1. Select **Add visualization** from a new dashboard or select **Add Panel** on an existing dashboard.
1. Do one of the following:
- New dashboard - Click **+ Add visualization**.
- Existing dashboard - Click **Edit** in the top-right corner and then select **Visualization** in the **Add** drop-down.
1. Search for and select the appropriate tracing data source.
1. In the top-right of the panel editor, select the **Visualizations** tab, search for, and select **Traces**.
1. Under the **Panel options**, enter a **Title** for your trace panel or have Grafana create one using [generative AI features](ref:generative-ai-features). For more information on the panel editor, refer to the [Configure panel options documentation](ref:configure-panel-options-documentation).
1. In the query editor, select the **TraceQL** query type tab.
1. In the top-right corner of the panel editor, select the **Visualizations** tab, search for, and select **Traces**.
1. Under the **Panel options**, enter a **Title** for your trace panel or have Grafana create one using [generative AI features](ref:generative-ai-features).
For more information on the panel editor, refer to the [Configure panel options documentation](ref:configure-panel-options-documentation).
1. In the query editor, click the **TraceQL** query type tab.
1. Enter `${traceId}` in the TraceQL query field to create a dashboard variable. This variable is used as the template query.
{{< figure src="/static/img/docs/panels/traces/screenshot-traces-template-query.png" caption="Add a template query" >}}
1. Select **Apply** in the panel editor to add the panel to the dashboard.
1. Go to the dashboard **Settings** and add a new variable called `traceId`, of variable type **Custom**, giving it a label if required. Select **Apply** to add the variable to the dashboard.
1. Click **Back to dashboard**.
1. Click **Settings** and go to the **Variables** tab.
1. Add a new variable called `traceId`, of variable type **Custom**, giving it a label if required.
{{< figure src="/static/img/docs/panels/traces/screenshot-traces-custom-variable.png" max-width="50%" caption="Add a Custom variable" >}}
{{< figure src="/media/docs/grafana/dashboards/screenshot-traces-custom-variable-11.2.png" max-width="50%" caption="Add a Custom variable" >}}
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
1. Verify that the panel works by using a valid trace ID for the data source used for the trace panel and editing the ID in the dashboard variable.
{{< figure src="/static/img/docs/panels/traces/screenshot-traces-traceid-panel.png" caption="Results of query in trace panel" >}}
@ -113,15 +119,19 @@ This procedure uses dashboard variables and templates to allow you to enter trac
While you can add a trace visualization to a dashboard, having to manually add trace IDs as a dashboard variable is cumbersome.
Its more useful to instead be able to use TraceQL queries to search for specific types of traces and then select appropriate traces from matching results.
1. In the same dashboard where you added the trace visualization, select **Add panel** to add a new visualization panel.
1. In the same dashboard where you added the trace visualization, click **Edit** in the top-right corner.
1. In the **Add** drop-down, select **Visualization**.
1. Select the same trace data source you used in the previous section.
1. In the top-right of the panel editor, select the **Visualizations** tab, search for, and select **Table**.
1. In the top-right corner of the panel editor, select the **Visualizations** tab, search for, and select **Table**.
1. In the query editor, select the **TraceQL** tab.
1. Under the **Panel options**, enter a **Title** for your trace panel or have Grafana create one using [generative AI features](ref:generative-ai-features).
1. Add an appropriate TraceQL query to search for traces that you would like to visualize in the dashboard. This example uses a simple, static query. You can write the TraceQL query as a template query to take advantage of other dashboard variables, if they exist. This lets you create dynamic queries based on these variables.
{{< figure src="/static/img/docs/panels/traces/screenshot-traces-dynamic-query.png" caption="Create a dynamic query" >}}
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
When results are returned from a query, the results are rendered in the panels table.
{{< figure src="/static/img/docs/panels/traces/screenshot-traces-returned-query.png" caption="Results of a returned query in the panel table" >}}
@ -132,7 +142,7 @@ The results in the traces visualization include links to the **Explore** page th
To create a set of data links in the panel, use the following steps:
1. In the right-side menu, under **Data links**, select **Add link**.
1. In the panel editor menu, under **Data links**, click **Add link**.
1. Add a **Title** for the data link.
1. Find the UUID of the dashboard by looking in your browsers address bar when the full dashboard is being rendered. Because this is a link to a dashboard in the same Grafana stack, only the path of the dashboard is required.
@ -144,8 +154,8 @@ To create a set of data links in the panel, use the following steps:
{{< figure src="/static/img/docs/panels/traces/screenshot-traces-edit-link.png" caption="Edit link and add the Trace link" >}}
1. Select **Save** to save the data link.
1. Select **Apply** from the panel editor to apply the panel to the dashboard.
1. Save the dashboard.
1. Click **Save dashboard**.
1. Click **Back to dashboard** and **Exit edit**.
You should now see a list of matching traces in the table visualization. While selecting the **TraceID** or **SpanID** fields will give you the option to either open the **Explore** page to visualize the trace or following the data link, selecting any other field (such as **Start time**, **Name** or **Duration**) automatically follows the data link, filling in the `traceId` dashboard variable, and then shows the relevant trace in the trace panel.

View File

@ -79,6 +79,8 @@ To follow this guide, ensure you have permissions in your Okta workspace to crea
1. Click **Save**.
1. Click the **Back to applications** link at the top of the page.
1. From the **More** button dropdown menu, click **Refresh Application Data**.
1. Include the `groups` scope in the **Scopes** field in Grafana of the Okta integration.
For Terraform or in the Grafana configuration file, include the `groups` scope in `scopes` field.
#### Optional: Add the role attribute to the User (default) Okta profile
@ -161,12 +163,7 @@ To integrate your Okta OIDC provider with Grafana using our Okta OIDC integratio
1. Review the list of other Okta OIDC [configuration options]({{< relref "#configuration-options" >}}) and complete them as necessary.
1. Optional: [Configure a refresh token]({{< relref "#configure-a-refresh-token" >}}):
a. Extend the `scopes` field of `[auth.okta]` section in Grafana configuration file with the refresh token scope used by your OIDC provider.
b. Enable the [refresh token]({{< relref "#configure-a-refresh-token" >}}) at the Okta application settings.
1. Optional: [Configure a refresh token]({{< relref "#configure-a-refresh-token" >}}).
1. [Configure role mapping]({{< relref "#configure-role-mapping" >}}).
1. Optional: [Configure team synchronization]({{< relref "#configure-team-synchronization-enterprise-only" >}}).
1. Restart Grafana.

View File

@ -33,6 +33,10 @@ This feature is automatically available in Grafana 10 (and newer) and Grafana Cl
To use the TraceQL query editor in self-hosted Grafana 9.3.2 and older, you need to [enable the `traceqlEditor` feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/).
If trying to query a self-managed Grafana Tempo or Grafana Enterprise Traces database with a gateway (e.g., nginx) in front of it from your hosted Grafana, that gateway (e.g., nginx) must allow gRPC connections. If it does not, streaming will not work and queries will fail to return results.
If you cannot configure your gateway to allow gRPC, open a support escalation to request streaming query results be disabled in your hosted Grafana.
## Write TraceQL queries using the query editor
The Tempo data sources TraceQL query editor helps you query and display traces from Tempo in **Explore**.

View File

@ -0,0 +1,172 @@
---
Feedback Link: https://github.com/grafana/tutorials/issues/new
categories:
- alerting
description: This is part 2 of the Get started with Grafana Alerting tutorials. Learn how to leverage alert instances, and set up a notification policy that routes alert notifications based on labels to a specific contact point.
id: alerting-get-started-pt2
labels:
products:
- enterprise
- oss
- cloud
tags:
- beginner
title: Get started with Grafana Alerting - Part 2
weight: 50
---
# Get started with Grafana Alerting - Part 2
## Introduction
This is part 2 of the [Get Started with Grafana Alerting tutorial](http://grafana.com/tutorials/alerting-get-started/).
In this guide, we dig into more complex yet equally fundamental elements of Grafana Alerting: **alert instances** and **notification policies**.
After introducing each component, you will learn how to:
- Configure an alert rule that returns more than one alert instance
- Create notification policies that route firing alert instances to different contact points
- Use labels to match alert instances and notification policies
Learning about alert instances and notification policies is useful if you have more than one contact point in your organization, or if your alert rule returns a number of metrics that you want to handle separately by routing each alert instance to a specific contact point. The tutorial will introduce each concept, followed by how to apply both concepts in a real-world scenario.
## Alert instances
An [alert instance](https://grafana.com/docs/grafana/latest/alerting/fundamentals/#alert-instances) is an event that matches a metric returned by an alert rule query.
Let's consider a scenario where you're monitoring website traffic using Grafana. You've set up an alert rule to trigger an alert instance if the number of page views exceeds a certain threshold (more than `1000` page views) within a specific time period, say, over the past `5` minutes.
If the query returns more than one time-series, each time-series represents a different metric or aspect being monitored. In this case, the alert rule is applied individually to each time-series.
{{< figure alt="Screenshot displaying alert instances in the context of an alert rule, highlighting the specific alerts triggered by the rule and their respective statuses" src="/media/docs/alerting/get-started-digram-instance-grey.png" max-width="1200px" caption="Alert Instances in the Context of an Alert Rule" >}}
In this scenario, each time-series is evaluated independently against the alert rule. It results in the creation of an alert instance for each time-series. The time-series corresponding to the desktop page views meets the threshold and, therefore, results in an alert instance in **Firing** state for which an alert notification is sent. The mobile alert instance state remains **Normal**.
## Notification policies
[Notification policies](https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/notification-policies/) route alerts to different communication channels, reducing alert noise and providing control over when and how alerts are sent. For example, you might use notification policies to ensure that critical alerts about server downtime are sent immediately to the on-call engineer. Another use case could be routing performance alerts to the development team for review and action.
Key Characteristics:
- Route alert notifications by matching alerts and policies with labels
- Manage when to send notifications
{{< figure alt="Screenshot illustrating the routing of alerts with notification policies, including the configuration and flow of alerts through different notification channels" src="/media/docs/alerting/get-started-notification-policy-tree-combo.png" max-width="1200px" caption="Routing of alerts with notification policies" >}}
In the above diagram, alert instances and notification policies are matched by labels. For instance, the label `team=operations` matches the alert instance “**Pod stuck in CrashLoop**” and “**Disk Usage -80%**” to child policies that send alert notifications to a particular contact point (operations@grafana.com).
## Create notification policies
Create a notification policy if you want to handle metrics returned by alert rules separately by routing each alert instance to a specific contact point. In Grafana, click on the icon at the top left corner of the screen to access the navigation menu.
1. Navigate to **Alerts & IRM > Alerting > Notification policies**.
1. In the Default policy, click **+ New child policy**.
1. In the field **Label** enter `device`, and in the field **Value** enter `desktop`.
1. From the **Contact point** drop-down, choose **Webhook**.
{{< admonition type="note" >}}
If you dont have any contact points, add a [Contact point](http://localhost:3002/docs/grafana/latest/tutorials/alerting-get-started/#create-a-contact-point).
{{</ admonition >}}
1. Click **Save Policy**.
This new child policy routes alerts that match the label `device=desktop` to the Webhook contact point.
1. **Repeat the steps above to create a second child policy** to match another alert instance. For labels use: `device=mobile`. Use the Webhook integration for the contact point. Alternatively, experiment by using a different Webhook endpoint or a [different integration](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/#list-of-supported-integrations).
## Create an alert rule that returns alert instances
The alert rule that you are about to create is meant to monitor web traffic page views. The objective is to explore what an alert instance is and how to leverage routing individual alert instances by using label matchers and notification policies.
### Add a data source
Grafana includes a [test data source](https://grafana.com/docs/grafana/latest/datasources/testdata/) that creates simulated time series data.
1. In Grafana navigate to **Connections > Add new connection**.
1. Search for **TestData**.
1. Click **Add new data source**.
1. Click **Save & test**.
You should see a message confirming that the data source is working.
### Create an alert rule
1. Navigate to **Alerting > Alert rules**.
1. Click **New alert rule**.
### Enter an alert rule name
Make it short and descriptive as this will appear in your alert notification. For instance, `web-traffic`.
### Define query and alert condition
In this section, we define queries, expressions (used to manipulate the data), and the condition that must be met for the alert to be triggered.
1. Select **TestData** data source from the drop-down menu.
1. From **Scenario** select **CSV Content**.
1. Copy in the following CSV data:
```
device,views
desktop,1200
mobile,900
```
The above CSV data simulates a data source returning multiple time series, each leading to the creation of an alert instance for that specific time series. Note that the data returned matches the example in the [Alert instance](#alert-instances) section.
1. Remove the B **Reduce expression** (click the bin icon). The Reduce expression is default, and in this case, is not required since the queried data is already reduced. Note that the Threshold expression is now your **Alert condition**.
1. In the C **Threshold expression**:
- Change the **Input** to **A** to select the data source.
- Enter `1000` as the threshold value. This is the value above which the alert rule should trigger.
1. Click **Preview** to run the queries.
It should return two series.`desktop` in Firing state, and `mobile` in Normal state. The values `1`, and `0` mean that the condition is either `true` or `false`.
{{< figure alt="Screenshot showing a preview of a query in Grafana that returns two alert instances, including the query results and relevant alert details" src="/media/docs/alerting/get-started-expression-instances.png" max-width="1200px" caption="Preview of a query returning two alert instances in Grafana." >}}
### Set evaluation behavior
In the [life cycle](http://grafana.com/docs/grafana/next/alerting/fundamentals/alert-rule-evaluation/) of alert instances, when an alert condition (threshold) is not met, the alert instance state is **Normal**. Similarly, when the condition is breached (for longer than the pending period, which in this tutorial will be 0), the alert instance state switches back to **Alerting**, which means that the alert rule state is **Firing**, and a notification is sent.
To set up evaluation behavior:
1. In **Folder**, click **+ New folder** and enter a name. For example: `web-traffic-alerts`. This folder will contain our alerts.
1. In the **Evaluation group**, repeat the above step to create a new evaluation group. We will name it `1m` (referring to “1 minute”).
1. Choose an Evaluation interval (how often the alert will be evaluated). Choose `1m`.
1. Set the pending period to `0s` (zero seconds), so the alert rule fires the moment the condition is met.
### Configure labels and notifications
In this section, you can select how you want to route your alert instances. Since we want to route by notification policy, we need to ensure that the labels match the alert instance.
1. Choose **Use notification policy**.
1. Click **Preview routing**. Based on the existing labels, you should see a preview of what policies are matching with the alerts. There should be two alert instances matching the labels that were previously setup in each notification policy: `device=desktop`, `device=mobile`.
These [types of labels](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/annotation-label/#label-types) are generated by the data source query and they can be leveraged to match our notification policies without needing to manually add them to the alert rule.
{{< figure alt="Screenshot showing a routing preview of matched notification policies, detailing how alerts are matched and routed to specific notification channels" src="/media/docs/alerting/get-started-alert-instace-routing-prev.png" max-width="1200px" caption="Routing preview of matched notification policies" >}}
{{< admonition type="note" >}}
Even if both labels match the policies, only the alert instance in Firing state produces an alert notification.
{{</ admonition >}}
1. Click **Save rule and exit**.
Now that we have set up the alert rule, its time to check the alert notification.
## Receive alert notifications
Now that the alert rule has been configured, you should receive alert [notifications](http://grafana.com/docs/grafana/next/alerting/fundamentals/alert-rule-evaluation/state-and-health/#notifications) in the contact point whenever the alert triggers and gets resolved. In our example, each alert instance should be routed separately as we configured labels to match notification policies. Once the evaluation interval has concluded (1m), you should receive an alert notification in the Webhook endpoint.
{{< figure alt="Screenshot showing the exploration of alert notification details in a webhook endpoint, displaying the content and structure of the alert payload received by the endpoint" src="/media/docs/alerting/get-started-webhook-alert-isntance.png" max-width="1200px" caption="Exploring alert notification details in webhook endpoint" >}}
The alert notification details show that the alert instance corresponding to the website views from desktop devices was correctly routed through the notification policy to the Webhook contact point. The notification also shows that the instance is in **Firing** state, as well as it includes the label `device=desktop`, which makes the routing of the alert instance possible.
Feel free to change the CSV data in the alert rule to trigger the routing of the alert instance that matches the label `device=mobile`.
## Summary
In this tutorial, you have learned how Grafana Alerting can route individual alert instances using the labels generated by the data-source query and match these labels with notification policies, which in turn routes alert notifications to specific contact points.
If you run into any problems, you are welcome to post questions in our [Grafana Community forum](https://community.grafana.com/).
Enjoy your monitoring!

View File

@ -1,11 +1,9 @@
---
Feedback Link: https://github.com/grafana/tutorials/issues/new
authors:
- antonio-calero-merello
categories:
- alerting
description: Get started with Grafana Alerting by creating your first alert in just a few minutes. Learn how to set up an alert, send alert notifications to a public webhook, and generate sample data to observe your alert in action.
id: alerting-get-started
id: alerting-get-started-pt1
labels:
products:
- enterprise
@ -13,11 +11,11 @@ labels:
- cloud
tags:
- beginner
title: Get started with Grafana Alerting
title: Get started with Grafana Alerting - Part 1
weight: 50
---
# Get Started with Grafana Alerting
# Get Started with Grafana Alerting - Part 1
In this guide, we'll walk you through the process of setting up your first alert in just a few minutes. You'll witness your alert in action with real-time data, as well as sending alert notifications.

View File

@ -93,7 +93,6 @@
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/add": "^2",
"@types/angular": "1.8.9",
"@types/angular-route": "1.7.6",
"@types/babel__core": "^7",
@ -401,8 +400,7 @@
"uuid": "9.0.1",
"visjs-network": "4.25.0",
"whatwg-fetch": "3.6.20",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz",
"yarn": "^1.22.22"
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
},
"resolutions": {
"underscore": "1.13.6",

View File

@ -16,6 +16,7 @@ export * from './themes';
export * from './monaco';
export * from './geo/layer';
export * from './query';
export { amendTable, trimTable, type Table } from './table/amendTimeSeries';
export {
type ValueMatcherOptions,
type BasicValueMatcherOptions,

View File

@ -1,4 +1,4 @@
import { Table, amendTable } from "./amendTimeSeries";
import { Table, amendTable } from './amendTimeSeries';
describe('amendTable', () => {
it('should append nextTable when there is no overlap (nextTable after prevTable)', () => {

View File

@ -1,4 +1,4 @@
import { closestIdx } from '@grafana/data';
import { closestIdx } from '../';
export type Table = [times: number[], ...values: any[][]];
@ -98,7 +98,7 @@ export function trimTable(table: Table, fromTime: number, toTime: number): Table
if (fromIdx != null || toIdx != null) {
times = times.slice(fromIdx ?? 0, toIdx);
vals = vals.map(vals2 => vals2.slice(fromIdx ?? 0, toIdx));
vals = vals.map((vals2) => vals2.slice(fromIdx ?? 0, toIdx));
}
return [times, ...vals];

View File

@ -133,16 +133,23 @@ Check if strings are marked for translation.
```tsx
// Bad ❌
const SearchTitle = ({ term }) => (
<div>
Results for <em>{term}</em>
</div>
);
const SearchTitle = ({ term }) => <div>Results for {term}</div>;
//Good ✅
// Good ✅
const SearchTitle = ({ term }) => <Trans i18nKey="search-page.results-title">Results for {{ term }}</Trans>;
// Good ✅ (if you need to interpolate variables inside nested components)
const SearchTerm = ({ term }) => <Text color="success">{term}</Text>;
const SearchTitle = ({ term }) => (
<Trans i18nKey="search-page.results-title">
Results for <em>{{ term }}</em>
Results for <SearchTerm term={term} />
</Trans>
);
// Good ✅ (if you need to interpolate variables and additional translated strings inside nested components)
const SearchTitle = ({ term }) => (
<Trans i18nKey="search-page.results-title" values={{ myVariable: term }}>
Results for <Text color="success">{'{{ myVariable }}'} and this translated text is also in green</Text>
</Trans>
);
```

View File

@ -423,6 +423,10 @@ const getStyles = (theme: GrafanaTheme2) => {
position: 'absolute',
top: 0,
width: '100%',
// this is to force the loading bar container to create a new stacking context
// otherwise, in webkit browsers on windows/linux, the aliasing of panel text changes when the loading bar is shown
// see https://github.com/grafana/grafana/issues/88104
zIndex: 1,
}),
containNone: css({
contain: 'none',

View File

@ -17,6 +17,7 @@ import { getMarkdownStyles } from './markdownStyles';
import { getPageStyles } from './page';
import { getRcTimePickerStyles } from './rcTimePicker';
import { getSkeletonStyles } from './skeletonStyles';
import { getSlateStyles } from './slate';
import { getUplotStyles } from './uPlot';
/** @internal */
@ -38,6 +39,7 @@ export function GlobalStyles() {
getAgularPanelStyles(theme),
getMarkdownStyles(theme),
getSkeletonStyles(theme),
getSlateStyles(theme),
getRcTimePickerStyles(theme),
getUplotStyles(theme),
getLegacySelectStyles(theme),

View File

@ -0,0 +1,145 @@
import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getSlateStyles(theme: GrafanaTheme2) {
return css({
'.slate-query-field': {
fontSize: theme.typography.fontSize,
fontFamily: theme.typography.fontFamilyMonospace,
height: 'auto',
wordBreak: 'break-word',
// Affects only placeholder in query field. Adds scrollbar only if content is cropped.
overflow: 'auto',
},
'.slate-query-field__wrapper': {
position: 'relative',
display: 'inline-block',
padding: '6px 8px',
minHeight: '32px',
width: '100%',
color: theme.colors.text.primary,
backgroundColor: theme.components.input.background,
backgroundImage: 'none',
border: `1px solid ${theme.components.input.borderColor}`,
borderRadius: theme.shape.radius.default,
transition: 'all 0.3s',
lineHeight: '18px',
},
'.slate-query-field__wrapper--disabled': {
backgroundColor: 'inherit',
cursor: 'not-allowed',
},
'.slate-typeahead': {
'.typeahead': {
position: 'relative',
zIndex: theme.zIndex.typeahead,
borderRadius: theme.shape.radius.default,
border: `1px solid ${theme.components.panel.borderColor}`,
maxHeight: '66vh',
overflowY: 'scroll',
overflowX: 'hidden',
outline: 'none',
listStyle: 'none',
background: theme.components.panel.background,
color: theme.colors.text.primary,
boxShadow: theme.shadows.z2,
},
'.typeahead-group__title': {
color: theme.colors.text.secondary,
fontSize: theme.typography.size.sm,
lineHeight: theme.typography.body.lineHeight,
padding: theme.spacing(1),
},
'.typeahead-item': {
height: 'auto',
fontFamily: theme.typography.fontFamilyMonospace,
padding: theme.spacing(1, 1, 1, 2),
fontSize: theme.typography.size.sm,
textOverflow: 'ellipsis',
overflow: 'hidden',
zIndex: 1,
display: 'block',
whiteSpace: 'nowrap',
cursor: 'pointer',
transition:
'color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1)',
},
'.typeahead-item__selected': {
backgroundColor: theme.isDark ? theme.v1.palette.dark9 : theme.v1.palette.gray6,
'.typeahead-item-hint': {
fontSize: theme.typography.size.xs,
color: theme.colors.text.primary,
whiteSpace: 'normal',
},
},
'.typeahead-match': {
color: theme.v1.palette.yellow,
borderBottom: `1px solid ${theme.v1.palette.yellow}`,
// Undoing mark styling
padding: 'inherit',
background: 'inherit',
},
},
/* SYNTAX */
'.slate-query-field, .prism-syntax-highlight': {
'.token.comment, .token.block-comment, .token.prolog, .token.doctype, .token.cdata': {
color: theme.colors.text.secondary,
},
'.token.variable, .token.entity': {
color: theme.colors.text.primary,
},
'.token.property, .token.tag, .token.constant, .token.symbol, .token.deleted': {
color: theme.colors.error.text,
},
'.token.attr-value, .token.selector, .token.string, .token.char, .token.builtin, .token.inserted': {
color: theme.colors.success.text,
},
'.token.boolean, .token.number, .token.operator, .token.url': {
color: '#fe85fc',
},
'.token.function, .token.attr-name, .token.function-name, .token.atrule, .token.keyword, .token.class-name': {
color: theme.colors.primary.text,
},
'.token.punctuation, .token.regex, .token.important': {
color: theme.v1.palette.orange,
},
'.token.important': {
fontWeight: 'normal',
},
'.token.bold': {
fontWeight: 'bold',
},
'.token.italic': {
fontStyle: 'italic',
},
'.token.entity': {
cursor: 'help',
},
'.namespace': {
opacity: 0.7,
},
},
});
}

View File

@ -1,11 +0,0 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=example.grafana.app
// The testing api is a dependency free service that we can use to experiment with
// api aggregation across multiple deployment models. Specifically:
// - standalone: running as part of the standard grafana build
// - aggregated: running as the target
package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/example/v0alpha1"

View File

@ -1,30 +0,0 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
const (
GROUP = "example.grafana.app"
VERSION = "v0alpha1"
APIVERSION = GROUP + "/" + VERSION
)
var RuntimeResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"runtime", "runtime", "RuntimeInfo",
func() runtime.Object { return &RuntimeInfo{} },
func() runtime.Object { return &RuntimeInfo{} },
)
var DummyResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"dummy", "dummy", "DummyResource",
func() runtime.Object { return &DummyResource{} },
func() runtime.Object { return &DummyResourceList{} },
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
)

View File

@ -1,48 +0,0 @@
package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// Mirrors the info exposed in "github.com/grafana/grafana/pkg/setting"
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type RuntimeInfo struct {
metav1.TypeMeta `json:",inline"`
// Unix timestamp when the process started
StartupTime int64 `json:"startupTime,omitempty"`
BuildVersion string `json:"buildVersion,omitempty"`
BuildCommit string `json:"buildCommit,omitempty"`
EnterpriseBuildCommit string `json:"enterpriseBuildCommit,omitempty"`
BuildBranch string `json:"buildBranch,omitempty"`
BuildStamp int64 `json:"buildStamp,omitempty"`
IsEnterprise bool `json:"enterprise,omitempty"`
Packaging string `json:"packaging,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DummyResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec common.Unstructured `json:"spec,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DummyResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DummyResource `json:"items,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DummySubresource struct {
metav1.TypeMeta `json:",inline"`
// add subresource info here
Info string `json:"info,omitempty"`
}

View File

@ -1,122 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DummyResource) DeepCopyInto(out *DummyResource) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummyResource.
func (in *DummyResource) DeepCopy() *DummyResource {
if in == nil {
return nil
}
out := new(DummyResource)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DummyResource) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DummyResourceList) DeepCopyInto(out *DummyResourceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DummyResource, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummyResourceList.
func (in *DummyResourceList) DeepCopy() *DummyResourceList {
if in == nil {
return nil
}
out := new(DummyResourceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DummyResourceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DummySubresource) DeepCopyInto(out *DummySubresource) {
*out = *in
out.TypeMeta = in.TypeMeta
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummySubresource.
func (in *DummySubresource) DeepCopy() *DummySubresource {
if in == nil {
return nil
}
out := new(DummySubresource)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DummySubresource) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RuntimeInfo) DeepCopyInto(out *RuntimeInfo) {
*out = *in
out.TypeMeta = in.TypeMeta
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeInfo.
func (in *RuntimeInfo) DeepCopy() *RuntimeInfo {
if in == nil {
return nil
}
out := new(RuntimeInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RuntimeInfo) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@ -1,19 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}

View File

@ -1,219 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
// This file was autogenerated by openapi-gen. Do not edit it manually!
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResource": schema_pkg_apis_example_v0alpha1_DummyResource(ref),
"github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResourceList": schema_pkg_apis_example_v0alpha1_DummyResourceList(ref),
"github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummySubresource": schema_pkg_apis_example_v0alpha1_DummySubresource(ref),
"github.com/grafana/grafana/pkg/apis/example/v0alpha1.RuntimeInfo": schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref),
}
}
func schema_pkg_apis_example_v0alpha1_DummyResource(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_example_v0alpha1_DummyResourceList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResource"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResource", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_example_v0alpha1_DummySubresource(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"info": {
SchemaProps: spec.SchemaProps{
Description: "add subresource info here",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Mirrors the info exposed in \"github.com/grafana/grafana/pkg/setting\"",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"startupTime": {
SchemaProps: spec.SchemaProps{
Description: "Unix timestamp when the process started",
Type: []string{"integer"},
Format: "int64",
},
},
"buildVersion": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"buildCommit": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"enterpriseBuildCommit": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"buildBranch": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"buildStamp": {
SchemaProps: spec.SchemaProps{
Type: []string{"integer"},
Format: "int64",
},
},
"enterprise": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
"packaging": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}

View File

@ -1 +0,0 @@
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/example/v0alpha1,RuntimeInfo,IsEnterprise

View File

@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/registry/apis/peakq"
@ -28,7 +27,6 @@ type Service struct{}
func ProvideRegistryServiceSink(
_ *dashboard.DashboardsAPIBuilder,
_ *playlist.PlaylistAPIBuilder,
_ *example.TestingAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *featuretoggle.FeatureFlagAPIBuilder,
_ *datasource.DataSourceAPIBuilder,

View File

@ -1,128 +0,0 @@
package example
import (
"context"
"fmt"
"slices"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
var (
_ rest.Storage = (*dummyStorage)(nil)
_ rest.Scoper = (*dummyStorage)(nil)
_ rest.SingularNameProvider = (*dummyStorage)(nil)
_ rest.Getter = (*dummyStorage)(nil)
_ rest.Lister = (*dummyStorage)(nil)
)
type dummyStorage struct {
store *genericregistry.Store
names []string
creationTimestamp metav1.Time
}
func newDummyStorage(gv schema.GroupVersion, scheme *runtime.Scheme, names ...string) *dummyStorage {
var resourceInfo = example.DummyResourceInfo
strategy := grafanaregistry.NewStrategy(scheme)
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
return &dummyStorage{
store: store,
names: names,
creationTimestamp: metav1.Now(),
}
}
func (s *dummyStorage) New() runtime.Object {
return s.store.New()
}
func (s *dummyStorage) Destroy() {}
func (s *dummyStorage) NamespaceScoped() bool {
return true
}
func (s *dummyStorage) GetSingularName() string {
return example.DummyResourceInfo.GetSingularName()
}
func (s *dummyStorage) NewList() runtime.Object {
return s.store.NewListFunc()
}
func (s *dummyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.store.TableConvertor.ConvertToTable(ctx, object, tableOptions)
}
func (s *dummyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
info, err := grafanarequest.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
idx := slices.Index(s.names, name)
if idx < 0 {
return nil, fmt.Errorf("dummy not found")
}
return &example.DummyResource{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: info.Value,
CreationTimestamp: s.creationTimestamp,
ResourceVersion: "1",
},
Spec: common.Unstructured{
Object: map[string]any{
"Dummy": name,
},
},
}, nil
}
func (s *dummyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
info, err := grafanarequest.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
res := &example.DummyResourceList{}
for _, name := range s.names {
res.Items = append(res.Items, example.DummyResource{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: info.Value,
CreationTimestamp: s.creationTimestamp,
ResourceVersion: "1",
},
Spec: common.Unstructured{
Object: map[string]any{
"Dummy": name,
},
},
})
}
return res, nil
}

View File

@ -1,226 +0,0 @@
package example
import (
"context"
"fmt"
"net/http"
"github.com/prometheus/client_golang/prometheus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/grafana/pkg/apimachinery/identity"
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
"github.com/grafana/grafana/pkg/apiserver/builder"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
var _ builder.APIGroupBuilder = (*TestingAPIBuilder)(nil)
// This is used just so wire has something unique to return
type TestingAPIBuilder struct {
codecs serializer.CodecFactory
gv schema.GroupVersion
}
func NewTestingAPIBuilder() *TestingAPIBuilder {
return &TestingAPIBuilder{
gv: schema.GroupVersion{Group: example.GROUP, Version: example.VERSION},
}
}
func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *TestingAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
}
builder := NewTestingAPIBuilder()
apiregistration.RegisterAPI(builder)
return builder
}
func (b *TestingAPIBuilder) GetGroupVersion() schema.GroupVersion {
return b.gv
}
func (b *TestingAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
// Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go
return grafanarest.Mode0
}
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
scheme.AddKnownTypes(gv,
&example.RuntimeInfo{},
&example.DummyResource{},
&example.DummyResourceList{},
&example.DummySubresource{},
)
}
func (b *TestingAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
addKnownTypes(scheme, b.gv)
// Link this version to the internal representation.
// This is used for server-side-apply (PATCH), and avoids the error:
// "no kind is registered for the type"
addKnownTypes(scheme, schema.GroupVersion{
Group: b.gv.Group,
Version: runtime.APIVersionInternal,
})
// If multiple versions exist, then register conversions from zz_generated.conversion.go
// if err := playlist.RegisterConversions(scheme); err != nil {
// return err
// }
metav1.AddToGroupVersion(scheme, b.gv)
return scheme.SetVersionPriority(b.gv)
}
func (b *TestingAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
_ generic.RESTOptionsGetter,
_ grafanarest.DualWriterMode,
_ prometheus.Registerer,
) (*genericapiserver.APIGroupInfo, error) {
b.codecs = codecs
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(b.gv.Group, scheme, metav1.ParameterCodec, codecs)
storage := map[string]rest.Storage{}
storage[example.RuntimeResourceInfo.StoragePath()] = newDeploymentInfoStorage(b.gv, scheme)
storage[example.DummyResourceInfo.StoragePath()] = newDummyStorage(b.gv, scheme, "test1", "test2", "test3")
storage[example.DummyResourceInfo.StoragePath("sub")] = &dummySubresourceREST{}
apiGroupInfo.VersionedResourcesStorageMap[b.gv.Version] = storage
return &apiGroupInfo, nil
}
func (b *TestingAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return example.GetOpenAPIDefinitions
}
// Register additional routes with the server
func (b *TestingAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
return &builder.APIRoutes{
Root: []builder.APIRouteHandler{
{
Path: "aaa",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
Parameters: []*spec3.Parameter{
{ParameterProps: spec3.ParameterProps{
Name: "a",
}},
},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
StatusCodeResponses: map[int]*spec3.Response{
200: {
ResponseProps: spec3.ResponseProps{
Description: "OK",
Content: map[string]*spec3.MediaType{
"text/plain": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
},
},
},
},
},
},
},
},
},
Handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Root level handler (aaa)"))
},
},
{
Path: "bbb",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
Parameters: []*spec3.Parameter{
{ParameterProps: spec3.ParameterProps{
Name: "b",
}},
},
},
},
},
Handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Root level handler (bbb)"))
},
},
},
Namespace: []builder.APIRouteHandler{
{
Path: "ccc",
Spec: &spec3.PathProps{
Summary: "an example at the root level",
Description: "longer description here?",
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
Parameters: []*spec3.Parameter{
{ParameterProps: spec3.ParameterProps{
Name: "a",
}},
},
},
},
},
Handler: func(w http.ResponseWriter, r *http.Request) {
info, ok := request.RequestInfoFrom(r.Context())
if !ok {
responsewriters.ErrorNegotiated(
apierrors.NewInternalError(fmt.Errorf("no RequestInfo found in the context")),
b.codecs, schema.GroupVersion{}, w, r,
)
return
}
_, _ = w.Write([]byte("Custom namespace route ccc: " + info.Namespace))
},
},
},
}
}
func (b *TestingAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if !attr.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
// require a user
_, err = identity.GetRequester(ctx)
if err != nil {
return authorizer.DecisionDeny, "valid user is required", err
}
return authorizer.DecisionNoOpinion, "", err // fallback to org/role logic
})
}

View File

@ -1,86 +0,0 @@
package example
import (
"context"
"time"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ rest.Storage = (*staticStorage)(nil)
_ rest.Scoper = (*staticStorage)(nil)
_ rest.SingularNameProvider = (*staticStorage)(nil)
_ rest.Lister = (*staticStorage)(nil)
)
type staticStorage struct {
Store *genericregistry.Store
info example.RuntimeInfo
}
func newDeploymentInfoStorage(gv schema.GroupVersion, scheme *runtime.Scheme) *staticStorage {
var resourceInfo = example.RuntimeResourceInfo
strategy := grafanaregistry.NewStrategy(scheme)
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,
PredicateFunc: grafanaregistry.Matcher,
DefaultQualifiedResource: resourceInfo.GroupResource(),
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
TableConvertor: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
return &staticStorage{
Store: store,
info: example.RuntimeInfo{
TypeMeta: example.RuntimeResourceInfo.TypeMeta(),
BuildVersion: setting.BuildVersion,
BuildCommit: setting.BuildCommit,
BuildBranch: setting.BuildBranch,
EnterpriseBuildCommit: setting.EnterpriseBuildCommit,
BuildStamp: setting.BuildStamp,
IsEnterprise: setting.IsEnterprise,
Packaging: setting.Packaging,
StartupTime: time.Now().UnixMilli(),
},
}
}
func (s *staticStorage) New() runtime.Object {
return s.Store.New()
}
func (s *staticStorage) Destroy() {}
func (s *staticStorage) NamespaceScoped() bool {
return false
}
func (s *staticStorage) GetSingularName() string {
return example.RuntimeResourceInfo.GetSingularName()
}
func (s *staticStorage) NewList() runtime.Object {
return s.Store.NewListFunc()
}
func (s *staticStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.Store.TableConvertor.ConvertToTable(ctx, object, tableOptions)
}
func (s *staticStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
return &s.info, nil
}

View File

@ -1,54 +0,0 @@
package example
import (
"context"
"fmt"
"net/http"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apimachinery/identity"
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
type dummySubresourceREST struct{}
var _ = rest.Connecter(&dummySubresourceREST{})
func (r *dummySubresourceREST) New() runtime.Object {
return &example.DummySubresource{}
}
func (r *dummySubresourceREST) Destroy() {
}
func (r *dummySubresourceREST) ConnectMethods() []string {
return []string{"GET"}
}
func (r *dummySubresourceREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, "" // true means you can use the trailing path as a variable
}
func (r *dummySubresourceREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
// This response object format is negotiated by k8s
dummy := &example.DummySubresource{
Info: fmt.Sprintf("%s/%s", info.Value, user.GetLogin()),
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
responder.Object(http.StatusOK, dummy)
}), nil
}

View File

@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/registry/apis/peakq"
@ -29,7 +28,6 @@ var WireSet = wire.NewSet(
// Each must be added here *and* in the ServiceSink above
playlist.RegisterAPIService,
dashboard.RegisterAPIService,
example.RegisterAPIService,
dashboardsnapshot.RegisterAPIService,
featuretoggle.RegisterAPIService,
datasource.RegisterAPIService,

View File

@ -9,20 +9,36 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
)
var (
allowedCoreActions = map[string]string{
"plugins.app:access": "plugins:id:",
"folders:create": "folders:uid:",
"folders:read": "folders:uid:",
"folders:write": "folders:uid:",
"folders:delete": "folders:uid:",
"folders.permissions:read": "folders:uid:",
"folders.permissions:write": "folders:uid:",
}
)
// ValidatePluginPermissions errors when a permission does not match expected pattern for plugins
func ValidatePluginPermissions(pluginID string, permissions []ac.Permission) error {
for i := range permissions {
if permissions[i].Action != pluginaccesscontrol.ActionAppAccess &&
!strings.HasPrefix(permissions[i].Action, pluginID+":") &&
scopePrefix, isCore := allowedCoreActions[permissions[i].Action]
if isCore {
if permissions[i].Scope != scopePrefix+pluginID {
return &ac.ErrorScopeTarget{Action: permissions[i].Action, Scope: permissions[i].Scope,
ExpectedScope: scopePrefix + pluginID}
}
// Prevent any unlikely injection
permissions[i].Scope = scopePrefix + pluginID
continue
}
if !strings.HasPrefix(permissions[i].Action, pluginID+":") &&
!strings.HasPrefix(permissions[i].Action, pluginID+".") {
return &ac.ErrorActionPrefixMissing{Action: permissions[i].Action,
Prefixes: []string{pluginaccesscontrol.ActionAppAccess, pluginID + ":", pluginID + "."}}
}
if strings.HasPrefix(permissions[i].Action, pluginaccesscontrol.ActionAppAccess) &&
permissions[i].Scope != pluginaccesscontrol.ScopeProvider.GetResourceScope(pluginID) {
return &ac.ErrorScopeTarget{Action: permissions[i].Action, Scope: permissions[i].Scope,
ExpectedScope: pluginaccesscontrol.ScopeProvider.GetResourceScope(pluginID)}
}
}
return nil

View File

@ -149,6 +149,29 @@ func TestValidatePluginRole(t *testing.T) {
},
wantErr: &ac.ErrorInvalidRole{},
},
{
name: "valid core permission targets plugin",
pluginID: "test-app",
role: ac.RoleDTO{
Name: "plugins:test-app:reader",
DisplayName: "Plugin Folder Reader",
Permissions: []ac.Permission{
{Action: "folders:read", Scope: "folders:uid:test-app"},
},
},
},
{
name: "invalid core permission targets other plugin",
pluginID: "test-app",
role: ac.RoleDTO{
Name: "plugins:test-app:reader",
DisplayName: "Plugin Folder Reader",
Permissions: []ac.Permission{
{Action: "folders:read", Scope: "folders:uid:other-app"},
},
},
wantErr: &ac.ErrorInvalidRole{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/query/client"
@ -75,9 +74,6 @@ func (p *DummyAPIFactory) MakeAPIServer(_ context.Context, tracer tracing.Tracer
}
switch gv.Group {
case "example.grafana.app":
return example.NewTestingAPIBuilder(), nil
// Only works with testdata
case "query.grafana.app":
return query.NewQueryAPIBuilder(

View File

@ -535,7 +535,7 @@ func (cma *CloudMigrationAPI) UploadSnapshot(c *contextmodel.ReqContext) respons
// 403: forbiddenError
// 500: internalServerError
func (cma *CloudMigrationAPI) CancelSnapshot(c *contextmodel.ReqContext) response.Response {
_, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.CancelSnapshot")
ctx, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.CancelSnapshot")
defer span.End()
sessUid, snapshotUid := web.Params(c.Req)[":uid"], web.Params(c.Req)[":snapshotUid"]
@ -546,7 +546,9 @@ func (cma *CloudMigrationAPI) CancelSnapshot(c *contextmodel.ReqContext) respons
return response.ErrOrFallback(http.StatusBadRequest, "invalid snapshot uid", err)
}
// Implement
if err := cma.cloudMigrationService.CancelSnapshot(ctx, sessUid, snapshotUid); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "error canceling snapshot", err)
}
return response.JSON(http.StatusOK, nil)
}

View File

@ -421,6 +421,240 @@ func TestCloudMigrationAPI_DeleteMigration(t *testing.T) {
}
}
func TestCloudMigrationAPI_CreateSnapshot(t *testing.T) {
tests := []TestCase{
{
desc: "should return 200 if everything is ok",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
basicRole: org.RoleAdmin,
expectedHttpResult: http.StatusOK,
expectedBody: `{"uid":"fake_uid"}`,
},
{
desc: "should return 403 if no used is not admin",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
basicRole: org.RoleEditor,
expectedHttpResult: http.StatusForbidden,
expectedBody: "",
},
{
desc: "should return 500 if service returns an error",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusInternalServerError,
expectedBody: "",
},
{
desc: "should return 400 if uid is invalid",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/***/snapshot",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.desc, runSimpleApiTest(tt))
}
}
func TestCloudMigrationAPI_GetSnapshot(t *testing.T) {
tests := []TestCase{
{
desc: "should return 200 if everything is ok",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1",
basicRole: org.RoleAdmin,
expectedHttpResult: http.StatusOK,
expectedBody: `{"uid":"fake_uid","status":"UNKNOWN","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z","results":[]}`,
},
{
desc: "should return 403 if no used is not admin",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1",
basicRole: org.RoleEditor,
expectedHttpResult: http.StatusForbidden,
expectedBody: "",
},
{
desc: "should return 500 if service returns an error",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusInternalServerError,
expectedBody: "",
},
{
desc: "should return 400 if uid is invalid",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/***/snapshot/1",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
{
desc: "should return 400 if snapshot_uid is invalid",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/***",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.desc, runSimpleApiTest(tt))
}
}
func TestCloudMigrationAPI_GetSnapshotList(t *testing.T) {
tests := []TestCase{
{
desc: "should return 200 if everything is ok",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshots",
basicRole: org.RoleAdmin,
expectedHttpResult: http.StatusOK,
expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"UNKNOWN","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"UNKNOWN","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z"}]}`,
},
{
desc: "should return 403 if no used is not admin",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshots",
basicRole: org.RoleEditor,
expectedHttpResult: http.StatusForbidden,
expectedBody: "",
},
{
desc: "should return 500 if service returns an error",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/1234/snapshots",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusInternalServerError,
expectedBody: "",
},
{
desc: "should return 400 if uid is invalid",
requestHttpMethod: http.MethodGet,
requestUrl: "/api/cloudmigration/migration/***/snapshots",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.desc, runSimpleApiTest(tt))
}
}
func TestCloudMigrationAPI_UploadSnapshot(t *testing.T) {
tests := []TestCase{
{
desc: "should return 200 if everything is ok",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1/upload",
basicRole: org.RoleAdmin,
expectedHttpResult: http.StatusOK,
expectedBody: "",
},
{
desc: "should return 403 if no used is not admin",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1/upload",
basicRole: org.RoleEditor,
expectedHttpResult: http.StatusForbidden,
expectedBody: "",
},
{
desc: "should return 500 if service returns an error",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1/upload",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusInternalServerError,
expectedBody: "",
},
{
desc: "should return 400 if uid is invalid",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/***/snapshot/1/upload",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
{
desc: "should return 400 if snapshot_uid is invalid",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/***/upload",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.desc, runSimpleApiTest(tt))
}
}
func TestCloudMigrationAPI_CancelSnapshot(t *testing.T) {
tests := []TestCase{
{
desc: "should return 200 if everything is ok",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1/cancel",
basicRole: org.RoleAdmin,
expectedHttpResult: http.StatusOK,
expectedBody: "",
},
{
desc: "should return 403 if no used is not admin",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1/cancel",
basicRole: org.RoleEditor,
expectedHttpResult: http.StatusForbidden,
expectedBody: "",
},
{
desc: "should return 500 if service returns an error",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/1/cancel",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusInternalServerError,
expectedBody: "",
},
{
desc: "should return 400 if uid is invalid",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/***/snapshot/1/cancel",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
{
desc: "should return 400 if snapshot_uid is invalid",
requestHttpMethod: http.MethodPost,
requestUrl: "/api/cloudmigration/migration/1234/snapshot/***/cancel",
basicRole: org.RoleAdmin,
serviceReturnError: true,
expectedHttpResult: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.desc, runSimpleApiTest(tt))
}
}
func runSimpleApiTest(tt TestCase) func(t *testing.T) {
return func(t *testing.T) {
// setup server

View File

@ -28,4 +28,5 @@ type Service interface {
GetSnapshot(ctx context.Context, sessionUid string, snapshotUid string) (*CloudMigrationSnapshot, error)
GetSnapshotList(ctx context.Context, query ListSnapshotsQuery) ([]CloudMigrationSnapshot, error)
UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error
CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error
}

View File

@ -582,6 +582,10 @@ func (s *Service) UploadSnapshot(ctx context.Context, sessionUid string, snapsho
return nil
}
func (s *Service) CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error {
panic("not implemented")
}
func (s *Service) parseCloudMigrationConfig() (string, error) {
if s.cfg == nil {
return "", fmt.Errorf("cfg cannot be nil")

View File

@ -75,3 +75,7 @@ func (s *NoopServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrat
func (s *NoopServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error {
return cloudmigration.ErrFeatureDisabledError
}
func (s *NoopServiceImpl) CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error {
return cloudmigration.ErrFeatureDisabledError
}

View File

@ -109,6 +109,63 @@ func Test_CreateGetRunMigrationsAndRuns(t *testing.T) {
require.NotNil(t, createResp.UID, delMigResp.UID)
}
func Test_ExecuteAsyncWorkflow(t *testing.T) {
s := setUpServiceTest(t, false)
createTokenResp, err := s.CreateToken(context.Background())
assert.NoError(t, err)
assert.NotEmpty(t, createTokenResp.Token)
cmd := cloudmigration.CloudMigrationSessionRequest{
AuthToken: createTokenResp.Token,
}
createResp, err := s.CreateSession(context.Background(), cmd)
require.NoError(t, err)
require.NotEmpty(t, createResp.UID)
require.NotEmpty(t, createResp.Slug)
getSessionResp, err := s.GetSession(context.Background(), createResp.UID)
require.NoError(t, err)
require.NotNil(t, getSessionResp)
require.Equal(t, createResp.UID, getSessionResp.UID)
require.Equal(t, createResp.Slug, getSessionResp.Slug)
listResp, err := s.GetSessionList(context.Background())
require.NoError(t, err)
require.NotNil(t, listResp)
require.Equal(t, 1, len(listResp.Sessions))
require.Equal(t, createResp.UID, listResp.Sessions[0].UID)
require.Equal(t, createResp.Slug, listResp.Sessions[0].Slug)
sessionUid := createResp.UID
snapshotResp, err := s.CreateSnapshot(ctxWithSignedInUser(), sessionUid)
require.NoError(t, err)
require.NotEmpty(t, snapshotResp.UID)
require.Equal(t, sessionUid, snapshotResp.SessionUID)
snapshotUid := snapshotResp.UID
snapshot, err := s.GetSnapshot(ctxWithSignedInUser(), sessionUid, snapshotUid)
require.NoError(t, err)
assert.Equal(t, snapshotResp.UID, snapshot.UID)
assert.Equal(t, snapshotResp.EncryptionKey, snapshot.EncryptionKey)
assert.Empty(t, snapshot.Result) // will change once we create a new table for migration items
snapshots, err := s.GetSnapshotList(ctxWithSignedInUser(), cloudmigration.ListSnapshotsQuery{SessionUID: sessionUid, Limit: 100})
require.NoError(t, err)
assert.Len(t, snapshots, 1)
assert.Equal(t, snapshotResp.UID, snapshots[0].UID)
assert.Equal(t, snapshotResp.EncryptionKey, snapshots[0].EncryptionKey)
assert.Empty(t, snapshots[0].Result) // should remain this way even after we create a new table
err = s.UploadSnapshot(ctxWithSignedInUser(), sessionUid, snapshotUid)
require.NoError(t, err)
assert.Panics(t, func() {
err = s.CancelSnapshot(ctxWithSignedInUser(), sessionUid, snapshotUid)
})
}
func ctxWithSignedInUser() context.Context {
c := &contextmodel.ReqContext{
SignedInUser: &user.SignedInUser{OrgID: 1},

View File

@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/services/gcom"
"github.com/grafana/grafana/pkg/util"
)
var fixedDate = time.Date(2024, 6, 5, 17, 30, 40, 0, time.UTC)
@ -140,7 +139,7 @@ func (m FakeServiceImpl) CreateSnapshot(ctx context.Context, sessionUid string)
return nil, fmt.Errorf("mock error")
}
return &cloudmigration.CloudMigrationSnapshot{
UID: util.GenerateShortUID(),
UID: "fake_uid",
SessionUID: sessionUid,
Status: cloudmigration.SnapshotStatusUnknown,
}, nil
@ -151,8 +150,8 @@ func (m FakeServiceImpl) GetSnapshot(ctx context.Context, sessionUid string, sna
return nil, fmt.Errorf("mock error")
}
return &cloudmigration.CloudMigrationSnapshot{
UID: util.GenerateShortUID(),
SessionUID: sessionUid,
UID: "fake_uid",
SessionUID: "fake_uid",
Status: cloudmigration.SnapshotStatusUnknown,
}, nil
}
@ -163,12 +162,12 @@ func (m FakeServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrati
}
return []cloudmigration.CloudMigrationSnapshot{
{
UID: util.GenerateShortUID(),
UID: "fake_uid",
SessionUID: query.SessionUID,
Status: cloudmigration.SnapshotStatusUnknown,
},
{
UID: util.GenerateShortUID(),
UID: "fake_uid",
SessionUID: query.SessionUID,
Status: cloudmigration.SnapshotStatusUnknown,
},
@ -181,3 +180,10 @@ func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string,
}
return nil
}
func (m FakeServiceImpl) CancelSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error {
if m.ReturnError {
return fmt.Errorf("mock error")
}
return nil
}

View File

@ -233,17 +233,23 @@ func (ss *sqlStore) GetSnapshotByUID(ctx context.Context, uid string) (*cloudmig
}
func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.ListSnapshotsQuery) ([]cloudmigration.CloudMigrationSnapshot, error) {
var runs = make([]cloudmigration.CloudMigrationSnapshot, 0)
var snapshots = make([]cloudmigration.CloudMigrationSnapshot, 0)
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
sess.Limit(query.Limit, query.Offset)
return sess.Find(&runs, &cloudmigration.CloudMigrationSnapshot{
return sess.Find(&snapshots, &cloudmigration.CloudMigrationSnapshot{
SessionUID: query.SessionUID,
})
})
if err != nil {
return nil, err
}
return runs, nil
for i, snapshot := range snapshots {
if err := ss.decryptKey(ctx, &snapshot); err != nil {
return nil, err
}
snapshots[i] = snapshot
}
return snapshots, nil
}
func (ss *sqlStore) encryptToken(ctx context.Context, cm *cloudmigration.CloudMigrationSession) error {

View File

@ -11,6 +11,7 @@ import (
fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/require"
)
@ -18,7 +19,7 @@ func TestMain(m *testing.M) {
testsuite.Run(m)
}
func Test_GetAllCloudMigrations(t *testing.T) {
func Test_GetAllCloudMigrationSessions(t *testing.T) {
_, s := setUpTest(t)
ctx := context.Background()
@ -44,11 +45,11 @@ func Test_GetAllCloudMigrations(t *testing.T) {
})
}
func Test_CreateMigration(t *testing.T) {
func Test_CreateMigrationSession(t *testing.T) {
_, s := setUpTest(t)
ctx := context.Background()
t.Run("creates migrations and reads it from the db", func(t *testing.T) {
t.Run("creates a session and reads it from the db", func(t *testing.T) {
cm := cloudmigration.CloudMigrationSession{
AuthToken: encodeToken("token"),
Slug: "fake_stack",
@ -56,15 +57,15 @@ func Test_CreateMigration(t *testing.T) {
RegionSlug: "fake_slug",
ClusterSlug: "fake_cluster_slug",
}
mig, err := s.CreateMigrationSession(ctx, cm)
sess, err := s.CreateMigrationSession(ctx, cm)
require.NoError(t, err)
require.NotEmpty(t, mig.ID)
require.NotEmpty(t, mig.UID)
require.NotEmpty(t, sess.ID)
require.NotEmpty(t, sess.UID)
getRes, err := s.GetMigrationSessionByUID(ctx, mig.UID)
getRes, err := s.GetMigrationSessionByUID(ctx, sess.UID)
require.NoError(t, err)
require.Equal(t, mig.ID, getRes.ID)
require.Equal(t, mig.UID, getRes.UID)
require.Equal(t, sess.ID, getRes.ID)
require.Equal(t, sess.UID, getRes.UID)
require.Equal(t, cm.AuthToken, getRes.AuthToken)
require.Equal(t, cm.Slug, getRes.Slug)
require.Equal(t, cm.StackID, getRes.StackID)
@ -73,7 +74,7 @@ func Test_CreateMigration(t *testing.T) {
})
}
func Test_GetMigrationByUID(t *testing.T) {
func Test_GetMigrationSessionByUID(t *testing.T) {
_, s := setUpTest(t)
ctx := context.Background()
t.Run("find session by uid", func(t *testing.T) {
@ -89,7 +90,7 @@ func Test_GetMigrationByUID(t *testing.T) {
})
}
func Test_DeleteMigration(t *testing.T) {
func Test_DeleteMigrationSession(t *testing.T) {
_, s := setUpTest(t)
ctx := context.Background()
@ -161,6 +162,46 @@ func Test_GetMigrationStatusList(t *testing.T) {
})
}
func Test_SnapshotManagement(t *testing.T) {
_, s := setUpTest(t)
ctx := context.Background()
t.Run("tests the snapshot lifecycle", func(t *testing.T) {
var snapshotUid string
sessionUid := util.GenerateShortUID()
// create a snapshot
cmr := cloudmigration.CloudMigrationSnapshot{
SessionUID: sessionUid,
Status: "initializing",
}
snapshotUid, err := s.CreateSnapshot(ctx, cmr)
require.NoError(t, err)
require.NotEmpty(t, snapshotUid)
//retrieve it from the db
snapshot, err := s.GetSnapshotByUID(ctx, snapshotUid)
require.NoError(t, err)
require.Equal(t, cloudmigration.SnapshotStatusInitializing, string(snapshot.Status))
// update its status
err = s.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{UID: snapshotUid, Status: cloudmigration.SnapshotStatusCreating})
require.NoError(t, err)
//retrieve it again
snapshot, err = s.GetSnapshotByUID(ctx, snapshotUid)
require.NoError(t, err)
require.Equal(t, cloudmigration.SnapshotStatusCreating, string(snapshot.Status))
// lists snapshots and ensures it's in there
snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUid, Offset: 0, Limit: 100})
require.NoError(t, err)
require.Len(t, snapshots, 1)
require.Equal(t, *snapshot, snapshots[0])
})
}
func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) {
testDB := db.InitTestDB(t)
s := &sqlStore{

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"fmt"
"sync"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@ -108,6 +109,27 @@ func (s *LDAPImpl) Reload(ctx context.Context, settings models.SSOSettings) erro
}
func (s *LDAPImpl) Validate(ctx context.Context, settings models.SSOSettings, oldSettings models.SSOSettings, requester identity.Requester) error {
ldapCfg, err := resolveServerConfig(settings.Settings["config"])
if err != nil {
return err
}
enabled := resolveBool(settings.Settings["enabled"], false)
if !enabled {
return nil
}
if len(ldapCfg.Servers) == 0 {
return fmt.Errorf("no servers configured for LDAP")
}
for i, server := range ldapCfg.Servers {
// host is required for every LDAP server config
if server.Host == "" {
return fmt.Errorf("no host configured for server with index %d", i)
}
}
return nil
}

View File

@ -297,3 +297,114 @@ func TestReload(t *testing.T) {
})
}
}
func TestValidate(t *testing.T) {
testCases := []struct {
description string
settings models.SSOSettings
isValid bool
containsError string
}{
{
description: "successfully validate basic settings",
settings: models.SSOSettings{
Provider: "ldap",
Settings: map[string]any{
"enabled": true,
"config": map[string]any{
"servers": []any{
map[string]any{
"host": "127.0.0.1",
},
},
},
},
},
isValid: true,
},
{
description: "successfully validate settings that are not enabled",
settings: models.SSOSettings{
Provider: "ldap",
Settings: map[string]any{
"enabled": false,
"config": map[string]any{
"servers": []any{
map[string]any{
"port": 123,
},
},
},
},
},
isValid: true,
},
{
description: "validation fails for invalid settings",
settings: models.SSOSettings{
Provider: "ldap",
Settings: map[string]any{
"enabled": true,
"config": map[string]any{
"servers": "invalid server config",
},
},
},
isValid: false,
containsError: "cannot unmarshal",
},
{
description: "validation fails when no servers are configured",
settings: models.SSOSettings{
Provider: "ldap",
Settings: map[string]any{
"enabled": true,
"config": map[string]any{
"servers": []any{},
},
},
},
isValid: false,
containsError: "no servers configured",
},
{
description: "validation fails if one server does not have a host configured",
settings: models.SSOSettings{
Provider: "ldap",
Settings: map[string]any{
"enabled": true,
"config": map[string]any{
"servers": []any{
map[string]any{
"host": "127.0.0.1",
},
map[string]any{
"port": 123,
},
},
},
},
},
isValid: false,
containsError: "no host configured",
},
}
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
ldapImpl := &LDAPImpl{
features: featuremgmt.WithManager(featuremgmt.FlagSsoSettingsApi),
loadingMutex: &sync.Mutex{},
}
err := ldapImpl.Validate(context.Background(), tt.settings, models.SSOSettings{}, nil)
if tt.isValid {
require.NoError(t, err)
} else {
require.Error(t, err)
require.ErrorContains(t, err, tt.containsError)
}
})
}
}

View File

@ -6,9 +6,10 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"golang.org/x/exp/slices"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
)
var (
@ -106,12 +107,13 @@ type LockCfg struct {
type dialectFunc func() Dialect
var supportedDialects = map[string]dialectFunc{
MySQL: NewMysqlDialect,
SQLite: NewSQLite3Dialect,
Postgres: NewPostgresDialect,
MySQL + "WithHooks": NewMysqlDialect,
SQLite + "WithHooks": NewSQLite3Dialect,
Postgres + "WithHooks": NewPostgresDialect,
MySQL: NewMysqlDialect,
SQLite: NewSQLite3Dialect,
Postgres: NewPostgresDialect,
MySQL + "WithHooks": NewMysqlDialect,
MySQL + "ReplicaWithHooks": NewMysqlDialect,
SQLite + "WithHooks": NewSQLite3Dialect,
Postgres + "WithHooks": NewPostgresDialect,
}
func NewDialect(driverName string) Dialect {

View File

@ -1,176 +0,0 @@
package playlist
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationExampleApp(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental APIs
DisableAnonymous: true,
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
},
})
t.Run("Check runtime info resource", func(t *testing.T) {
// Resource is not namespaced!
client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{
Group: "example.grafana.app",
Version: "v0alpha1",
Resource: "runtime",
})
rsp, err := client.List(context.Background(), metav1.ListOptions{})
require.NoError(t, err)
v, ok := rsp.Object["startupTime"].(int64)
require.True(t, ok)
require.Greater(t, v, time.Now().Add(-1*time.Hour).UnixMilli()) // should be within the last hour
})
t.Run("Check discovery client", func(t *testing.T) {
disco := helper.NewDiscoveryClient()
resources, err := disco.ServerResourcesForGroupVersion("example.grafana.app/v0alpha1")
require.NoError(t, err)
v1Disco, err := json.MarshalIndent(resources, "", " ")
require.NoError(t, err)
//fmt.Printf("%s", string(v1Disco))
require.JSONEq(t, `{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "example.grafana.app/v0alpha1",
"resources": [
{
"name": "dummy",
"singularName": "dummy",
"namespaced": true,
"kind": "DummyResource",
"verbs": [
"get",
"list"
]
},
{
"name": "dummy/sub",
"singularName": "",
"namespaced": true,
"kind": "DummySubresource",
"verbs": [
"get"
]
},
{
"name": "runtime",
"singularName": "runtime",
"namespaced": false,
"kind": "RuntimeInfo",
"verbs": [
"list"
]
}
]
}`, string(v1Disco))
//fmt.Printf("%s", string(v1Disco))
require.JSONEq(t, `[
{
"version": "v0alpha1",
"freshness": "Current",
"resources": [
{
"resource": "dummy",
"responseKind": {
"group": "",
"kind": "DummyResource",
"version": ""
},
"scope": "Namespaced",
"singularResource": "dummy",
"subresources": [
{
"responseKind": {
"group": "",
"kind": "DummySubresource",
"version": ""
},
"subresource": "sub",
"verbs": [
"get"
]
}
],
"verbs": [
"get",
"list"
]
},
{
"resource": "runtime",
"responseKind": {
"group": "",
"kind": "RuntimeInfo",
"version": ""
},
"scope": "Cluster",
"singularResource": "runtime",
"verbs": [
"list"
]
}
]
}
]`, helper.GetGroupVersionInfoJSON("example.grafana.app"))
})
t.Run("Check dummy with subresource", func(t *testing.T) {
client := helper.Org1.Viewer.ResourceClient(t, schema.GroupVersionResource{
Group: "example.grafana.app",
Version: "v0alpha1",
Resource: "dummy",
}).Namespace("default")
rsp, err := client.Get(context.Background(), "test2", metav1.GetOptions{})
require.NoError(t, err)
v, ok, err := unstructured.NestedString(rsp.Object, "spec", "Dummy")
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, "test2", v)
require.Equal(t, "DummyResource", rsp.GetObjectKind().GroupVersionKind().Kind)
// Now a sub-resource
rsp, err = client.Get(context.Background(), "test2", metav1.GetOptions{}, "sub")
require.NoError(t, err)
raw, err := json.MarshalIndent(rsp, "", " ")
require.NoError(t, err)
//fmt.Printf("%s", string(raw))
require.JSONEq(t, `{
"apiVersion": "example.grafana.app/v0alpha1",
"kind": "DummySubresource",
"info": "default/viewer-1"
}`, string(raw))
})
}

View File

@ -0,0 +1,23 @@
import * as createDetectChangesWorker from 'app/features/dashboard-scene/saving/createDetectChangesWorker';
import { DashboardSceneChangeTracker } from './DashboardSceneChangeTracker';
describe('DashboardSceneChangeTracker', () => {
it('should set _changesWorker to undefined when terminate is called', () => {
const terminate = jest.fn();
jest.spyOn(createDetectChangesWorker, 'createWorker').mockImplementation(
() =>
({
terminate,
}) as any
);
const changeTracker = new DashboardSceneChangeTracker({
subscribeToEvent: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
} as any);
changeTracker.startTrackingChanges();
expect(changeTracker['_changesWorker']).not.toBeUndefined();
changeTracker.terminate();
expect(changeTracker['_changesWorker']).toBeUndefined();
});
});

View File

@ -188,5 +188,6 @@ export class DashboardSceneChangeTracker {
public terminate() {
this.stopTrackingChanges();
this._changesWorker?.terminate();
this._changesWorker = undefined;
}
}

View File

@ -24,7 +24,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const pageNav = model.getPageNav(location, navIndex);
const bodyToRender = model.getBodyToRender();
const navModel = getNavModel(navIndex, 'dashboards/browse');
const isHomePage = !meta.url && !meta.slug && !meta.isNew && !meta.isSnapshot;
const hasControls = controls?.hasControls();
if (editview) {
@ -84,7 +83,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
className={styles.scrollbarContainer}
testId={selectors.pages.Dashboard.DashNav.scrollContainer}
>
<div className={cx(styles.canvasContent, isHomePage && styles.homePagePadding)}>{body}</div>
<div className={cx(styles.canvasContent)}>{body}</div>
</CustomScrollbar>
</div>
)}
@ -137,9 +136,6 @@ function getStyles(theme: GrafanaTheme2) {
controlsWrapperWithScopes: css({
padding: theme.spacing(2, 2, 2, 0),
}),
homePagePadding: css({
padding: theme.spacing(2, 2),
}),
canvasContent: css({
label: 'canvas-content',
display: 'flex',

View File

@ -1,16 +1,18 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { createTheme } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { PanelModel } from '../../state';
import PanelHeaderCorner, { Props } from './PanelHeaderCorner';
import { PanelHeaderCorner, Props } from './PanelHeaderCorner';
const setup = () => {
const testPanel = new PanelModel({ title: 'test', description: 'test panel' });
const props: Props = {
panel: testPanel,
theme: createTheme(),
};
return render(<PanelHeaderCorner {...props} />);
};

View File

@ -5,7 +5,7 @@ import { renderMarkdown, LinkModelSupplier, ScopedVars, IconName } from '@grafan
import { GrafanaTheme2 } from '@grafana/data/';
import { selectors } from '@grafana/e2e-selectors';
import { locationService, getTemplateSrv } from '@grafana/runtime';
import { Tooltip, PopoverContent, Icon } from '@grafana/ui';
import { Tooltip, PopoverContent, Icon, Themeable2, withTheme2 } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui/';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
@ -17,7 +17,7 @@ enum InfoMode {
Links = 'Links',
}
export interface Props {
export interface Props extends Themeable2 {
panel: PanelModel;
title?: string;
description?: string;
@ -45,22 +45,23 @@ export class PanelHeaderCorner extends Component<Props> {
};
getInfoContent = (): JSX.Element => {
const { panel } = this.props;
const { panel, theme } = this.props;
const markdown = panel.description || '';
const interpolatedMarkdown = getTemplateSrv().replace(markdown, panel.scopedVars);
const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown);
const links = this.props.links && this.props.links.getLinks(panel.replaceVariables);
const styles = getContentStyles(theme);
return (
<div className="panel-info-content markdown-html">
<div className={styles.content}>
<div dangerouslySetInnerHTML={{ __html: markedInterpolatedMarkdown }} />
{links && links.length > 0 && (
<ul className="panel-info-corner-links">
<ul className={styles.cornerLinks}>
{links.map((link, idx) => {
return (
<li key={idx}>
<a className="panel-info-corner-links__item" href={link.href} target={link.target}>
<a href={link.href} target={link.target}>
{link.title}
</a>
</li>
@ -102,7 +103,7 @@ export class PanelHeaderCorner extends Component<Props> {
}
}
export default PanelHeaderCorner;
export default withTheme2(PanelHeaderCorner);
interface PanelInfoCornerProps {
infoMode: InfoMode;
@ -135,6 +136,25 @@ const iconMap: Record<InfoMode, IconName> = {
[InfoMode.Links]: 'external-link-alt',
};
const getContentStyles = (theme: GrafanaTheme2) => ({
content: css({
overflow: 'auto',
code: {
whiteSpace: 'normal',
wordWrap: 'break-word',
},
'pre > code': {
display: 'block',
},
}),
cornerLinks: css({
listStyle: 'none',
paddingLeft: 0,
}),
});
const getStyles = (theme: GrafanaTheme2) => {
return {
icon: css({

View File

@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React from 'react';
import { DataSourcePluginMeta } from '@grafana/data';
import { LinkButton } from '@grafana/ui';
import { DataSourcePluginMeta, GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { DataSourcePluginCategory } from 'app/types';
import { ROUTES } from '../../connections/constants';
@ -18,13 +19,14 @@ export type Props = {
export function DataSourceCategories({ categories, onClickDataSourceType }: Props) {
const moreDataSourcesLink = `${ROUTES.AddNewConnection}?cat=data-source`;
const styles = useStyles2(getStyles);
return (
<>
{/* Categories */}
{categories.map(({ id, title, plugins }) => (
<div className="add-data-source-category" key={id}>
<div className="add-data-source-category__header" id={id}>
<div className={styles.category} key={id}>
<div className={styles.header} id={id}>
{title}
</div>
<DataSourceTypeCardList dataSourcePlugins={plugins} onClickDataSourceType={onClickDataSourceType} />
@ -32,7 +34,7 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop
))}
{/* Find more */}
<div className="add-data-source-more">
<div className={styles.more}>
<LinkButton variant="secondary" href={moreDataSourcesLink} target="_self" rel="noopener">
Find more data source plugins
</LinkButton>
@ -40,3 +42,17 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop
</>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
category: css({
marginBottom: theme.spacing(2),
}),
header: css({
fontSize: theme.typography.h5.fontSize,
marginBottom: theme.spacing(1),
}),
more: css({
margin: theme.spacing(4),
textAlign: 'center',
}),
});

View File

@ -162,7 +162,7 @@ async function doImportPluginModuleInSandbox(meta: SandboxPluginMeta): Promise<S
}
try {
const resolvedDeps = resolvePluginDependencies(dependencies, meta.id);
const resolvedDeps = resolvePluginDependencies(dependencies, meta);
// execute the plugin's code
const pluginExportsRaw = factory.apply(null, resolvedDeps);
// only after the plugin has been executed
@ -213,7 +213,21 @@ async function doImportPluginModuleInSandbox(meta: SandboxPluginMeta): Promise<S
});
}
function resolvePluginDependencies(deps: string[], pluginId: string) {
/**
*
* This function resolves the dependencies using the array of AMD deps.
* Additionally it supports the RequireJS magic modules `module` and `exports`.
* https://github.com/requirejs/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#magic
*
*/
function resolvePluginDependencies(deps: string[], pluginMeta: SandboxPluginMeta) {
const pluginExports = {};
const pluginModuleDep: ModuleMeta = {
id: pluginMeta.id,
uri: pluginMeta.module,
exports: pluginExports,
};
// resolve dependencies
const resolvedDeps: CompartmentDependencyModule[] = [];
for (const dep of deps) {
@ -222,10 +236,18 @@ function resolvePluginDependencies(deps: string[], pluginId: string) {
resolvedDep = resolvedDep.default;
}
if (dep === 'module') {
resolvedDep = pluginModuleDep;
}
if (dep === 'exports') {
resolvedDep = pluginExports;
}
if (!resolvedDep) {
const error = new Error(`[sandbox] Could not resolve dependency ${dep}`);
logError(error, {
pluginId,
pluginId: pluginMeta.id,
dependency: dep,
error: String(error),
});
@ -235,3 +257,9 @@ function resolvePluginDependencies(deps: string[], pluginId: string) {
}
return resolvedDeps;
}
interface ModuleMeta {
id: string;
uri: string;
exports: System.Module;
}

View File

@ -1,4 +1,5 @@
import { cx } from '@emotion/css';
import { intervalToDuration } from 'date-fns';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
@ -11,9 +12,10 @@ import {
getFieldDisplayName,
} from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Checkbox, Icon, IconName, TagList, Text } from '@grafana/ui';
import { Checkbox, Icon, IconName, TagList, Text, Tooltip } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
import { formatDate, formatDuration } from 'app/core/internationalization/dates';
import { PluginIconName } from 'app/features/plugins/admin/types';
import { ShowModalReactEvent } from 'app/types/events';
@ -25,6 +27,7 @@ import { ExplainScorePopup } from './ExplainScorePopup';
import { TableColumn } from './SearchResultsTable';
const TYPE_COLUMN_WIDTH = 175;
const DURATION_COLUMN_WIDTH = 200;
const DATASOURCE_COLUMN_WIDTH = 200;
export const generateColumns = (
@ -112,15 +115,20 @@ export const generateColumns = (
Cell: (p) => {
let classNames = cx(styles.nameCellStyle);
let name = access.name.values[p.row.index];
const isDeleted = access.isDeleted?.values[p.row.index];
if (!name?.length) {
const loading = p.row.index >= response.view.dataFrame.length;
name = loading ? 'Loading...' : 'Missing title'; // normal for panels
classNames += ' ' + styles.missingTitleText;
}
return (
<div className={styles.cell} {...p.cellProps}>
{!response.isItemLoaded(p.row.index) ? (
<Skeleton width={200} />
) : isDeleted ? (
<span className={classNames}>{name}</span>
) : (
<a href={p.userProps.href} onClick={p.userProps.onClick} className={classNames} title={name}>
{name}
@ -136,9 +144,18 @@ export const generateColumns = (
});
availableWidth -= width;
width = TYPE_COLUMN_WIDTH;
columns.push(makeTypeColumn(response, access.kind, access.panel_type, width, styles));
availableWidth -= width;
const showDeletedRemaining =
response.view.fields.permanentlyDeleteDate && hasValue(response.view.fields.permanentlyDeleteDate);
if (showDeletedRemaining && access.permanentlyDeleteDate) {
width = DURATION_COLUMN_WIDTH;
columns.push(makeDeletedRemainingColumn(response, access.permanentlyDeleteDate, width, styles));
availableWidth -= width;
} else {
width = TYPE_COLUMN_WIDTH;
columns.push(makeTypeColumn(response, access.kind, access.panel_type, width, styles));
availableWidth -= width;
}
// Show datasources if we have any
if (access.ds_uid && onDatasourceChange) {
@ -328,6 +345,46 @@ function makeDataSourceColumn(
};
}
function makeDeletedRemainingColumn(
response: QueryResponse,
deletedField: Field<Date | undefined>,
width: number,
styles: Record<string, string>
): TableColumn {
return {
id: 'column-delete-age',
field: deletedField,
width,
Header: t('search.results-table.deleted-remaining-header', 'Time remaining'),
Cell: (p) => {
const i = p.row.index;
const deletedDate = deletedField.values[i];
if (!deletedDate || !response.isItemLoaded(p.row.index)) {
return (
<div {...p.cellProps} className={cx(styles.cell, styles.typeCell)}>
<Skeleton width={100} />
</div>
);
}
const duration = calcCoarseDuration(new Date(), deletedDate);
const isDeletingSoon = !Object.values(duration).some((v) => v > 0);
const formatted = isDeletingSoon
? t('search.results-table.deleted-less-than-1-min', '< 1 min')
: formatDuration(duration, { style: 'long' });
return (
<div {...p.cellProps} className={cx(styles.cell, styles.typeCell)}>
<Tooltip content={formatDate(deletedDate, { dateStyle: 'medium', timeStyle: 'short' })}>
<span>{formatted}</span>
</Tooltip>
</div>
);
},
};
}
function makeTypeColumn(
response: QueryResponse,
kindField: Field<string>,
@ -442,3 +499,22 @@ function getDisplayValue({
}
return formattedValueToString(getDisplay(value));
}
/**
* Calculates the rough duration between two dates, keeping only the most significant unit
*/
function calcCoarseDuration(start: Date, end: Date) {
let { years = 0, months = 0, days = 0, hours = 0, minutes = 0 } = intervalToDuration({ start, end });
if (years > 0) {
return { years };
} else if (months > 0) {
return { months };
} else if (days > 0) {
return { days };
} else if (hours > 0) {
return { hours };
}
return { minutes };
}

View File

@ -158,6 +158,8 @@ export class SQLSearcher implements GrafanaSearcher {
const tags: string[][] = [];
const location: string[] = [];
const sortBy: number[] = [];
const isDeleted: boolean[] = [];
const permanentlyDeleteDate: Array<Date | undefined> = [];
let sortMetaName: string | undefined;
for (let hit of rsp) {
@ -168,6 +170,8 @@ export class SQLSearcher implements GrafanaSearcher {
url.push(hit.url);
tags.push(hit.tags);
sortBy.push(hit.sortMeta!);
isDeleted.push(hit.isDeleted ?? false);
permanentlyDeleteDate.push(hit.permanentlyDeleteDate ? new Date(hit.permanentlyDeleteDate) : undefined);
let v = hit.folderUid;
if (!v && k === 'dashboard') {
@ -204,6 +208,8 @@ export class SQLSearcher implements GrafanaSearcher {
{ name: 'url', type: FieldType.string, config: {}, values: url },
{ name: 'tags', type: FieldType.other, config: {}, values: tags },
{ name: 'location', type: FieldType.string, config: {}, values: location },
{ name: 'isDeleted', type: FieldType.boolean, config: {}, values: isDeleted },
{ name: 'permanentlyDeleteDate', type: FieldType.time, config: {}, values: permanentlyDeleteDate },
],
length: name.length,
meta: {

View File

@ -39,6 +39,8 @@ export interface DashboardQueryResult {
tags: string[];
location: string; // url that can be split
ds_uid: string[];
isDeleted?: boolean;
permanentlyDeleteDate?: Date;
// debugging fields
score: number;

View File

@ -30,6 +30,8 @@ export interface DashboardSearchHit extends WithAccessControlMetadata {
url: string;
sortMeta?: number;
sortMetaName?: string;
isDeleted?: boolean;
permanentlyDeleteDate?: string;
}
/**

View File

@ -1673,6 +1673,8 @@
},
"results-table": {
"datasource-header": "Data source",
"deleted-less-than-1-min": "< 1 min",
"deleted-remaining-header": "Time remaining",
"location-header": "Location",
"name-header": "Name",
"tags-header": "Tags",

View File

@ -1673,6 +1673,8 @@
},
"results-table": {
"datasource-header": "Đäŧä şőūřčę",
"deleted-less-than-1-min": "< 1 mįʼn",
"deleted-remaining-header": "Ŧįmę řęmäįʼnįʼnģ",
"location-header": "Ŀőčäŧįőʼn",
"name-header": "Ńämę",
"tags-header": "Ŧäģş",

View File

@ -1864,3 +1864,74 @@ $easing: cubic-bezier(0, 0, 0.265, 1);
background: transparent;
display: block;
}
.add-data-source-item {
padding: $space-md;
display: flex;
align-items: center;
cursor: pointer;
background: $card-background;
border-radius: 3px;
margin-bottom: $space-xxs;
&:hover {
background: $card-background-hover;
.add-data-source-item-actions {
opacity: 1;
transition: 0.15s opacity ease-in-out;
}
}
}
.add-data-source-item-text-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.add-data-source-item-logo {
margin-right: $space-lg;
margin-left: $space-sm;
width: 55px;
max-height: 55px;
}
.add-data-source-item-desc {
font-size: $font-size-sm;
color: $text-color-weak;
}
.add-data-source-item-actions {
opacity: 0;
padding-left: $space-md;
display: flex;
align-items: center;
> button {
margin-left: $space-md;
cursor: pointer;
}
}
.add-data-source-item-badge {
margin-top: 6px;
}
.add-data-source-item-text {
font-size: $font-size-h5;
}
.panel-empty {
display: flex;
align-items: center;
height: 100%;
width: 100%;
p {
text-align: center;
color: $text-muted;
font-size: $font-size-lg;
width: 100%;
}
}

View File

@ -24,15 +24,12 @@
@import 'components/tags';
@import 'components/gf-form';
@import 'components/filter-table';
@import 'components/slate_editor';
@import 'components/modals';
@import 'components/dropdown';
@import 'components/infobox';
@import 'components/query_editor';
@import 'components/query_part';
@import 'components/dashboard_grid';
@import 'components/add_data_source';
@import 'components/panel_header';
// PAGES
@import 'pages/dashboard';

View File

@ -1,86 +0,0 @@
.add-data-source-header {
margin-bottom: $space-xl;
padding-top: $spacer;
text-align: center;
}
.add-data-source-search {
display: flex;
justify-content: center;
margin-bottom: $space-lg;
}
.add-data-source-category {
margin-bottom: $space-md;
}
.add-data-source-category__header {
font-size: $font-size-h5;
margin-bottom: $space-sm;
}
.add-data-source-item {
padding: $space-md;
display: flex;
align-items: center;
cursor: pointer;
background: $card-background;
border-radius: 3px;
margin-bottom: $space-xxs;
&:hover {
background: $card-background-hover;
.add-data-source-item-actions {
opacity: 1;
transition: 0.15s opacity ease-in-out;
}
}
}
.add-data-source-item-text-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.add-data-source-item-desc {
font-size: $font-size-sm;
color: $text-color-weak;
}
.add-data-source-item-badge {
margin-top: 6px;
}
.add-data-source-item-text {
font-size: $font-size-h5;
}
.add-data-source-item-logo {
margin-right: $space-lg;
margin-left: $space-sm;
width: 55px;
max-height: 55px;
}
.add-data-source-item-actions {
opacity: 0;
padding-left: $space-md;
display: flex;
align-items: center;
> button {
margin-left: $space-md;
cursor: pointer;
}
}
.add-datasource-item-actions__btn-icon {
margin-left: $space-sm;
}
.add-data-source-more {
text-align: center;
margin: $space-xl;
}

View File

@ -1,122 +0,0 @@
.panel-header {
&:hover {
transition: background-color 0.1s ease-in-out;
background-color: $panel-header-hover-bg;
}
}
.panel-container--no-title {
.panel-header {
position: absolute;
left: min(50px, 10%); // allows space for interaction in the corders
right: min(50px, 10%);
z-index: $panel-header-z-index;
&:hover {
left: 0;
right: 0;
}
}
.panel-content {
height: 100%;
}
}
.panel-title-container {
cursor: move;
word-wrap: break-word;
display: block;
}
.panel-title {
border: 0px;
font-weight: $font-weight-semi-bold;
position: relative;
width: 100%;
display: flex;
flex-wrap: nowrap;
justify-content: center;
height: $panel-header-height;
line-height: $panel-header-height;
align-items: center;
}
.panel-menu-container {
width: 0px;
height: 19px;
display: inline-block;
}
.panel-menu-toggle {
position: absolute;
top: calc(50% - 9px);
color: $text-color-weak;
cursor: pointer;
margin: 2px 0 0 2px;
visibility: hidden;
opacity: 0;
&:hover {
color: $link-hover-color;
}
}
.panel-loading {
position: absolute;
top: 0px;
right: 4px;
z-index: $panel-header-z-index + 1;
font-size: $font-size-lg;
color: $text-color-weak;
&:hover {
cursor: pointer;
}
}
.panel-empty {
display: flex;
align-items: center;
height: 100%;
width: 100%;
p {
text-align: center;
color: $text-muted;
font-size: $font-size-lg;
width: 100%;
}
}
.panel-menu {
top: 25px;
left: -100px;
}
.panel-info-content {
overflow: auto;
code {
white-space: normal;
word-wrap: break-word;
}
pre > code {
display: block;
}
.panel-info-corner-links {
list-style: none;
padding-left: 0;
}
}
.panel-time-info {
font-weight: $font-weight-semi-bold;
float: right;
margin-right: 8px;
color: $blue;
font-size: 85%;
position: absolute;
right: 0;
}

View File

@ -1,165 +0,0 @@
.slate-query-field {
font-size: $font-size-base;
font-family: $font-family-monospace;
height: auto;
word-break: break-word;
// Affects only placeholder in query field. Adds scrollbar only if content is cropped.
overflow: auto;
}
.slate-query-field__wrapper {
position: relative;
display: inline-block;
padding: 6px 8px;
min-height: $input-height;
width: 100%;
color: $text-color;
background-color: $input-bg;
background-image: none;
border: $input-border;
border-radius: $border-radius;
transition: all 0.3s;
line-height: $input-line-height;
}
.slate-query-field__wrapper--disabled {
background-color: inherit;
cursor: not-allowed;
}
.slate-typeahead {
.typeahead {
position: relative;
z-index: $zindex-typeahead;
border-radius: $border-radius;
border: $panel-border;
max-height: calc(66vh);
overflow-y: scroll;
overflow-x: hidden;
outline: none;
list-style: none;
background: $panel-bg;
color: $text-color;
box-shadow: $typeahead-shadow;
}
.typeahead-group__title {
color: $text-color-weak;
font-size: $font-size-sm;
line-height: $line-height-base;
padding: $space-sm;
}
.typeahead-item {
height: auto;
font-family: $font-family-monospace;
padding: $space-sm $space-sm $space-sm $space-md;
font-size: $font-size-sm;
text-overflow: ellipsis;
overflow: hidden;
z-index: 1;
display: block;
white-space: nowrap;
cursor: pointer;
transition:
color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.typeahead-item__selected {
background-color: $typeahead-selected-bg;
.typeahead-item-hint {
font-size: $font-size-xs;
color: $text-color;
white-space: normal;
}
}
.typeahead-match {
color: $typeahead-selected-color;
border-bottom: 1px solid $typeahead-selected-color;
// Undoing mark styling
padding: inherit;
background: inherit;
}
}
/* SYNTAX */
.slate-query-field,
.prism-syntax-highlight {
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: $text-color-weak;
}
.token.variable,
.token.entity {
color: $text-color;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: $query-red;
}
.token.attr-value,
.token.selector,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: $query-green;
}
.token.boolean,
.token.number,
.token.operator,
.token.url {
color: $query-purple;
}
.token.function,
.token.attr-name,
.token.function-name,
.token.atrule,
.token.keyword,
.token.class-name {
color: $text-blue;
}
.token.punctuation,
.token.regex,
.token.important {
color: $query-orange;
}
.token.important {
font-weight: normal;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}
}

View File

@ -8000,13 +8000,6 @@ __metadata:
languageName: node
linkType: hard
"@types/add@npm:^2":
version: 2.0.3
resolution: "@types/add@npm:2.0.3"
checksum: 10/c15815ef0e4903113f7fda84b4c2704a1b694fd6f53a6e3c9327b5b7067aba676c75e696b950401de45be7646b37b7059303a7bb5c4363de3eb3e0c06d25f565
languageName: node
linkType: hard
"@types/angular-route@npm:1.7.6":
version: 1.7.6
resolution: "@types/angular-route@npm:1.7.6"
@ -17148,7 +17141,6 @@ __metadata:
"@testing-library/react": "npm:15.0.2"
"@testing-library/react-hooks": "npm:^8.0.1"
"@testing-library/user-event": "npm:14.5.2"
"@types/add": "npm:^2"
"@types/angular": "npm:1.8.9"
"@types/angular-route": "npm:1.7.6"
"@types/babel__core": "npm:^7"
@ -17408,7 +17400,6 @@ __metadata:
xlsx: "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
yaml: "npm:^2.0.0"
yargs: "npm:^17.5.1"
yarn: "npm:^1.22.22"
dependenciesMeta:
prettier@3.3.1:
unplugged: true
@ -31158,16 +31149,6 @@ __metadata:
languageName: node
linkType: hard
"yarn@npm:^1.22.22":
version: 1.22.22
resolution: "yarn@npm:1.22.22"
bin:
yarn: bin/yarn.js
yarnpkg: bin/yarn.js
checksum: 10/98d80230beaa81f186b2256dff5ef9dce2dd6073c94299209f8e562da9018cff4275c95c27c788aaa4a9c3c186ea8a9aee9a5b83570696a4c8a9d1fff2d4da3a
languageName: node
linkType: hard
"yauzl@npm:^2.10.0":
version: 2.10.0
resolution: "yauzl@npm:2.10.0"