diff --git a/.betterer.results b/.betterer.results index dd98080fa88..fbe38508f3d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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 ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "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 ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -4607,14 +4619,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "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 ", "0"] ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf7bc7537ad..e26d5baa7a2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/contribute/internationalization.md b/contribute/internationalization.md index 5ca730ab835..8729f8175f5 100644 --- a/contribute/internationalization.md +++ b/contribute/internationalization.md @@ -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 }) => Results for {{ term }}; +``` + +Prefer using `` 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 }) => {term}; + const SearchTitle = ({ term }) => ( - Results for {{ term }} + Results for ); ``` -Prefer using `` 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 }) => ( + + Results for {'{{ myVariable }}'} and this translated text is also in green + +); +``` When translating in `grafana-ui`, use a relative path to import `` and `t()` from `src/utils/i18n`. diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 3f75bca900a..1ec8a8a0b26 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -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
`alert.instances:create`
`alert.instances:write` for organization scope
`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
`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
`alert.notifications:write`for organization scope
`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
`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
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete`
`alert.silences:create`
`alert.silences:write` for scope `folders:*`
`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:*`
`alert.rules.external:read` for scope `datasources:*`
`alert.notifications.time-intervals:read`
`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`
`fixed:alerting.instances:writer`
`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`
`fixed:alerting.instances:reader`
`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`
`annotations.create`
`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`
`annotations:write`
`annotations.create`
`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
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | -| `fixed:authentication.config:writer` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | -| `fixed:dashboards:creator` | `dashboards:create`
`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
`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
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`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`
`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
`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`
`datasources:query` | Read and query data sources. | -| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`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
`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`
`dashboards:read` | Read all folders and dashboards. | -| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`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`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | -| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | -| `fixed:library.panels:creator` | `library.panels:create`
`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
`library.panels:create`
`library.panels:delete`
`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
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. | -| `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | -| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`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
`org.users:add`
`org.users:remove`
`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
`orgs:write`
`orgs:create`
`orgs:delete`
`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`
`orgs.quotas:read` | Read an organization and its quotas. | -| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`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`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | -| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | -| `fixed:roles:reader` | `roles:read`
`teams.roles:read`
`users.roles:read`
`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
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`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`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`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
`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`
`org.users:read` | Create a team and list organization users (required to manage the created team). | -| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | -| `fixed:users:reader` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | -| `fixed:users:writer` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`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 user’s 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`
`fixed:alerting.instances:reader`
`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`
`fixed:alerting.instances:writer`
`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
`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
`alert.instances:create`
`alert.instances:write` for organization scope
`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
`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
`alert.notifications:write`for organization scope
`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:*`
`alert.rules.external:read` for scope `datasources:*`
`alert.notifications.time-intervals:read`
`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
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete`
`alert.silences:create`
`alert.silences:write` for scope `folders:*`
`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`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | +| `fixed:annotations.dashboard:writer` | `fixed_8A775xenXeKaJk4Cr7bchP9yXOA` | `annotations:write`
`annotations.create`
`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
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | +| `fixed:authentication.config:writer` | `fixed_0rYhZ2Qnzs8AdB1nX7gexk3fHDw` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | +| `fixed:dashboards:creator` | `fixed_ZorKUcEPCM01A1fPakEzGBUyU64` | `dashboards:create`
`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
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`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
`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`
`datasources:query` | Read and query data sources. | +| `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`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`
`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
`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`
`dashboards:read` | Read all folders and dashboards. | +| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`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
`folders.permissions:write` | Read and update all folder permissions. | +| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`
`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
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | +| `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`
`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
`library.panels:create`
`library.panels:delete`
`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
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions. | +| `fixed:licensing:reader` | `fixed_OADpuXvNEylO2Kelu3GIuBXEAYE` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | +| `fixed:licensing:writer` | `fixed_gzbz3rJpQMdaKHt-E4q0PVaKMoE` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`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
`org.users:add`
`org.users:remove`
`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
`orgs:write`
`orgs:create`
`orgs:delete`
`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`
`orgs.quotas:read` | Read an organization and its quotas. | +| `fixed:organization:writer` | `fixed_Y4jGqDd8w1yCrPwlik8z5Iu8-3M` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`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`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | +| `fixed:reports:writer` | `fixed_jBW3_7g1EWOjGVBYeVRwtFxhUNw` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | +| `fixed:roles:reader` | `fixed_GkfG-1NSwEGb4hpK3-E3qHyNltc` | `roles:read`
`teams.roles:read`
`users.roles:read`
`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
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`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`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`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
`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`
`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`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | +| `fixed:users:reader` | `fixed_buZastUG3reWyQpPemcWjGqPAd0` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | 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
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`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 user’s authentication token, or update quotas for all users. | ### Alerting roles diff --git a/docs/sources/dashboards/assess-dashboard-usage/index.md b/docs/sources/dashboards/assess-dashboard-usage/index.md index 1be93c317ed..260ed8baaf0 100644 --- a/docs/sources/dashboards/assess-dashboard-usage/index.md +++ b/docs/sources/dashboards/assess-dashboard-usage/index.md @@ -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: diff --git a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md index 838b37fe6d2..c7862d79151 100644 --- a/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md +++ b/docs/sources/dashboards/build-dashboards/annotate-visualizations/index.md @@ -21,12 +21,12 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/ + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/ annotations-api: - pattern: /docs/grafana/ destination: /docs/grafana//developers/http_api/annotations/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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. - -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. - -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. diff --git a/docs/sources/dashboards/build-dashboards/create-dashboard-url-variables/index.md b/docs/sources/dashboards/build-dashboards/create-dashboard-url-variables/index.md index 5e8a7ae4002..85ccb5423b2 100644 --- a/docs/sources/dashboards/build-dashboards/create-dashboard-url-variables/index.md +++ b/docs/sources/dashboards/build-dashboards/create-dashboard-url-variables/index.md @@ -21,28 +21,28 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/add-template-variables/#add-ad-hoc-filters - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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//dashboards/build-dashboards/manage-dashboard-links/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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//dashboards/build-dashboards/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/build-dashboards/ + destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/ template-and-variables: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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 diff --git a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md index f5ff0f8781d..aacf8f483ef 100644 --- a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md +++ b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md @@ -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**. diff --git a/docs/sources/dashboards/build-dashboards/import-dashboards/index.md b/docs/sources/dashboards/build-dashboards/import-dashboards/index.md index 1c919212a2a..6378fe4288a 100644 --- a/docs/sources/dashboards/build-dashboards/import-dashboards/index.md +++ b/docs/sources/dashboards/build-dashboards/import-dashboards/index.md @@ -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 diff --git a/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md b/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md index 94e75533736..d0917600909 100644 --- a/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md +++ b/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md @@ -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. diff --git a/docs/sources/dashboards/build-dashboards/manage-version-history/index.md b/docs/sources/dashboards/build-dashboards/manage-version-history/index.md index ffd4fa349d2..d1ed28efc3f 100644 --- a/docs/sources/dashboards/build-dashboards/manage-version-history/index.md +++ b/docs/sources/dashboards/build-dashboards/manage-version-history/index.md @@ -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 \** 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. diff --git a/docs/sources/dashboards/build-dashboards/modify-dashboard-settings/index.md b/docs/sources/dashboards/build-dashboards/modify-dashboard-settings/index.md index c251a6063a6..a58f0332e50 100644 --- a/docs/sources/dashboards/build-dashboards/modify-dashboard-settings/index.md +++ b/docs/sources/dashboards/build-dashboards/modify-dashboard-settings/index.md @@ -19,17 +19,22 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/variables/ + destination: /docs/grafana-cloud/visualizations/dashboards/variables/ json-fields: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/build-dashboards/view-dashboard-json-model/#json-fields - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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//datasources/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/ + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/ + dashboard-links: + - pattern: /docs/grafana/ + destination: /docs/grafana//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). diff --git a/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md b/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md index e639d4d54a7..58190abe04e 100644 --- a/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md +++ b/docs/sources/dashboards/build-dashboards/view-dashboard-json-model/index.md @@ -21,7 +21,7 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/build-dashboards/annotate-visualizations/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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 diff --git a/docs/sources/dashboards/dashboard-public/index.md b/docs/sources/dashboards/dashboard-public/index.md index 252bdfba6d2..d4b69d5ed57 100644 --- a/docs/sources/dashboards/dashboard-public/index.md +++ b/docs/sources/dashboards/dashboard-public/index.md @@ -15,7 +15,7 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/share-dashboards-panels/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/share-dashboards-panels/ + destination: /docs/grafana-cloud/visualizations/dashboards/share-dashboards-panels/ custom-branding: - pattern: /docs/grafana/ destination: /docs/grafana//setup-grafana/configure-grafana/configure-custom-branding/ @@ -25,7 +25,7 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/assess-dashboard-usage/#dashboard-insights - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/assess-dashboard-usage/#dashboard-insights + destination: /docs/grafana-cloud/visualizations/dashboards/assess-dashboard-usage/ caching: - pattern: /docs/grafana/ destination: /docs/grafana//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). diff --git a/docs/sources/dashboards/variables/add-template-variables/index.md b/docs/sources/dashboards/variables/add-template-variables/index.md index b7769ef6509..21ae42d1e56 100644 --- a/docs/sources/dashboards/variables/add-template-variables/index.md +++ b/docs/sources/dashboards/variables/add-template-variables/index.md @@ -49,32 +49,32 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/add-template-variables/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/variables/add-template-variables/ + destination: /docs/grafana-cloud/visualizations/dashboards/variables/add-template-variables/ inspect: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/inspect-variable/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/variables/inspect-variable/ + destination: /docs/grafana-cloud/visualizations/dashboards/variables/inspect-variable/ prometheus-query-variables: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/prometheus/template-variables/#use-**rate_interval - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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//dashboards/variables/variable-syntax/#raw - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/variables/variable-syntax/#raw + destination: /docs/grafana-cloud/visualizations/dashboards/variables/variable-syntax/#raw data-source: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/ + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/ raw-format: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/variable-syntax/#raw - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//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//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 >}} diff --git a/docs/sources/panels-visualizations/panel-overview/index.md b/docs/sources/panels-visualizations/panel-overview/index.md index caa196fa1e2..09545bcf74e 100644 --- a/docs/sources/panels-visualizations/panel-overview/index.md +++ b/docs/sources/panels-visualizations/panel-overview/index.md @@ -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 diff --git a/docs/sources/panels-visualizations/visualizations/traces/index.md b/docs/sources/panels-visualizations/visualizations/traces/index.md index 46a309fccd1..ba7926f1b6d 100644 --- a/docs/sources/panels-visualizations/visualizations/traces/index.md +++ b/docs/sources/panels-visualizations/visualizations/traces/index.md @@ -41,10 +41,6 @@ refs: destination: /docs/grafana//datasources/tempo/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/ - - pattern: /docs/grafana/ - destination: /docs/grafana//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//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. It’s 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 panel’s 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 browser’s 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. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 20d1ee803f2..2aaef56a35c 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -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. diff --git a/docs/sources/shared/datasources/tempo-editor-traceql.md b/docs/sources/shared/datasources/tempo-editor-traceql.md index 65e5a992dd4..0556ffead7f 100644 --- a/docs/sources/shared/datasources/tempo-editor-traceql.md +++ b/docs/sources/shared/datasources/tempo-editor-traceql.md @@ -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 source’s TraceQL query editor helps you query and display traces from Tempo in **Explore**. diff --git a/docs/sources/tutorials/alerting-get-started-pt2/index.md b/docs/sources/tutorials/alerting-get-started-pt2/index.md new file mode 100644 index 00000000000..5a1ae5709e4 --- /dev/null +++ b/docs/sources/tutorials/alerting-get-started-pt2/index.md @@ -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 don’t have any contact points, add a [Contact point](http://localhost:3002/docs/grafana/latest/tutorials/alerting-get-started/#create-a-contact-point). + {{}} +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. + {{}} + +1. Click **Save rule and exit**. + +Now that we have set up the alert rule, it’s 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! diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 18ac15cee5b..15357f0fa0a 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -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. diff --git a/package.json b/package.json index 1968f5ffd78..46d46ddf051 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index bbe35a8fbe1..8ce1be9fe18 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -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, diff --git a/public/app/features/live/data/amendTimeSeries.test.ts b/packages/grafana-data/src/table/amendTimeSeries.test.ts similarity index 98% rename from public/app/features/live/data/amendTimeSeries.test.ts rename to packages/grafana-data/src/table/amendTimeSeries.test.ts index fce39e371da..4e49bf7df53 100644 --- a/public/app/features/live/data/amendTimeSeries.test.ts +++ b/packages/grafana-data/src/table/amendTimeSeries.test.ts @@ -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)', () => { diff --git a/public/app/features/live/data/amendTimeSeries.ts b/packages/grafana-data/src/table/amendTimeSeries.ts similarity index 96% rename from public/app/features/live/data/amendTimeSeries.ts rename to packages/grafana-data/src/table/amendTimeSeries.ts index bdc6c2e13dc..4eb35d0a6cf 100644 --- a/public/app/features/live/data/amendTimeSeries.ts +++ b/packages/grafana-data/src/table/amendTimeSeries.ts @@ -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]; diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index 060db7211d9..2f73d46a529 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -133,16 +133,23 @@ Check if strings are marked for translation. ```tsx // Bad ❌ -const SearchTitle = ({ term }) => ( -
- Results for {term} -
-); +const SearchTitle = ({ term }) =>
Results for {term}
; -//Good ✅ +// Good ✅ +const SearchTitle = ({ term }) => Results for {{ term }}; + +// Good ✅ (if you need to interpolate variables inside nested components) +const SearchTerm = ({ term }) => {term}; const SearchTitle = ({ term }) => ( - Results for {{ term }} + Results for + +); + +// Good ✅ (if you need to interpolate variables and additional translated strings inside nested components) +const SearchTitle = ({ term }) => ( + + Results for {'{{ myVariable }}'} and this translated text is also in green ); ``` diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 1aeff49e69f..e6f36c80491 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -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', diff --git a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx index e8880ad6f5e..5a437110ab5 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx +++ b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx @@ -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), diff --git a/packages/grafana-ui/src/themes/GlobalStyles/slate.ts b/packages/grafana-ui/src/themes/GlobalStyles/slate.ts new file mode 100644 index 00000000000..e8fd858d456 --- /dev/null +++ b/packages/grafana-ui/src/themes/GlobalStyles/slate.ts @@ -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, + }, + }, + }); +} diff --git a/pkg/apis/example/v0alpha1/doc.go b/pkg/apis/example/v0alpha1/doc.go deleted file mode 100644 index 96849073b83..00000000000 --- a/pkg/apis/example/v0alpha1/doc.go +++ /dev/null @@ -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" diff --git a/pkg/apis/example/v0alpha1/register.go b/pkg/apis/example/v0alpha1/register.go deleted file mode 100644 index e254cad1d34..00000000000 --- a/pkg/apis/example/v0alpha1/register.go +++ /dev/null @@ -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} -) diff --git a/pkg/apis/example/v0alpha1/types.go b/pkg/apis/example/v0alpha1/types.go deleted file mode 100644 index a8fef3ffe6f..00000000000 --- a/pkg/apis/example/v0alpha1/types.go +++ /dev/null @@ -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"` -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/example/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 063ad395718..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -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 -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.defaults.go b/pkg/apis/example/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -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 -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.openapi.go b/pkg/apis/example/v0alpha1/zz_generated.openapi.go deleted file mode 100644 index 8c441c55064..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.openapi.go +++ /dev/null @@ -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: "", - }, - }, - }, - }, - }, - } -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 75c16b7b44e..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1 +0,0 @@ -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/example/v0alpha1,RuntimeInfo,IsEnterprise diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 3ccfae82528..a44271582af 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -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, diff --git a/pkg/registry/apis/example/dummy_storage.go b/pkg/registry/apis/example/dummy_storage.go deleted file mode 100644 index 574a87dd527..00000000000 --- a/pkg/registry/apis/example/dummy_storage.go +++ /dev/null @@ -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 -} diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go deleted file mode 100644 index 6dc5644bffa..00000000000 --- a/pkg/registry/apis/example/register.go +++ /dev/null @@ -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 - }) -} diff --git a/pkg/registry/apis/example/storage.go b/pkg/registry/apis/example/storage.go deleted file mode 100644 index 4e2c1858837..00000000000 --- a/pkg/registry/apis/example/storage.go +++ /dev/null @@ -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 -} diff --git a/pkg/registry/apis/example/subresource.go b/pkg/registry/apis/example/subresource.go deleted file mode 100644 index 3fa5807a9e9..00000000000 --- a/pkg/registry/apis/example/subresource.go +++ /dev/null @@ -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 -} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index d9782c44697..2ce96e4af0d 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -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, diff --git a/pkg/services/accesscontrol/pluginutils/utils.go b/pkg/services/accesscontrol/pluginutils/utils.go index ffbde39295f..7db8cf635a0 100644 --- a/pkg/services/accesscontrol/pluginutils/utils.go +++ b/pkg/services/accesscontrol/pluginutils/utils.go @@ -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 diff --git a/pkg/services/accesscontrol/pluginutils/utils_test.go b/pkg/services/accesscontrol/pluginutils/utils_test.go index 38ae6f4765c..80289ec77e7 100644 --- a/pkg/services/accesscontrol/pluginutils/utils_test.go +++ b/pkg/services/accesscontrol/pluginutils/utils_test.go @@ -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) { diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index 34ae2316813..a2c413d69a9 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -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( diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index 408bf5cda5e..96d1679ade6 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -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) } diff --git a/pkg/services/cloudmigration/api/api_test.go b/pkg/services/cloudmigration/api/api_test.go index 6fc50df2e78..35fe13466f5 100644 --- a/pkg/services/cloudmigration/api/api_test.go +++ b/pkg/services/cloudmigration/api/api_test.go @@ -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 diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index c0660cb96db..a49e35ea947 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -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 } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index 4fca6857fdb..e349b15c91c 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -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") diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 9db3eba17bd..3365f16c77b 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -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 +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 9f4b1d1e4ec..83ecb800381 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -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}, diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index a6cd5e44bf5..4b636ecd874 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -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 +} diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index 0a7b444e7e4..22d9e809070 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -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 { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index fb6601ad66f..186033cc2c2 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -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{ diff --git a/pkg/services/ldap/service/ldap.go b/pkg/services/ldap/service/ldap.go index 2125b322cb6..d3d87e9b2fc 100644 --- a/pkg/services/ldap/service/ldap.go +++ b/pkg/services/ldap/service/ldap.go @@ -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 } diff --git a/pkg/services/ldap/service/ldap_test.go b/pkg/services/ldap/service/ldap_test.go index 74cb4ab1a8d..6cf33fc1bff 100644 --- a/pkg/services/ldap/service/ldap_test.go +++ b/pkg/services/ldap/service/ldap_test.go @@ -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) + } + }) + } +} diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index 5aae742b6ca..a62d0bfe284 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -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 { diff --git a/pkg/tests/apis/example/example_test.go b/pkg/tests/apis/example/example_test.go deleted file mode 100644 index c702402837f..00000000000 --- a/pkg/tests/apis/example/example_test.go +++ /dev/null @@ -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)) - }) -} diff --git a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.test.ts b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.test.ts new file mode 100644 index 00000000000..413035cf22b --- /dev/null +++ b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.test.ts @@ -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(); + }); +}); diff --git a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts index 4bc186849e9..bf30ddd9e99 100644 --- a/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts +++ b/public/app/features/dashboard-scene/saving/DashboardSceneChangeTracker.ts @@ -188,5 +188,6 @@ export class DashboardSceneChangeTracker { public terminate() { this.stopTrackingChanges(); this._changesWorker?.terminate(); + this._changesWorker = undefined; } } diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 7687efa6107..6164551eb1c 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -24,7 +24,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps -
{body}
+
{body}
)} @@ -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', diff --git a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx index c92baacc746..5e2067ce961 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx @@ -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(); }; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx index c761570fdc4..1d5c7a5a8e1 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx @@ -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 { }; 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 ( -
+
{links && links.length > 0 && ( -