mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/main' into drclau/unistor/replace-authenticators-3
This commit is contained in:
commit
c591631135
@ -3049,19 +3049,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx:5381": [
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
|
@ -4,7 +4,7 @@ go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.19.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apimachinery v0.31.1
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
|
||||
)
|
||||
|
||||
|
@ -101,8 +101,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
@ -1740,7 +1740,7 @@ hide_angular_deprecation =
|
||||
# Comma separated list of plugin ids for which environment variables should be forwarded. Used only when feature flag pluginsSkipHostEnvVars is enabled.
|
||||
forward_host_env_vars =
|
||||
# Comma separated list of plugin ids to install as part of the startup process.
|
||||
preinstall =
|
||||
preinstall = grafana-lokiexplore-app
|
||||
# Controls whether preinstall plugins asynchronously (in the background) or synchronously (blocking). Useful when preinstalled plugins are used with provisioning.
|
||||
preinstall_async = true
|
||||
# Disables preinstall feature. It has the same effect as setting preinstall to an empty list.
|
||||
|
@ -17,3 +17,7 @@ apps:
|
||||
org_id: 1
|
||||
org_name: Main Org.
|
||||
disabled: false
|
||||
- type: grafana-extensionexample3-app
|
||||
org_id: 1
|
||||
org_name: Main Org.
|
||||
disabled: false
|
||||
|
@ -48,6 +48,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `angularDeprecationUI` | Display Angular warnings in dashboards and panels | Yes |
|
||||
| `dashgpt` | Enable AI powered features in dashboards | Yes |
|
||||
| `alertingInsights` | Show the new alerting insights landing page | Yes |
|
||||
| `externalServiceAccounts` | Automatic service account and token setup for plugins | Yes |
|
||||
| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes |
|
||||
| `formatString` | Enable format string transformer | Yes |
|
||||
| `transformationsVariableSupport` | Allows using variables in transformations | Yes |
|
||||
@ -100,7 +101,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel |
|
||||
| `sqlDatasourceDatabaseSelection` | Enables previous SQL data source dataset dropdown behavior |
|
||||
| `reportingRetries` | Enables rendering retries for the reporting feature |
|
||||
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
|
||||
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
|
||||
| `teamHttpHeaders` | Enables LBAC for datasources to apply LogQL filtering of logs to the client requests for users in teams |
|
||||
| `pdfTables` | Enables generating table data as PDF in reporting |
|
||||
|
@ -0,0 +1,121 @@
|
||||
---
|
||||
aliases:
|
||||
- ../../../auth/enhanced-ldap/
|
||||
description: Learn about configuring LDAP authentication in Grafana using the Grafana UI.
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: LDAP user interface
|
||||
title: Configure LDAP authentication using the Grafana user interface
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Configure LDAP authentication using the Grafana user interface
|
||||
|
||||
This page explains how to configure LDAP authentication in Grafana using the Grafana user interface. For more detailed information about configuring LDAP authentication using the configuration file, refer to [LDAP authentication]({{< relref "../ldap" >}}).
|
||||
|
||||
Benefits of using the Grafana user interface to configure LDAP authentication include:
|
||||
|
||||
- There is no need to edit the configuration file manually.
|
||||
- Quickly test the connection to the LDAP server.
|
||||
- There is no need to restart Grafana after making changes.
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
Any configuration changes made through the Grafana user interface (UI) will take precedence over settings specified in the Grafana configuration file or through environment variables. If you modify any configuration settings in the UI, they will override any corresponding settings set via environment variables or defined in the configuration file.
|
||||
{{% /admonition %}}
|
||||
|
||||
## Before you begin
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Knowledge of LDAP authentication and how it works.
|
||||
- Grafana instance v11.3.0 or later.
|
||||
- Permissions `settings:read` and `settings:write` with `settings:auth.ldap:*` scope.
|
||||
- This feature requires the `ssoSettingsLDAP` feature toggle to be enabled.
|
||||
|
||||
## Steps to configure LDAP authentication
|
||||
|
||||
Sign in to Grafana and navigate to **Administration > Authentication > LDAP**.
|
||||
|
||||
### 1. Complete mandatory fields
|
||||
|
||||
The mandatory fields have an asterisk (**\***) next to them. Complete the following fields:
|
||||
|
||||
1. **Server host**: Host name or IP address of the LDAP server.
|
||||
1. **Search filter**: The LDAP search filter finds entries within the directory.
|
||||
1. **Search base DNS**: List of base DNs to search through.
|
||||
|
||||
### 2. Complete optional fields
|
||||
|
||||
Complete the optional fields as needed:
|
||||
|
||||
1. **Bind DN**: Distinguished name (DN) of the user to bind to.
|
||||
1. **Bind password**: Password for the server.
|
||||
|
||||
### 3. Advanced settings
|
||||
|
||||
Click the **Edit** button in the **Advanced settings** section to configure the following settings:
|
||||
|
||||
#### 1. Miscellaneous settings
|
||||
|
||||
Complementary settings for LDAP authentication.
|
||||
|
||||
1. **Allow sign-up**: Allows new users to register upon logging in.
|
||||
1. **Port**: Port number of the LDAP server. The default is 389.
|
||||
1. **Timeout**: Time in seconds to wait for a response from the LDAP server.
|
||||
|
||||
#### 2. Attributes
|
||||
|
||||
Attributes used to map LDAP user assertion to Grafana user attributes.
|
||||
|
||||
1. **Name**: Name of the assertion attribute to map to the Grafana user name.
|
||||
1. **Surname**: Name of the assertion attribute to map to the Grafana user surname.
|
||||
1. **Username**: Name of the assertion attribute to map to the Grafana user username.
|
||||
1. **Member Of**: Name of the assertion attribute to map to the Grafana user membership.
|
||||
1. **Email**: Name of the assertion attribute to map to the Grafana user email.
|
||||
|
||||
#### 3. Group mapping
|
||||
|
||||
Map LDAP groups to Grafana roles.
|
||||
|
||||
1. **Skip organization role sync**: This option avoids syncing organization roles. It is useful when you want to manage roles manually.
|
||||
1. **Group search filter**: The LDAP search filter finds groups within the directory.
|
||||
1. **Group search base DNS**: List of base DNS to specify the matching groups' locations.
|
||||
1. **Group name attribute**: Identifies users within group entries.
|
||||
1. **Manage group mappings**:
|
||||
|
||||
When managing group mappings, the following fields will become available. To add a new group mapping, click the **Add group mapping** button.
|
||||
|
||||
1. **Add a group DN mapping**: The name of the key used to extract the ID token.
|
||||
1. **Add an organization role mapping**: Select the Basic Role mapped to this group.
|
||||
1. **Add the organization ID membership mapping**: Map the group to an organization ID.
|
||||
1. **Define Grafana Admin membership**: Enable Grafana Admin privileges to the group.
|
||||
|
||||
#### 4. Extra security settings
|
||||
|
||||
Additional security settings options for LDAP authentication.
|
||||
|
||||
1. **Enable SSL**: This option will enable SSL to connect to the LDAP server.
|
||||
1. **Start TLS**: Use StartTLS to secure the connection to the LDAP server.
|
||||
1. **Min TLS version**: Choose the minimum TLS version to use. TLS1.2 or TLS1.3
|
||||
1. **TLS ciphers**: List the ciphers to use for the connection. For a complete list of ciphers, refer to the [Cipher Go library](https://go.dev/src/crypto/tls/cipher_suites.go).
|
||||
1. **Encryption key and certificate provision specification**:
|
||||
This section allows you to specify the key and certificate for the LDAP server. You can provide the key and certificate in two ways: **base-64** encoded or **path to files**.
|
||||
1. **Base-64 encoded certificate**:
|
||||
All values used in this section must be base-64 encoded.
|
||||
1. **Root CA certificate content**: List of root CA certificates.
|
||||
1. **Client certificate content**: Client certificate content.
|
||||
1. **Client key content**: Client key content.
|
||||
1. **Path to files**:
|
||||
Path in the file system to the key and certificate files
|
||||
1. **Root CA certificate path**: Path to the root CA certificate.
|
||||
1. **Client certificate path**: Path to the client certificate.
|
||||
1. **Client key path**: Path to the client key.
|
||||
|
||||
### 4. Persisting the configuration
|
||||
|
||||
Once you have configured the LDAP settings, click **Save** to persist the configuration.
|
||||
|
||||
If you want to delete all the changes made through the UI and revert to the configuration file settings, click the three dots menu icon and click **Reset to default values**.
|
@ -9,7 +9,7 @@ type ReusableComponentProps = {
|
||||
|
||||
export function AddedComponents() {
|
||||
const { components } = usePluginComponents<ReusableComponentProps>({
|
||||
extensionPointId: 'plugins/grafana-extensionexample2-app/addComponent/v1',
|
||||
extensionPointId: 'plugins/grafana-extensionstest-app/addComponent/v1',
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -16,7 +16,7 @@ type ReusableComponentProps = {
|
||||
|
||||
export function LegacyGetters() {
|
||||
const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
|
||||
const extensionPointId2 = 'plugins/grafana-extensionexample2-app/configure-extension-component/v1';
|
||||
const extensionPointId2 = 'plugins/grafana-extensionstest-app/configure-extension-component/v1';
|
||||
const context: AppExtensionContext = {};
|
||||
|
||||
const { extensions } = getPluginExtensions({
|
||||
|
@ -16,7 +16,7 @@ type ReusableComponentProps = {
|
||||
|
||||
export function LegacyHooks() {
|
||||
const extensionPointId1 = 'plugins/grafana-extensionstest-app/actions';
|
||||
const extensionPointId2 = 'plugins/grafana-extensionexample2-app/configure-extension-component/v1';
|
||||
const extensionPointId2 = 'plugins/grafana-extensionstest-app/configure-extension-component/v1';
|
||||
const context: AppExtensionContext = {};
|
||||
|
||||
const { extensions } = usePluginExtensions({
|
||||
|
@ -60,8 +60,43 @@
|
||||
"defaultNav": false
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"addedLinks": [
|
||||
{
|
||||
"targets": ["grafana/dashboard/panel/menu"],
|
||||
"title": "Open from time series or pie charts (path)",
|
||||
"description": "This link will only be visible on time series and pie charts"
|
||||
},
|
||||
{
|
||||
"targets": ["grafana/dashboard/panel/menu"],
|
||||
"title": "Open from time series or pie charts (onClick)",
|
||||
"description": "This link will only be visible on time series and pie charts"
|
||||
}
|
||||
],
|
||||
"extensionPoints": [
|
||||
{
|
||||
"id": "plugins/grafana-extensionstest-app/use-plugin-links/v1",
|
||||
"title": "Extension point - links"
|
||||
},
|
||||
{
|
||||
"id": "plugins/grafana-extensionstest-app/addComponent/v1",
|
||||
"title": "Extension point - components"
|
||||
},
|
||||
{
|
||||
"id": "plugins/grafana-extensionstest-app/actions",
|
||||
"title": "Legacy extension point - usePluginExtensions() and usePluginLinkExtensions()"
|
||||
},
|
||||
{
|
||||
"id": "plugins/grafana-extensionstest-app/configure-extension-component/v1",
|
||||
"title": "Legacy extension point - usePluginComponentExtensions()"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.4.0",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": ["grafana-extensionexample1-app/reusable-component/v1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,30 @@
|
||||
"defaultNav": false
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"exposedComponents": [
|
||||
{
|
||||
"id": "grafana-extensionexample1-app/reusable-component/v1",
|
||||
"title": "Exposed component",
|
||||
"description": "A component that can be reused by other app plugins."
|
||||
}
|
||||
],
|
||||
"addedLinks": [
|
||||
{
|
||||
"targets": [
|
||||
"plugins/grafana-extensionstest-app/actions",
|
||||
"plugins/grafana-extensionstest-app/use-plugin-links/v1"
|
||||
],
|
||||
"title": "Go to A",
|
||||
"description": "Navigating to pluging A"
|
||||
},
|
||||
{
|
||||
"targets": ["plugins/grafana-extensionstest-app/use-plugin-links/v1"],
|
||||
"title": "Basic link",
|
||||
"description": "..."
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.3.3",
|
||||
"plugins": []
|
||||
|
@ -19,13 +19,13 @@ export const plugin = new AppPlugin<{}>()
|
||||
},
|
||||
})
|
||||
.configureExtensionComponent({
|
||||
extensionPointId: 'plugins/grafana-extensionexample2-app/configure-extension-component/v1',
|
||||
extensionPointId: 'plugins/grafana-extensionstest-app/configure-extension-component/v1',
|
||||
title: 'Configure extension component from B',
|
||||
description: 'A component that can be reused by other app plugins. Shared using configureExtensionComponent api',
|
||||
component: ({ name }: { name: string }) => <div data-testid={testIds.appB.reusableComponent}>Hello {name}!</div>,
|
||||
})
|
||||
.addComponent<{ name: string }>({
|
||||
targets: 'plugins/grafana-extensionexample2-app/addComponent/v1',
|
||||
targets: 'plugins/grafana-extensionstest-app/addComponent/v1',
|
||||
title: 'Added component from B',
|
||||
description: 'A component that can be reused by other app plugins. Shared using addComponent api',
|
||||
component: ({ name }: { name: string }) => (
|
||||
|
@ -18,6 +18,30 @@
|
||||
"version": "%VERSION%",
|
||||
"updated": "%TODAY%"
|
||||
},
|
||||
"extensions": {
|
||||
"addedLinks": [
|
||||
{
|
||||
"targets": [
|
||||
"plugins/grafana-extensionstest-app/actions",
|
||||
"plugins/grafana-extensionstest-app/use-plugin-links/v1"
|
||||
],
|
||||
"title": "Open from B",
|
||||
"description": "Open a modal from plugin B"
|
||||
}
|
||||
],
|
||||
"addedComponents": [
|
||||
{
|
||||
"targets": ["plugins/grafana-extensionstest-app/configure-extension-component/v1"],
|
||||
"title": "Configure extension component from B",
|
||||
"description": "A component that can be reused by other app plugins. Shared using configureExtensionComponent api"
|
||||
},
|
||||
{
|
||||
"targets": ["plugins/grafana-extensionstest-app/addComponent/v1"],
|
||||
"title": "Added component from B",
|
||||
"description": "A component that can be reused by other app plugins. Shared using addComponent api"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includes": [
|
||||
{
|
||||
"type": "page",
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { usePluginExtensions, usePluginLinks } from '@grafana/runtime';
|
||||
import { Stack } from '@grafana/ui';
|
||||
import { testIds } from '../../../../testIds';
|
||||
import { ActionButton } from '../../../../components/ActionButton';
|
||||
|
||||
export const LINKS_EXTENSION_POINT_ID = 'plugins/grafana-extensionstest-app/use-plugin-links/v1';
|
||||
|
||||
export function AddedLinks() {
|
||||
const { links } = usePluginLinks({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
||||
const { extensions } = usePluginExtensions({ extensionPointId: LINKS_EXTENSION_POINT_ID });
|
||||
|
||||
return (
|
||||
<Stack direction={'column'} gap={4}>
|
||||
<section data-testid={testIds.appC.section1}>
|
||||
<h3>Link extensions defined with addLink and retrieved using usePluginLinks</h3>
|
||||
<ActionButton extensions={links} />
|
||||
</section>
|
||||
<section data-testid={testIds.appC.section2}>
|
||||
<h3>Link extensions defined with addLink and retrieved using usePluginExtensions</h3>
|
||||
<ActionButton extensions={extensions} />
|
||||
</section>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { AppRootProps } from '@grafana/data';
|
||||
import { AddedLinks } from './AddedLinks';
|
||||
import { testIds } from '../../../../testIds';
|
||||
|
||||
export class App extends React.PureComponent<AppRootProps> {
|
||||
render() {
|
||||
return (
|
||||
<div data-testid={testIds.appC.container} className="page-container">
|
||||
Hello Grafana!
|
||||
<AddedLinks></AddedLinks>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81.9 71.52"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="42.95" y1="16.88" x2="81.9" y2="16.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M55.46,62.43A2,2,0,0,1,54.07,59l4.72-4.54a2,2,0,0,1,2.2-.39l3.65,1.63,3.68-3.64a2,2,0,1,1,2.81,2.84l-4.64,4.6a2,2,0,0,1-2.22.41L60.6,58.26l-3.76,3.61A2,2,0,0,1,55.46,62.43Z"/><path class="cls-2" d="M37,0H2A2,2,0,0,0,0,2V31.76a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V2A2,2,0,0,0,37,0ZM4,29.76V8.84H35V29.76Z"/><path class="cls-3" d="M79.9,0H45a2,2,0,0,0-2,2V31.76a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V2A2,2,0,0,0,79.9,0ZM47,29.76V8.84h31V29.76Z"/><path class="cls-2" d="M37,37.76H2a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V39.76A2,2,0,0,0,37,37.76ZM4,67.52V46.6H35V67.52Z"/><path class="cls-2" d="M79.9,37.76H45a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V39.76A2,2,0,0,0,79.9,37.76ZM47,67.52V46.6h31V67.52Z"/><rect class="cls-1" x="10.48" y="56.95" width="4" height="5.79"/><rect class="cls-1" x="17.43" y="53.95" width="4" height="8.79"/><rect class="cls-1" x="24.47" y="50.95" width="4" height="11.79"/><path class="cls-1" d="M19.47,25.8a6.93,6.93,0,1,1,6.93-6.92A6.93,6.93,0,0,1,19.47,25.8Zm0-9.85a2.93,2.93,0,1,0,2.93,2.93A2.93,2.93,0,0,0,19.47,16Z"/></g></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,44 @@
|
||||
import { AppPlugin } from '@grafana/data';
|
||||
|
||||
import { LINKS_EXTENSION_POINT_ID } from '../../pages/AddedLinks';
|
||||
import { testIds } from '../../testIds';
|
||||
import { App } from '../../components/App';
|
||||
|
||||
export const plugin = new AppPlugin<{}>()
|
||||
.setRootPage(App)
|
||||
.configureExtensionLink({
|
||||
title: 'configureExtensionLink (where meta data is missing)',
|
||||
description: 'Open a modal from plugin B',
|
||||
extensionPointId: 'plugins/grafana-extensionstest-app/actions',
|
||||
onClick: (_, { openModal }) => {
|
||||
openModal({
|
||||
title: 'Modal from app B',
|
||||
body: () => <div data-testid={testIds.appB.modal}>From plugin B</div>,
|
||||
});
|
||||
},
|
||||
})
|
||||
.configureExtensionComponent({
|
||||
extensionPointId: 'plugins/grafana-extensionstest-app/configure-extension-component/v1',
|
||||
title: 'configureExtensionComponent (where meta data is missing)',
|
||||
description: 'A component that can be reused by other app plugins. Shared using configureExtensionComponent api',
|
||||
component: ({ name }: { name: string }) => <div data-testid={testIds.appB.reusableComponent}>Hello {name}!</div>,
|
||||
})
|
||||
.addComponent<{ name: string }>({
|
||||
targets: ['plugins/grafana-extensionstest-app/addComponent/v1'],
|
||||
title: 'Added component (where meta data is missing)',
|
||||
description: '.',
|
||||
component: ({ name }: { name: string }) => (
|
||||
<div data-testid={testIds.appB.reusableAddedComponent}>Hello {name}!</div>
|
||||
),
|
||||
})
|
||||
.addLink({
|
||||
title: 'Added link (where meta data is missing)',
|
||||
description: '.',
|
||||
targets: [LINKS_EXTENSION_POINT_ID],
|
||||
onClick: (_, { openModal }) => {
|
||||
openModal({
|
||||
title: 'Modal from app C',
|
||||
body: () => <div data-testid={testIds.appB.modal}>From plugin B</div>,
|
||||
});
|
||||
},
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
|
||||
"type": "app",
|
||||
"name": "D App",
|
||||
"id": "grafana-extensionexample3-app",
|
||||
"preload": true,
|
||||
"info": {
|
||||
"keywords": ["app"],
|
||||
"description": "Will extend root app with ui extensions",
|
||||
"author": {
|
||||
"name": "grafana"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/logo.svg",
|
||||
"large": "img/logo.svg"
|
||||
},
|
||||
"screenshots": [],
|
||||
"version": "%VERSION%",
|
||||
"updated": "%TODAY%"
|
||||
},
|
||||
"includes": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Default",
|
||||
"path": "/a/grafana-extensionexample3-app",
|
||||
"role": "Admin",
|
||||
"addToNav": false,
|
||||
"defaultNav": false
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.3.3",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
@ -16,6 +16,11 @@ export const testIds = {
|
||||
reusableAddedComponent: 'b-app-add-component',
|
||||
exposedComponent: 'b-app-exposed-component',
|
||||
},
|
||||
appC: {
|
||||
container: 'c-app-body',
|
||||
section1: 'use-plugin-links',
|
||||
section2: 'use-plugin-extensions',
|
||||
},
|
||||
legacyGettersPage: {
|
||||
container: 'data-testid pg-legacy-getters-container',
|
||||
section1: 'get-plugin-extensions',
|
||||
|
@ -2,6 +2,7 @@ import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { testIds } from '../../testIds';
|
||||
import pluginJson from '../../plugin.json';
|
||||
import testApp3pluginJson from '../../plugins/grafana-extensionexample3-app/plugin.json';
|
||||
|
||||
test.describe('usePluginExtensions + configureExtensionLink', () => {
|
||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
||||
@ -12,6 +13,17 @@ test.describe('usePluginExtensions + configureExtensionLink', () => {
|
||||
await page.getByTestId(testIds.modal.open).click();
|
||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not display extensions that have not been declared in plugin.json when in development mode', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`/a/${pluginJson.id}/legacy-hooks`);
|
||||
const section = await page.getByTestId(testIds.legacyHooksPage.section1);
|
||||
await section.getByTestId(testIds.actions.button).click();
|
||||
await expect(
|
||||
page.getByTestId(testIds.container).getByText('configureExtensionLink (where meta data is missing)')
|
||||
).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('usePluginExtensions + configureExtensionComponent', () => {
|
||||
@ -43,3 +55,13 @@ test.describe('usePluginComponentExtensions + configureExtensionComponent', () =
|
||||
).toHaveText('Hello World!');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('usePluginExtensions + addLink', () => {
|
||||
test('should not display extensions in case extension point has not been declared in plugin json (dev mode only)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`/a/${testApp3pluginJson.id}/legacy-hooks`);
|
||||
const section = await page.getByTestId(testIds.appC.section2);
|
||||
await expect(section.getByTestId(testIds.actions.button)).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import pluginJson from '../plugin.json';
|
||||
import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json';
|
||||
import { testIds } from '../testIds';
|
||||
|
||||
test('should extend the actions menu with a link to a-app plugin', async ({ page }) => {
|
||||
@ -28,3 +29,11 @@ test('should extend main app with basic link extension from app A', async ({ pag
|
||||
await page.getByTestId(testIds.modal.open).click();
|
||||
await expect(page.getByTestId(testIds.appA.container)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`/a/${testApp3pluginJson.id}`);
|
||||
const container = await page.getByTestId(testIds.appC.section1);
|
||||
await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible();
|
||||
});
|
||||
|
18
go.mod
18
go.mod
@ -111,7 +111,7 @@ require (
|
||||
github.com/huandu/xstrings v1.3.3 // @grafana/partner-datasources
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.13.0 // @grafana/observability-metrics
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // @grafana/grafana-app-platform-squad
|
||||
github.com/jmespath/go-jmespath v0.4.0 // @grafana/grafana-backend-group
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect; @grafana/grafana-backend-group
|
||||
github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group
|
||||
github.com/json-iterator/go v1.1.12 // @grafana/grafana-backend-group
|
||||
github.com/lib/pq v1.10.9 // @grafana/grafana-backend-group
|
||||
@ -187,13 +187,13 @@ require (
|
||||
gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend
|
||||
gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group
|
||||
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend
|
||||
k8s.io/api v0.31.0 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/apimachinery v0.31.0 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/apiserver v0.31.0 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/client-go v0.31.0 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/component-base v0.31.0 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/api v0.31.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/apimachinery v0.31.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/apiserver v0.31.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/client-go v0.31.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/component-base v0.31.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/klog/v2 v2.130.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/kube-aggregator v0.31.0 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/kube-aggregator v0.31.1 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // @grafana/partner-datasources
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // @grafana-app-platform-squad
|
||||
@ -447,7 +447,7 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/kms v0.31.0 // indirect
|
||||
k8s.io/kms v0.31.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.41.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
@ -478,6 +478,8 @@ require (
|
||||
github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990 // @grafana/grafana-app-platform-squad
|
||||
)
|
||||
|
||||
require github.com/jmespath-community/go-jmespath v1.1.1 // @grafana/identity-access-team
|
||||
|
||||
require (
|
||||
cloud.google.com/go/longrunning v0.5.12 // indirect
|
||||
github.com/at-wat/mqtt-go v0.19.4 // indirect
|
||||
|
30
go.sum
30
go.sum
@ -2524,6 +2524,8 @@ github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuT
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/jmattheis/goverter v1.4.0/go.mod h1:iVIl/4qItWjWj2g3vjouGoYensJbRqDHpzlEVMHHFeY=
|
||||
github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4=
|
||||
github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
@ -4547,18 +4549,18 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
|
||||
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
|
||||
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
|
||||
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
|
||||
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
|
||||
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
|
||||
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
|
||||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
|
||||
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
|
||||
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
|
||||
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
|
||||
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
|
||||
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
|
||||
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
|
||||
k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
|
||||
k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
|
||||
k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
|
||||
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
@ -4569,10 +4571,10 @@ k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.31.0 h1:KchILPfB1ZE+ka7223mpU5zeFNkmb45jl7RHnlImUaI=
|
||||
k8s.io/kms v0.31.0/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94=
|
||||
k8s.io/kube-aggregator v0.31.0 h1:3DqSpmqHF8rey7fY+qYXLJms0tYPhxrgWvjpnKVnS0Y=
|
||||
k8s.io/kube-aggregator v0.31.0/go.mod h1:Fa+OVSpMQC7zbTTz7/QG7FXe9jZ8usuJQej5sMdCrkM=
|
||||
k8s.io/kms v0.31.1 h1:cGLyV3cIwb0ovpP/jtyIe2mEuQ/MkbhmeBF2IYCA9Io=
|
||||
k8s.io/kms v0.31.1/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94=
|
||||
k8s.io/kube-aggregator v0.31.1 h1:vrYBTTs3xMrpiEsmBjsLETZE9uuX67oQ8B3i1BFfMPw=
|
||||
k8s.io/kube-aggregator v0.31.1/go.mod h1:+aW4NX50uneozN+BtoCxI4g7ND922p8Wy3tWKFDiWVk=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
|
28
go.work.sum
28
go.work.sum
@ -495,8 +495,6 @@ github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8
|
||||
github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw=
|
||||
github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s=
|
||||
github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
@ -534,6 +532,7 @@ github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
|
||||
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
|
||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
|
||||
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
@ -550,8 +549,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
|
||||
github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198 h1:FSii2UQeSLngl3jFoR4tUKZLprO7qUlh/TKKticc0BM=
|
||||
github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
|
||||
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo=
|
||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
||||
@ -595,8 +592,6 @@ github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d/go.mod h1:GMLi6d0
|
||||
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.3 h1:QlgIwyyozYYQf/dL279Baicyax+SuE99Set5chMnq1s=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.3/go.mod h1:5nt5Gmp6+GyM+Jr7xsXKJtbizxbYXXLmEac6kw5paQI=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE=
|
||||
github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc=
|
||||
github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26/go.mod h1:Pn9nfzCk7nV0mvNgwusgCjCROZP6nm4GpwTnmEhLT24=
|
||||
@ -675,8 +670,6 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw
|
||||
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
|
||||
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
|
||||
github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
|
||||
@ -710,8 +703,6 @@ github.com/matryer/moq v0.3.3 h1:pScMH9VyrdT4S93yiLpVyU8rCDqGQr24uOyBxmktG5Q=
|
||||
github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s=
|
||||
github.com/matryer/moq v0.5.0 h1:h2PJUYjZSiyEahzVogDRmrgL9Bsx9xYAl8l+LPfmwL8=
|
||||
github.com/matryer/moq v0.5.0/go.mod h1:39GTnrD0mVWHPvWdYj5ki/lxfhLQEtHcLh+tWoYF/iE=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
@ -742,6 +733,7 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM=
|
||||
github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk=
|
||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.97.0 h1:8GH8y3Cq54Ey6He9tyhcVYLfG4TEs/7pp3s6934zNKA=
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter v0.97.0/go.mod h1:QBHXt+tHds39B4xGyBkbOx2TST+p8JLWBiXbKKAhNss=
|
||||
@ -830,7 +822,6 @@ github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH
|
||||
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
|
||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w=
|
||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY=
|
||||
@ -855,6 +846,7 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o=
|
||||
github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk=
|
||||
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
|
||||
@ -1007,6 +999,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqR
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 h1:q/Nj5/2TZRIt6PderQ9oU0M00fzoe8UZuINGw6ETGTw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1/go.mod h1:DTE9yAu6r08jU3xa68GiSeI7oRcSEQ2RpKbbQGO+dWM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs=
|
||||
@ -1016,6 +1010,7 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
@ -1024,11 +1019,13 @@ go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
@ -1036,7 +1033,9 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhp
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
@ -1052,6 +1051,7 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
@ -1063,6 +1063,7 @@ gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPj
|
||||
gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU=
|
||||
@ -1093,10 +1094,15 @@ gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
|
||||
k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
|
||||
k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
|
||||
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
|
||||
k8s.io/code-generator v0.31.0 h1:w607nrMi1KeDKB3/F/J4lIoOgAwc+gV9ZKew4XRfMp8=
|
||||
k8s.io/code-generator v0.31.0/go.mod h1:84y4w3es8rOJOUUP1rLsIiGlO1JuEaPFXQPA9e/K6U0=
|
||||
k8s.io/code-generator v0.31.1 h1:GvkRZEP2g2UnB2QKT2Dgc/kYxIkDxCHENv2Q1itioVs=
|
||||
k8s.io/code-generator v0.31.1/go.mod h1:oL2ky46L48osNqqZAeOcWWy0S5BXj50vVdwOtTefqIs=
|
||||
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
|
||||
k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks=
|
||||
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
|
||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||
|
@ -2,7 +2,7 @@ module github.com/grafana/grafana/hack
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require k8s.io/code-generator v0.31.0
|
||||
require k8s.io/code-generator v0.31.1
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
|
@ -10,8 +10,8 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
k8s.io/code-generator v0.31.0 h1:w607nrMi1KeDKB3/F/J4lIoOgAwc+gV9ZKew4XRfMp8=
|
||||
k8s.io/code-generator v0.31.0/go.mod h1:84y4w3es8rOJOUUP1rLsIiGlO1JuEaPFXQPA9e/K6U0=
|
||||
k8s.io/code-generator v0.31.1 h1:GvkRZEP2g2UnB2QKT2Dgc/kYxIkDxCHENv2Q1itioVs=
|
||||
k8s.io/code-generator v0.31.1/go.mod h1:oL2ky46L48osNqqZAeOcWWy0S5BXj50vVdwOtTefqIs=
|
||||
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
|
||||
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
|
@ -11,7 +11,7 @@ set -o pipefail
|
||||
|
||||
SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
|
||||
pushd "${SCRIPT_ROOT}/hack" && GO111MODULE=on go mod tidy && popd
|
||||
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.31.0)}
|
||||
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo $(go env GOPATH)/pkg/mod/k8s.io/code-generator@v0.31.1)}
|
||||
|
||||
OUTDIR="${HOME}/go/src"
|
||||
OPENAPI_VIOLATION_EXCEPTIONS_FILENAME="zz_generated.openapi_violation_exceptions.list"
|
||||
|
@ -2,10 +2,14 @@ import { useContext } from 'react';
|
||||
|
||||
import { Context, PluginContextType } from './PluginContext';
|
||||
|
||||
export function usePluginContext(): PluginContextType {
|
||||
export function usePluginContext(): PluginContextType | null {
|
||||
const context = useContext(Context);
|
||||
|
||||
// The extensions hooks (e.g. `usePluginLinks()`) are using this hook to check
|
||||
// if they are inside a plugin or not (core Grafana), so we should be able to return an empty state as well (`null`).
|
||||
if (!context) {
|
||||
throw new Error('usePluginContext must be used within a PluginContextProvider');
|
||||
return null;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
@ -586,6 +586,7 @@ export {
|
||||
type AngularMeta,
|
||||
type PluginMeta,
|
||||
type PluginDependencies,
|
||||
type PluginExtensions,
|
||||
type PluginInclude,
|
||||
type PluginBuildInfo,
|
||||
type ScreenshotInfo,
|
||||
|
@ -98,6 +98,7 @@ export interface PluginMeta<T extends KeyValue = {}> {
|
||||
angular?: AngularMeta;
|
||||
angularDetected?: boolean;
|
||||
loadingStrategy?: PluginLoadingStrategy;
|
||||
extensions?: PluginExtensions;
|
||||
}
|
||||
|
||||
interface PluginDependencyInfo {
|
||||
@ -111,6 +112,38 @@ export interface PluginDependencies {
|
||||
grafanaDependency?: string;
|
||||
grafanaVersion: string;
|
||||
plugins: PluginDependencyInfo[];
|
||||
extensions: {
|
||||
// A list of exposed component IDs
|
||||
exposedComponents: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export type ExtensionInfo = {
|
||||
targets: string | string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export interface PluginExtensions {
|
||||
// The component extensions that the plugin registers
|
||||
addedComponents: ExtensionInfo[];
|
||||
|
||||
// The link extensions that the plugin registers
|
||||
addedLinks: ExtensionInfo[];
|
||||
|
||||
// The React components that the plugin exposes
|
||||
exposedComponents: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}>;
|
||||
|
||||
// The extension points that the plugin provides
|
||||
extensionPoints: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export enum PluginIncludeType {
|
||||
|
@ -12,6 +12,13 @@ export function usePluginInteractionReporter(): typeof reportInteraction {
|
||||
const context = usePluginContext();
|
||||
|
||||
return useMemo(() => {
|
||||
// Happens when the hook is not used inside a plugin (e.g. in core Grafana)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
`No PluginContext found. The usePluginInteractionReporter() hook can only be used from a plugin.`
|
||||
);
|
||||
}
|
||||
|
||||
const info = isDataSourcePluginContext(context)
|
||||
? createDataSourcePluginEventProperties(context.instanceSettings)
|
||||
: createPluginEventProperties(context.meta);
|
||||
|
@ -18,6 +18,8 @@ import {
|
||||
getThemeById,
|
||||
AngularMeta,
|
||||
PluginLoadingStrategy,
|
||||
PluginDependencies,
|
||||
PluginExtensions,
|
||||
} from '@grafana/data';
|
||||
|
||||
export interface AzureSettings {
|
||||
@ -42,6 +44,8 @@ export type AppPluginConfig = {
|
||||
preload: boolean;
|
||||
angular: AngularMeta;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
dependencies: PluginDependencies;
|
||||
extensions: PluginExtensions;
|
||||
};
|
||||
|
||||
export type PreinstalledPlugin = {
|
||||
|
@ -10,11 +10,11 @@ require (
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
k8s.io/api v0.31.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apiserver v0.31.0
|
||||
k8s.io/client-go v0.31.0
|
||||
k8s.io/component-base v0.31.0
|
||||
k8s.io/api v0.31.1
|
||||
k8s.io/apimachinery v0.31.1
|
||||
k8s.io/apiserver v0.31.1
|
||||
k8s.io/client-go v0.31.1
|
||||
k8s.io/component-base v0.31.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
||||
|
@ -508,16 +508,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
|
||||
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
|
||||
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
|
||||
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
|
||||
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
|
||||
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
|
||||
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
|
||||
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
|
||||
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
|
||||
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
|
||||
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
|
||||
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
|
||||
k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
|
||||
k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
@ -17,6 +17,7 @@ type PluginSetting struct {
|
||||
Info plugins.Info `json:"info"`
|
||||
Includes []*plugins.Includes `json:"includes"`
|
||||
Dependencies plugins.Dependencies `json:"dependencies"`
|
||||
Extensions plugins.Extensions `json:"extensions"`
|
||||
JsonData map[string]any `json:"jsonData"`
|
||||
SecureJsonFields map[string]bool `json:"secureJsonFields"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -39,35 +38,29 @@ const REDACTED = "redacted"
|
||||
func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authorize func(accesscontrol.Evaluator) web.Handler) {
|
||||
// #TODO add back auth part
|
||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(accesscontrol.Parameter(":id"))
|
||||
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
|
||||
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
|
||||
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
||||
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
|
||||
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
|
||||
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
|
||||
|
||||
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
|
||||
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
|
||||
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
||||
})
|
||||
})
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
|
||||
// Use k8s client to implement legacy API
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Get("/", handler.searchFolders)
|
||||
folderRoute.Post("/", handler.createFolder)
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", handler.getFolder)
|
||||
folderUidRoute.Delete("/", handler.deleteFolder)
|
||||
folderUidRoute.Put("/:uid", handler.updateFolder)
|
||||
})
|
||||
} else {
|
||||
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(accesscontrol.Parameter(":id"))
|
||||
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
|
||||
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
|
||||
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
|
||||
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
||||
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
|
||||
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
|
||||
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
|
||||
|
||||
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
|
||||
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
|
||||
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -634,6 +627,8 @@ type folderK8sHandler struct {
|
||||
namespacer request.NamespaceMapper
|
||||
gvr schema.GroupVersionResource
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
// #TODO check if it makes more sense to move this to FolderAPIBuilder
|
||||
accesscontrolService accesscontrol.Service
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
@ -645,34 +640,36 @@ func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler {
|
||||
gvr: folderalpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(hs.Cfg),
|
||||
clientConfigProvider: hs.clientConfigProvider,
|
||||
accesscontrolService: hs.accesscontrolService,
|
||||
}
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) searchFolders(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
out, err := client.List(c.Req.Context(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
// #TODO uncomment when we reinstate their corresponding routes
|
||||
// func (fk8s *folderK8sHandler) searchFolders(c *contextmodel.ReqContext) {
|
||||
// client, ok := fk8s.getClient(c)
|
||||
// if !ok {
|
||||
// return // error is already sent
|
||||
// }
|
||||
// out, err := client.List(c.Req.Context(), v1.ListOptions{})
|
||||
// if err != nil {
|
||||
// fk8s.writeError(c, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
query := strings.ToUpper(c.Query("query"))
|
||||
folders := []folder.Folder{}
|
||||
for _, item := range out.Items {
|
||||
p := internalfolders.UnstructuredToLegacyFolder(item)
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if query != "" && !strings.Contains(strings.ToUpper(p.Title), query) {
|
||||
continue // query filter
|
||||
}
|
||||
folders = append(folders, *p)
|
||||
}
|
||||
c.JSON(http.StatusOK, folders)
|
||||
}
|
||||
// query := strings.ToUpper(c.Query("query"))
|
||||
// folders := []folder.Folder{}
|
||||
// for _, item := range out.Items {
|
||||
// p := internalfolders.UnstructuredToLegacyFolder(item)
|
||||
// if p == nil {
|
||||
// continue
|
||||
// }
|
||||
// if query != "" && !strings.Contains(strings.ToUpper(p.Title), query) {
|
||||
// continue // query filter
|
||||
// }
|
||||
// folders = append(folders, *p)
|
||||
// }
|
||||
// c.JSON(http.StatusOK, folders)
|
||||
// }
|
||||
|
||||
func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
@ -695,6 +692,7 @@ func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
return
|
||||
}
|
||||
|
||||
fk8s.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
@ -703,68 +701,68 @@ func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
c.JSON(http.StatusOK, f)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
// func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
|
||||
// client, ok := fk8s.getClient(c)
|
||||
// if !ok {
|
||||
// return // error is already sent
|
||||
// }
|
||||
// uid := web.Params(c.Req)[":uid"]
|
||||
// out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
|
||||
// if err != nil {
|
||||
// fk8s.writeError(c, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
// f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
|
||||
// if err != nil {
|
||||
// fk8s.writeError(c, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
c.JSON(http.StatusOK, f)
|
||||
}
|
||||
// c.JSON(http.StatusOK, f)
|
||||
// }
|
||||
|
||||
func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, "")
|
||||
}
|
||||
// func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) {
|
||||
// client, ok := fk8s.getClient(c)
|
||||
// if !ok {
|
||||
// return // error is already sent
|
||||
// }
|
||||
// uid := web.Params(c.Req)[":uid"]
|
||||
// err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{})
|
||||
// if err != nil {
|
||||
// fk8s.writeError(c, err)
|
||||
// return
|
||||
// }
|
||||
// c.JSON(http.StatusOK, "")
|
||||
// }
|
||||
|
||||
func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
cmd := folder.UpdateFolderCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||
return
|
||||
}
|
||||
obj := internalfolders.LegacyUpdateCommandToUnstructured(cmd)
|
||||
obj.SetName(uid)
|
||||
out, err := client.Update(c.Req.Context(), &obj, v1.UpdateOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
// func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) {
|
||||
// client, ok := fk8s.getClient(c)
|
||||
// if !ok {
|
||||
// return // error is already sent
|
||||
// }
|
||||
// uid := web.Params(c.Req)[":uid"]
|
||||
// cmd := folder.UpdateFolderCommand{}
|
||||
// if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
// c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||
// return
|
||||
// }
|
||||
// obj := internalfolders.LegacyUpdateCommandToUnstructured(cmd)
|
||||
// obj.SetName(uid)
|
||||
// out, err := client.Update(c.Req.Context(), &obj, v1.UpdateOptions{})
|
||||
// if err != nil {
|
||||
// fk8s.writeError(c, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
// f, err := internalfolders.UnstructuredToLegacyFolderDTO(*out)
|
||||
// if err != nil {
|
||||
// fk8s.writeError(c, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
c.JSON(http.StatusOK, f)
|
||||
}
|
||||
// c.JSON(http.StatusOK, f)
|
||||
// }
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// Utility functions
|
||||
|
@ -561,6 +561,8 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin,
|
||||
Preload: false,
|
||||
Angular: plugin.Angular,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
||||
Extensions: plugin.Extensions,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if settings.Enabled {
|
||||
|
@ -209,6 +209,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
||||
SecureJsonFields: map[string]bool{},
|
||||
AngularDetected: plugin.Angular.Detected,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
||||
Extensions: plugin.Extensions,
|
||||
}
|
||||
|
||||
if plugin.IsApp() {
|
||||
|
@ -6,8 +6,8 @@ require (
|
||||
github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/claims v0.0.0-20240903121118-16441568af1e // @grafana/identity-access-team
|
||||
github.com/stretchr/testify v1.9.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apiserver v0.31.0
|
||||
k8s.io/apimachinery v0.31.1
|
||||
k8s.io/apiserver v0.31.1
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
|
||||
)
|
||||
|
||||
|
@ -152,10 +152,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
|
||||
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
|
||||
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
@ -11,9 +11,9 @@ require (
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.30.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apiserver v0.31.0
|
||||
k8s.io/component-base v0.31.0
|
||||
k8s.io/apimachinery v0.31.1
|
||||
k8s.io/apiserver v0.31.1
|
||||
k8s.io/component-base v0.31.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
||||
@ -89,8 +89,8 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.31.0 // indirect
|
||||
k8s.io/client-go v0.31.0 // indirect
|
||||
k8s.io/api v0.31.1 // indirect
|
||||
k8s.io/client-go v0.31.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
|
@ -325,16 +325,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
|
||||
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
|
||||
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
|
||||
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
|
||||
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
|
||||
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
|
||||
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
|
||||
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
|
||||
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
|
||||
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
|
||||
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
|
||||
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
|
||||
k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
|
||||
k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
@ -50,6 +50,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Backend: true,
|
||||
@ -82,6 +91,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
|
||||
@ -104,6 +122,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
|
||||
@ -150,6 +177,9 @@ func TestFinder_Find(t *testing.T) {
|
||||
{ID: "graphite", Type: "datasource", Name: "Graphite", Version: "1.0.0"},
|
||||
{ID: "graph", Type: "panel", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
@ -169,6 +199,12 @@ func TestFinder_Find(t *testing.T) {
|
||||
{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Action: "plugins.app:access"},
|
||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Action: "plugins.app:access"},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "includes-symlinks")),
|
||||
},
|
||||
@ -197,6 +233,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested")),
|
||||
@ -219,6 +264,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testData, "duplicate-plugins/nested/nested")),
|
||||
@ -241,6 +295,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Backend: true,
|
||||
@ -272,6 +335,15 @@ func TestFinder_Find(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Backend: true,
|
||||
|
@ -98,6 +98,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Category: "cloud",
|
||||
Annotations: true,
|
||||
@ -141,6 +150,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Executable: "test",
|
||||
Backend: true,
|
||||
@ -195,6 +213,9 @@ func TestLoader_Load(t *testing.T) {
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
@ -228,6 +249,12 @@ func TestLoader_Load(t *testing.T) {
|
||||
Slug: "nginx-datasource",
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-app/module.js",
|
||||
@ -266,6 +293,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
@ -312,6 +348,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
@ -393,11 +438,20 @@ func TestLoader_Load(t *testing.T) {
|
||||
GrafanaDependency: ">=8.0.0",
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-memory"},
|
||||
{Name: "Root Page (react)", Type: "page", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Path: "/a/my-simple-app", DefaultNav: true, AddToNav: true, Slug: "root-page-react"},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
DefaultNavURL: "/plugins/test-app/page/root-page-react",
|
||||
|
@ -1,6 +1,7 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@ -42,9 +43,100 @@ func (e DuplicateError) Is(err error) bool {
|
||||
}
|
||||
|
||||
type Dependencies struct {
|
||||
GrafanaDependency string `json:"grafanaDependency"`
|
||||
GrafanaVersion string `json:"grafanaVersion"`
|
||||
Plugins []Dependency `json:"plugins"`
|
||||
GrafanaDependency string `json:"grafanaDependency"`
|
||||
GrafanaVersion string `json:"grafanaVersion"`
|
||||
Plugins []Dependency `json:"plugins"`
|
||||
Extensions ExtensionsDependencies `json:"extensions"`
|
||||
}
|
||||
|
||||
// We need different versions for the Extensions struct because there is a now deprecated plugin.json schema out there, where the "extensions" prop
|
||||
// is in a different format (Extensions V1). In order to support those as well while reading the plugin.json, we need to add a custom unmarshaling logic for extensions.
|
||||
type ExtensionV1 struct {
|
||||
ExtensionPointID string `json:"extensionPointId"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type ExtensionsV2 struct {
|
||||
AddedLinks []AddedLink `json:"addedLinks"`
|
||||
AddedComponents []AddedComponent `json:"addedComponents"`
|
||||
ExposedComponents []ExposedComponent `json:"exposedComponents"`
|
||||
ExtensionPoints []ExtensionPoint `json:"extensionPoints"`
|
||||
}
|
||||
|
||||
type Extensions ExtensionsV2
|
||||
|
||||
func (e *Extensions) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var extensionsV2 ExtensionsV2
|
||||
|
||||
if err = json.Unmarshal(data, &extensionsV2); err == nil {
|
||||
e.AddedComponents = extensionsV2.AddedComponents
|
||||
e.AddedLinks = extensionsV2.AddedLinks
|
||||
e.ExposedComponents = extensionsV2.ExposedComponents
|
||||
e.ExtensionPoints = extensionsV2.ExtensionPoints
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback (V1)
|
||||
var extensionsV1 []ExtensionV1
|
||||
if err = json.Unmarshal(data, &extensionsV1); err == nil {
|
||||
// Trying to process old format and add them to `AddedLinks` and `AddedComponents`
|
||||
for _, extensionV1 := range extensionsV1 {
|
||||
if extensionV1.Type == "link" {
|
||||
extensionV2 := AddedLink{
|
||||
Targets: []string{extensionV1.ExtensionPointID},
|
||||
Title: extensionV1.Title,
|
||||
Description: extensionV1.Description,
|
||||
}
|
||||
e.AddedLinks = append(e.AddedLinks, extensionV2)
|
||||
}
|
||||
|
||||
if extensionV1.Type == "component" {
|
||||
extensionV2 := AddedComponent{
|
||||
Targets: []string{extensionV1.ExtensionPointID},
|
||||
Title: extensionV1.Title,
|
||||
Description: extensionV1.Description,
|
||||
}
|
||||
|
||||
e.AddedComponents = append(e.AddedComponents, extensionV2)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type AddedLink struct {
|
||||
Targets []string `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type AddedComponent struct {
|
||||
Targets []string `json:"targets"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ExposedComponent struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ExtensionPoint struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ExtensionsDependencies struct {
|
||||
ExposedComponents []string `json:"exposedComponents"`
|
||||
}
|
||||
|
||||
type Includes struct {
|
||||
@ -231,6 +323,8 @@ type AppDTO struct {
|
||||
Preload bool `json:"preload"`
|
||||
Angular AngularMeta `json:"angular"`
|
||||
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
|
||||
Extensions Extensions `json:"extensions"`
|
||||
Dependencies Dependencies `json:"dependencies"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -109,7 +109,8 @@ type JSONData struct {
|
||||
SkipDataQuery bool `json:"skipDataQuery"`
|
||||
|
||||
// App settings
|
||||
AutoEnabled bool `json:"autoEnabled"`
|
||||
AutoEnabled bool `json:"autoEnabled"`
|
||||
Extensions Extensions `json:"extensions"`
|
||||
|
||||
// Datasource settings
|
||||
Annotations bool `json:"annotations"`
|
||||
@ -173,6 +174,26 @@ func ReadPluginJSON(reader io.Reader) (JSONData, error) {
|
||||
plugin.Dependencies.GrafanaVersion = "*"
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies.Extensions.ExposedComponents) == 0 {
|
||||
plugin.Dependencies.Extensions.ExposedComponents = make([]string, 0)
|
||||
}
|
||||
|
||||
if plugin.Extensions.AddedLinks == nil {
|
||||
plugin.Extensions.AddedLinks = []AddedLink{}
|
||||
}
|
||||
|
||||
if plugin.Extensions.AddedComponents == nil {
|
||||
plugin.Extensions.AddedComponents = []AddedComponent{}
|
||||
}
|
||||
|
||||
if plugin.Extensions.ExposedComponents == nil {
|
||||
plugin.Extensions.ExposedComponents = []ExposedComponent{}
|
||||
}
|
||||
|
||||
if plugin.Extensions.ExtensionPoints == nil {
|
||||
plugin.Extensions.ExtensionPoints = []ExtensionPoint{}
|
||||
}
|
||||
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Role == "" {
|
||||
include.Role = org.RoleViewer
|
||||
|
@ -52,13 +52,25 @@ func Test_ReadPluginJSON(t *testing.T) {
|
||||
Updated: "2015-02-10",
|
||||
Keywords: []string{"test"},
|
||||
},
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{},
|
||||
AddedComponents: []AddedComponent{},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
|
||||
Includes: []*Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
|
||||
@ -94,10 +106,22 @@ func Test_ReadPluginJSON(t *testing.T) {
|
||||
ID: "grafana-piechart-panel",
|
||||
Type: TypePanel,
|
||||
Name: "Pie Chart (old)",
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{},
|
||||
AddedComponents: []AddedComponent{},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
|
||||
Includes: []*Includes{
|
||||
{Name: "Pie Charts", Path: "dashboards/demo.json", Type: "dashboard", Role: org.RoleViewer},
|
||||
},
|
||||
@ -117,10 +141,21 @@ func Test_ReadPluginJSON(t *testing.T) {
|
||||
ID: "grafana-pyroscope-datasource",
|
||||
AliasIDs: []string{"phlare"}, // Hardcoded from the parser
|
||||
Type: TypeDataSource,
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{},
|
||||
AddedComponents: []AddedComponent{},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaDependency: "",
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -142,18 +177,257 @@ func Test_ReadPluginJSON(t *testing.T) {
|
||||
Dependencies: Dependencies{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "can read the latest versions of extensions information (v2)",
|
||||
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||
pJSON := `{
|
||||
"id": "myorg-extensions-app",
|
||||
"name": "Extensions App",
|
||||
"type": "app",
|
||||
"extensions": {
|
||||
"addedLinks": [
|
||||
{
|
||||
"title": "Added link 1",
|
||||
"description": "Added link 1 description",
|
||||
"targets": ["grafana/dashboard/panel/menu"]
|
||||
}
|
||||
],
|
||||
"addedComponents": [
|
||||
{
|
||||
"title": "Added component 1",
|
||||
"description": "Added component 1 description",
|
||||
"targets": ["grafana/user/profile/tab"]
|
||||
}
|
||||
],
|
||||
"exposedComponents": [
|
||||
{
|
||||
"title": "Exposed component 1",
|
||||
"description": "Exposed component 1 description",
|
||||
"id": "myorg-extensions-app/component-1/v1"
|
||||
}
|
||||
],
|
||||
"extensionPoints": [
|
||||
{
|
||||
"title": "Extension point 1",
|
||||
"description": "Extension points 1 description",
|
||||
"id": "myorg-extensions-app/extensions-point-1/v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
return io.NopCloser(strings.NewReader(pJSON))
|
||||
},
|
||||
expected: JSONData{
|
||||
ID: "myorg-extensions-app",
|
||||
Name: "Extensions App",
|
||||
Type: TypeApp,
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{
|
||||
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||
},
|
||||
AddedComponents: []AddedComponent{
|
||||
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||
},
|
||||
ExposedComponents: []ExposedComponent{
|
||||
{Id: "myorg-extensions-app/component-1/v1", Title: "Exposed component 1", Description: "Exposed component 1 description"},
|
||||
},
|
||||
ExtensionPoints: []ExtensionPoint{
|
||||
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
|
||||
},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "can read deprecated extensions info (v1) and parse it as v2",
|
||||
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||
pJSON := `{
|
||||
"id": "myorg-extensions-app",
|
||||
"name": "Extensions App",
|
||||
"type": "app",
|
||||
"extensions": [
|
||||
{
|
||||
"extensionPointId": "grafana/dashboard/panel/menu",
|
||||
"title": "Added link 1",
|
||||
"description": "Added link 1 description",
|
||||
"type": "link"
|
||||
},
|
||||
{
|
||||
"extensionPointId": "grafana/dashboard/panel/menu",
|
||||
"title": "Added link 2",
|
||||
"description": "Added link 2 description",
|
||||
"type": "link"
|
||||
},
|
||||
{
|
||||
"extensionPointId": "grafana/user/profile/tab",
|
||||
"title": "Added component 1",
|
||||
"description": "Added component 1 description",
|
||||
"type": "component"
|
||||
}
|
||||
]
|
||||
}`
|
||||
return io.NopCloser(strings.NewReader(pJSON))
|
||||
},
|
||||
expected: JSONData{
|
||||
ID: "myorg-extensions-app",
|
||||
Name: "Extensions App",
|
||||
Type: TypeApp,
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{
|
||||
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||
{Title: "Added link 2", Description: "Added link 2 description", Targets: []string{"grafana/dashboard/panel/menu"}},
|
||||
},
|
||||
AddedComponents: []AddedComponent{
|
||||
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
|
||||
},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "works if extensions info is empty",
|
||||
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||
pJSON := `{
|
||||
"id": "myorg-extensions-app",
|
||||
"name": "Extensions App",
|
||||
"type": "app",
|
||||
"extensions": []
|
||||
}`
|
||||
return io.NopCloser(strings.NewReader(pJSON))
|
||||
},
|
||||
expected: JSONData{
|
||||
ID: "myorg-extensions-app",
|
||||
Name: "Extensions App",
|
||||
Type: TypeApp,
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{},
|
||||
AddedComponents: []AddedComponent{},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "works if extensions info is completely missing",
|
||||
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||
pJSON := `{
|
||||
"id": "myorg-extensions-app",
|
||||
"name": "Extensions App",
|
||||
"type": "app"
|
||||
}`
|
||||
return io.NopCloser(strings.NewReader(pJSON))
|
||||
},
|
||||
expected: JSONData{
|
||||
ID: "myorg-extensions-app",
|
||||
Name: "Extensions App",
|
||||
Type: TypeApp,
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{},
|
||||
AddedComponents: []AddedComponent{},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "can read extensions related dependencies",
|
||||
pluginJSON: func(t *testing.T) io.ReadCloser {
|
||||
pJSON := `{
|
||||
"id": "myorg-extensions-app",
|
||||
"name": "Extensions App",
|
||||
"type": "app",
|
||||
"dependencies": {
|
||||
"grafanaDependency": "10.0.0",
|
||||
"extensions": {
|
||||
"exposedComponents": ["myorg-extensions-app/component-1/v1"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
return io.NopCloser(strings.NewReader(pJSON))
|
||||
},
|
||||
expected: JSONData{
|
||||
ID: "myorg-extensions-app",
|
||||
Name: "Extensions App",
|
||||
Type: TypeApp,
|
||||
|
||||
Extensions: Extensions{
|
||||
AddedLinks: []AddedLink{},
|
||||
AddedComponents: []AddedComponent{},
|
||||
ExposedComponents: []ExposedComponent{},
|
||||
ExtensionPoints: []ExtensionPoint{},
|
||||
},
|
||||
|
||||
Dependencies: Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
GrafanaDependency: "10.0.0",
|
||||
Plugins: []Dependency{},
|
||||
Extensions: ExtensionsDependencies{
|
||||
ExposedComponents: []string{"myorg-extensions-app/component-1/v1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := tt.pluginJSON(t)
|
||||
got, err := ReadPluginJSON(p)
|
||||
|
||||
// Check if the test returns the same error as expected
|
||||
// (unneccary to check further if there is an error at this point)
|
||||
if tt.err == nil && err != nil {
|
||||
t.Errorf("Error while reading pluginJSON: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the test returns the same error as expected
|
||||
if tt.err != nil {
|
||||
require.ErrorIs(t, err, tt.err)
|
||||
}
|
||||
|
||||
// Check if the test returns the expected pluginJSONData
|
||||
if !cmp.Equal(got, tt.expected) {
|
||||
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected))
|
||||
}
|
||||
|
||||
// Should be able to close the reader
|
||||
require.NoError(t, p.Close())
|
||||
})
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ require (
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apimachinery v0.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -373,8 +373,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
@ -6,12 +6,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
@ -32,6 +31,8 @@ type AccessControl interface {
|
||||
// This is useful when we don't want to reuse any pre-configured resolvers
|
||||
// for a authorization call.
|
||||
WithoutResolvers() AccessControl
|
||||
Check(ctx context.Context, req CheckRequest) (bool, error)
|
||||
ListObjects(ctx context.Context, req ListObjectsRequest) ([]string, error)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
|
@ -119,26 +119,24 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque
|
||||
|
||||
return eval.EvaluateCustom(func(action, scope string) (bool, error) {
|
||||
kind, _, identifier := accesscontrol.SplitScope(scope)
|
||||
key, ok := zanzana.TranslateToTuple(user.GetUID(), action, kind, identifier, user.GetOrgID())
|
||||
tupleKey, ok := zanzana.TranslateToTuple(user.GetUID(), action, kind, identifier, user.GetOrgID())
|
||||
if !ok {
|
||||
// unsupported translation
|
||||
return false, errAccessNotImplemented
|
||||
}
|
||||
|
||||
a.log.Debug("evaluating zanzana", "user", key.User, "relation", key.Relation, "object", key.Object)
|
||||
res, err := a.zclient.Check(ctx, &openfgav1.CheckRequest{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: key.User,
|
||||
Relation: key.Relation,
|
||||
Object: key.Object,
|
||||
},
|
||||
a.log.Debug("evaluating zanzana", "user", tupleKey.User, "relation", tupleKey.Relation, "object", tupleKey.Object)
|
||||
allowed, err := a.Check(ctx, accesscontrol.CheckRequest{
|
||||
User: tupleKey.User,
|
||||
Relation: tupleKey.Relation,
|
||||
Object: tupleKey.Object,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return res.Allowed, nil
|
||||
return allowed, nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -221,3 +219,30 @@ func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg
|
||||
|
||||
a.log.FromContext(ctx).Debug(msg, "id", ident.GetID(), "orgID", ident.GetOrgID(), "permissions", eval.GoString())
|
||||
}
|
||||
|
||||
func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckRequest) (bool, error) {
|
||||
key := &openfgav1.CheckRequestTupleKey{
|
||||
User: req.User,
|
||||
Relation: req.Relation,
|
||||
Object: req.Object,
|
||||
}
|
||||
in := &openfgav1.CheckRequest{TupleKey: key}
|
||||
res, err := a.zclient.Check(ctx, in)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Allowed, err
|
||||
}
|
||||
|
||||
func (a *AccessControl) ListObjects(ctx context.Context, req accesscontrol.ListObjectsRequest) ([]string, error) {
|
||||
in := &openfgav1.ListObjectsRequest{
|
||||
Type: req.Type,
|
||||
User: req.User,
|
||||
Relation: req.Relation,
|
||||
}
|
||||
res, err := a.zclient.ListObjects(ctx, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Objects, err
|
||||
}
|
||||
|
@ -8,11 +8,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
|
@ -75,6 +75,14 @@ func (f FakeAccessControl) Evaluate(ctx context.Context, user identity.Requester
|
||||
func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||
}
|
||||
|
||||
func (f FakeAccessControl) Check(ctx context.Context, in accesscontrol.CheckRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (f FakeAccessControl) ListObjects(ctx context.Context, in accesscontrol.ListObjectsRequest) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f FakeAccessControl) WithoutResolvers() accesscontrol.AccessControl {
|
||||
return f
|
||||
}
|
||||
|
@ -94,12 +94,12 @@ func (z *ZanzanaSynchroniser) Sync(ctx context.Context) error {
|
||||
func managedPermissionsCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "managed"
|
||||
const query = `
|
||||
query := `
|
||||
SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id
|
||||
FROM permission p
|
||||
INNER JOIN role r ON p.role_id = r.id
|
||||
LEFT JOIN user_role ur ON r.id = ur.role_id
|
||||
LEFT JOIN user u ON u.id = ur.user_id
|
||||
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id
|
||||
LEFT JOIN team_role tr ON r.id = tr.role_id
|
||||
LEFT JOIN team t ON tr.team_id = t.id
|
||||
LEFT JOIN builtin_role br ON r.id = br.role_id
|
||||
@ -156,11 +156,11 @@ func teamMembershipCollector(store db.DB) TupleCollector {
|
||||
defer span.End()
|
||||
|
||||
const collectorID = "team_membership"
|
||||
const query = `
|
||||
query := `
|
||||
SELECT t.uid as team_uid, u.uid as user_uid, tm.permission
|
||||
FROM team_member tm
|
||||
INNER JOIN team t ON tm.team_id = t.id
|
||||
INNER JOIN user u ON tm.user_id = u.id
|
||||
INNER JOIN ` + store.GetDialect().Quote("user") + ` u ON tm.user_id = u.id
|
||||
`
|
||||
|
||||
type membership struct {
|
||||
@ -253,8 +253,10 @@ func dashboardFolderCollector(store db.DB) TupleCollector {
|
||||
defer span.End()
|
||||
|
||||
const collectorID = "folder"
|
||||
const query = `
|
||||
SELECT org_id, uid, folder_uid, is_folder FROM dashboard WHERE is_folder = 0 AND folder_uid IS NOT NULL
|
||||
query := `
|
||||
SELECT org_id, uid, folder_uid, is_folder FROM dashboard
|
||||
WHERE is_folder = ` + store.GetDialect().BooleanStr(false) + `
|
||||
AND folder_uid IS NOT NULL
|
||||
`
|
||||
type dashboard struct {
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
@ -426,10 +428,10 @@ func customRolesCollector(store db.DB) TupleCollector {
|
||||
func basicRoleAssignemtCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "basic_role_assignment"
|
||||
const query = `
|
||||
query := `
|
||||
SELECT ou.org_id, u.uid as user_uid, ou.role as org_role, u.is_admin
|
||||
FROM org_user ou
|
||||
LEFT JOIN user u ON u.id = ou.user_id
|
||||
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ou.user_id
|
||||
`
|
||||
type Assignment struct {
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
@ -474,11 +476,11 @@ func basicRoleAssignemtCollector(store db.DB) TupleCollector {
|
||||
func userRoleAssignemtCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "user_role_assignment"
|
||||
const query = `
|
||||
query := `
|
||||
SELECT ur.org_id, u.uid AS user_uid, r.uid AS role_uid, r.name AS role_name
|
||||
FROM user_role ur
|
||||
LEFT JOIN role r ON r.id = ur.role_id
|
||||
LEFT JOIN user u ON u.id = ur.user_id
|
||||
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id
|
||||
WHERE r.name NOT LIKE 'managed:%'
|
||||
`
|
||||
|
||||
|
@ -266,6 +266,14 @@ func (m *Mock) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mock) Check(ctx context.Context, in accesscontrol.CheckRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *Mock) ListObjects(ctx context.Context, in accesscontrol.ListObjectsRequest) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// WithoutResolvers implements fullAccessControl.
|
||||
func (m *Mock) WithoutResolvers() accesscontrol.AccessControl {
|
||||
return m
|
||||
|
@ -585,3 +585,15 @@ type QueryWithOrg struct {
|
||||
OrgId *int64 `json:"orgId"`
|
||||
Global bool `json:"global"`
|
||||
}
|
||||
|
||||
type CheckRequest struct {
|
||||
User string
|
||||
Relation string
|
||||
Object string
|
||||
}
|
||||
|
||||
type ListObjectsRequest struct {
|
||||
Type string
|
||||
Relation string
|
||||
User string
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"github.com/openfga/language/pkg/go/transformer"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
@ -14,6 +16,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/schema"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/authz/zanzana/client")
|
||||
|
||||
type ClientOption func(c *Client)
|
||||
|
||||
func WithTenantID(tenantID string) ClientOption {
|
||||
@ -82,12 +86,19 @@ func New(ctx context.Context, cc grpc.ClientConnInterface, opts ...ClientOption)
|
||||
}
|
||||
|
||||
func (c *Client) Check(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Check")
|
||||
defer span.End()
|
||||
|
||||
in.StoreId = c.storeID
|
||||
in.AuthorizationModelId = c.modelID
|
||||
return c.client.Check(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authz.zanzana.client.ListObjects")
|
||||
span.SetAttributes(attribute.String("resource.type", in.Type))
|
||||
defer span.End()
|
||||
|
||||
in.StoreId = c.storeID
|
||||
in.AuthorizationModelId = c.modelID
|
||||
return c.client.ListObjects(ctx, in)
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@ -25,7 +27,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboard/database")
|
||||
@ -942,7 +943,9 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
||||
filters = append(filters, searchstore.K6FolderFilter{})
|
||||
}
|
||||
|
||||
filters = append(filters, permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported))
|
||||
if !query.SkipAccessControlFilter {
|
||||
filters = append(filters, permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported))
|
||||
}
|
||||
|
||||
filters = append(filters, searchstore.DeletedFilter{Deleted: query.IsDeleted})
|
||||
|
||||
|
@ -130,6 +130,7 @@ var (
|
||||
ErrFolderInvalidUID = errors.New("invalid uid for folder provided")
|
||||
ErrFolderSameNameExists = errors.New("a folder with the same name already exists in the current location")
|
||||
ErrFolderAccessDenied = errors.New("access denied to folder")
|
||||
ErrUserIsNotSignedInToOrg = errors.New("user is not signed in to organization")
|
||||
ErrMoveAccessDenied = errutil.Forbidden("folders.forbiddenMove", errutil.WithPublicMessage("Access denied to the destination folder"))
|
||||
ErrFolderAccessEscalation = errutil.Forbidden("folders.accessEscalation", errutil.WithPublicMessage("Cannot move a folder to a folder where you have higher permissions"))
|
||||
ErrFolderCreationAccessDenied = errutil.Forbidden("folders.forbiddenCreation", errutil.WithPublicMessage("not enough permissions to create a folder in the selected location"))
|
||||
|
@ -423,4 +423,8 @@ type FindPersistedDashboardsQuery struct {
|
||||
IsDeleted bool
|
||||
|
||||
Filters []any
|
||||
|
||||
// Skip access control checks. This field is used by OpenFGA search implementation.
|
||||
// Should not be used anywhere else.
|
||||
SkipAccessControlFilter bool
|
||||
}
|
||||
|
@ -8,12 +8,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@ -716,7 +717,13 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards")
|
||||
defer span.End()
|
||||
|
||||
res, err := dr.FindDashboards(ctx, query)
|
||||
var res []dashboards.DashboardSearchProjection
|
||||
var err error
|
||||
if dr.features.IsEnabled(ctx, featuremgmt.FlagZanzana) {
|
||||
res, err = dr.FindDashboardsZanzana(ctx, query)
|
||||
} else {
|
||||
res, err = dr.FindDashboards(ctx, query)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -10,8 +10,12 @@ const (
|
||||
metricsSubSystem = "dashboards"
|
||||
)
|
||||
|
||||
var defaultBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25}
|
||||
|
||||
type dashboardsMetrics struct {
|
||||
sharedWithMeFetchDashboardsRequestsDuration *prometheus.HistogramVec
|
||||
searchRequestsDuration *prometheus.HistogramVec
|
||||
searchRequestStatusTotal *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func newDashboardsMetrics(r prometheus.Registerer) *dashboardsMetrics {
|
||||
@ -20,7 +24,27 @@ func newDashboardsMetrics(r prometheus.Registerer) *dashboardsMetrics {
|
||||
prometheus.HistogramOpts{
|
||||
Name: "sharedwithme_fetch_dashboards_duration_seconds",
|
||||
Help: "Duration of fetching dashboards with permissions directly assigned to user",
|
||||
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100},
|
||||
Buckets: defaultBuckets,
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
},
|
||||
[]string{"status"},
|
||||
),
|
||||
searchRequestsDuration: promauto.With(r).NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "search_dashboards_duration_seconds",
|
||||
Help: "Duration of dashboards search (by authorization engine)",
|
||||
Buckets: defaultBuckets,
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
},
|
||||
[]string{"engine"},
|
||||
),
|
||||
|
||||
searchRequestStatusTotal: promauto.With(r).NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "search_dashboards_status_total",
|
||||
Help: "Search status (success or error) for zanzana",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: metricsSubSystem,
|
||||
},
|
||||
|
342
pkg/services/dashboards/service/zanzana.go
Normal file
342
pkg/services/dashboards/service/zanzana.go
Normal file
@ -0,0 +1,342 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
)
|
||||
|
||||
const (
|
||||
// If search query string shorter than this value, then "List, then check" strategy will be used
|
||||
listQueryLengthThreshold = 8
|
||||
// If query limit set to value higher than this value, then "List, then check" strategy will be used
|
||||
listQueryLimitThreshold = 50
|
||||
defaultQueryLimit = 1000
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
runner string
|
||||
result []dashboards.DashboardSearchProjection
|
||||
err error
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) FindDashboardsZanzana(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
if dr.cfg.Zanzana.ZanzanaOnlyEvaluation {
|
||||
return dr.findDashboardsZanzanaOnly(ctx, *query)
|
||||
}
|
||||
return dr.findDashboardsZanzanaCompare(ctx, *query)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaOnly(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
timer := prometheus.NewTimer(dr.metrics.searchRequestsDuration.WithLabelValues("zanzana"))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
return dr.findDashboardsZanzana(ctx, query)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaCompare(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
result := make(chan searchResult, 2)
|
||||
|
||||
go func() {
|
||||
timer := prometheus.NewTimer(dr.metrics.searchRequestsDuration.WithLabelValues("zanzana"))
|
||||
defer timer.ObserveDuration()
|
||||
start := time.Now()
|
||||
|
||||
queryZanzana := query
|
||||
res, err := dr.findDashboardsZanzana(ctx, queryZanzana)
|
||||
result <- searchResult{"zanzana", res, err, time.Since(start)}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
timer := prometheus.NewTimer(dr.metrics.searchRequestsDuration.WithLabelValues("grafana"))
|
||||
defer timer.ObserveDuration()
|
||||
start := time.Now()
|
||||
|
||||
res, err := dr.FindDashboards(ctx, &query)
|
||||
result <- searchResult{"grafana", res, err, time.Since(start)}
|
||||
}()
|
||||
|
||||
first, second := <-result, <-result
|
||||
close(result)
|
||||
|
||||
if second.runner == "grafana" {
|
||||
first, second = second, first
|
||||
}
|
||||
|
||||
if second.err != nil {
|
||||
dr.log.Error("zanzana search failed", "error", second.err)
|
||||
dr.metrics.searchRequestStatusTotal.WithLabelValues("error").Inc()
|
||||
} else if len(first.result) != len(second.result) {
|
||||
dr.metrics.searchRequestStatusTotal.WithLabelValues("error").Inc()
|
||||
dr.log.Warn(
|
||||
"zanzana search result does not match grafana",
|
||||
"grafana_result_len", len(first.result),
|
||||
"zanana_result_len", len(second.result),
|
||||
"grafana_duration", first.duration,
|
||||
"zanzana_duration", second.duration,
|
||||
)
|
||||
} else {
|
||||
dr.metrics.searchRequestStatusTotal.WithLabelValues("success").Inc()
|
||||
dr.log.Debug("zanzana search is correct", "result_len", len(first.result), "grafana_duration", first.duration, "zanzana_duration", second.duration)
|
||||
}
|
||||
|
||||
return first.result, first.err
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzana(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
findDashboards := dr.getFindDashboardsFn(query)
|
||||
return findDashboards(ctx, query)
|
||||
}
|
||||
|
||||
type findDashboardsFn func(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error)
|
||||
|
||||
// getFindDashboardsFn makes a decision which search method should be used
|
||||
func (dr *DashboardServiceImpl) getFindDashboardsFn(query dashboards.FindPersistedDashboardsQuery) findDashboardsFn {
|
||||
if query.Limit > 0 && query.Limit < listQueryLimitThreshold && len(query.Title) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
if len(query.DashboardUIDs) > 0 || len(query.DashboardIds) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
if len(query.FolderUIDs) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
if len(query.Title) <= listQueryLengthThreshold {
|
||||
return dr.findDashboardsZanzanaList
|
||||
}
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
|
||||
// findDashboardsZanzanaCheck implements "Search, then check" strategy. It first performs search query, then filters out results
|
||||
// by checking access to each item.
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaCheck(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaCheck")
|
||||
defer span.End()
|
||||
|
||||
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
|
||||
var page int64 = 1
|
||||
query.SkipAccessControlFilter = true
|
||||
// Remember initial query limit
|
||||
limit := query.Limit
|
||||
// Set limit to default to prevent pagination issues
|
||||
query.Limit = defaultQueryLimit
|
||||
|
||||
for len(result) < int(limit) {
|
||||
query.Page = page
|
||||
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remains := limit - int64(len(result))
|
||||
res, err := dr.checkDashboards(ctx, query, findRes, remains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, res...)
|
||||
page++
|
||||
|
||||
// Stop when last page reached
|
||||
if len(findRes) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) checkDashboards(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, searchRes []dashboards.DashboardSearchProjection, remains int64) ([]dashboards.DashboardSearchProjection, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.checkDashboards")
|
||||
defer span.End()
|
||||
|
||||
if len(searchRes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
orgId := query.OrgId
|
||||
if orgId == 0 && query.SignedInUser.GetOrgID() != 0 {
|
||||
orgId = query.SignedInUser.GetOrgID()
|
||||
} else {
|
||||
return nil, dashboards.ErrUserIsNotSignedInToOrg
|
||||
}
|
||||
|
||||
concurrentRequests := dr.cfg.Zanzana.ConcurrentChecks
|
||||
var wg sync.WaitGroup
|
||||
res := make([]dashboards.DashboardSearchProjection, 0)
|
||||
resToCheck := make(chan dashboards.DashboardSearchProjection, concurrentRequests)
|
||||
allowedResults := make(chan dashboards.DashboardSearchProjection, len(searchRes))
|
||||
|
||||
for i := 0; i < int(concurrentRequests); i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for d := range resToCheck {
|
||||
if int64(len(allowedResults)) >= remains {
|
||||
return
|
||||
}
|
||||
|
||||
objectType := zanzana.TypeDashboard
|
||||
if d.IsFolder {
|
||||
objectType = zanzana.TypeFolder
|
||||
}
|
||||
|
||||
req := accesscontrol.CheckRequest{
|
||||
User: query.SignedInUser.GetUID(),
|
||||
Relation: "read",
|
||||
Object: zanzana.NewScopedTupleEntry(objectType, d.UID, "", strconv.FormatInt(orgId, 10)),
|
||||
}
|
||||
|
||||
allowed, err := dr.ac.Check(ctx, req)
|
||||
if err != nil {
|
||||
dr.log.Error("error checking access", "error", err)
|
||||
} else if allowed {
|
||||
allowedResults <- d
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, r := range searchRes {
|
||||
resToCheck <- r
|
||||
}
|
||||
close(resToCheck)
|
||||
|
||||
wg.Wait()
|
||||
close(allowedResults)
|
||||
|
||||
for r := range allowedResults {
|
||||
if int64(len(res)) >= remains {
|
||||
break
|
||||
}
|
||||
res = append(res, r)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// findDashboardsZanzanaList implements "List, then search" strategy. It first retrieve a list of resources
|
||||
// with given type available to the user and then passes that list as a filter to the search query.
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaList(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
// Always use "search, then check" if dashboard or folder UIDs provided. Otherwise we should make intersection
|
||||
// of user's resources and provided UIDs which might not be correct if ListObjects() request is limited by OpenFGA.
|
||||
if len(query.DashboardUIDs) > 0 || len(query.DashboardIds) > 0 || len(query.FolderUIDs) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck(ctx, query)
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaList")
|
||||
defer span.End()
|
||||
|
||||
resourceUIDs, err := dr.listUserResources(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resourceUIDs) == 0 {
|
||||
return []dashboards.DashboardSearchProjection{}, nil
|
||||
}
|
||||
|
||||
query.DashboardUIDs = resourceUIDs
|
||||
query.SkipAccessControlFilter = true
|
||||
return dr.dashboardStore.FindDashboards(ctx, &query)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) listUserResources(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]string, error) {
|
||||
tasks := make([]func() ([]string, error), 0)
|
||||
var resourceTypes []string
|
||||
|
||||
// For some search types we need dashboards or folders only
|
||||
switch query.Type {
|
||||
case searchstore.TypeDashboard:
|
||||
resourceTypes = []string{zanzana.TypeDashboard}
|
||||
case searchstore.TypeFolder, searchstore.TypeAlertFolder:
|
||||
resourceTypes = []string{zanzana.TypeFolder}
|
||||
default:
|
||||
resourceTypes = []string{zanzana.TypeDashboard, zanzana.TypeFolder}
|
||||
}
|
||||
|
||||
for _, resourceType := range resourceTypes {
|
||||
tasks = append(tasks, func() ([]string, error) {
|
||||
return dr.listAllowedResources(ctx, query, resourceType)
|
||||
})
|
||||
}
|
||||
|
||||
uids, err := runBatch(tasks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return uids, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) listAllowedResources(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, resourceType string) ([]string, error) {
|
||||
res, err := dr.ac.ListObjects(ctx, accesscontrol.ListObjectsRequest{
|
||||
User: query.SignedInUser.GetUID(),
|
||||
Type: resourceType,
|
||||
Relation: "read",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgId := query.OrgId
|
||||
if orgId == 0 && query.SignedInUser.GetOrgID() != 0 {
|
||||
orgId = query.SignedInUser.GetOrgID()
|
||||
} else {
|
||||
return nil, dashboards.ErrUserIsNotSignedInToOrg
|
||||
}
|
||||
// dashboard:<orgId>-
|
||||
prefix := fmt.Sprintf("%s:%d-", resourceType, orgId)
|
||||
|
||||
resourceUIDs := make([]string, 0)
|
||||
for _, d := range res {
|
||||
if uid, found := strings.CutPrefix(d, prefix); found {
|
||||
resourceUIDs = append(resourceUIDs, uid)
|
||||
}
|
||||
}
|
||||
|
||||
return resourceUIDs, nil
|
||||
}
|
||||
|
||||
func runBatch(tasks []func() ([]string, error)) ([]string, error) {
|
||||
var wg sync.WaitGroup
|
||||
tasksNum := len(tasks)
|
||||
resChan := make(chan []string, tasksNum)
|
||||
errChan := make(chan error, tasksNum)
|
||||
|
||||
for _, task := range tasks {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
res, err := task()
|
||||
resChan <- res
|
||||
errChan <- err
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(resChan)
|
||||
close(errChan)
|
||||
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0)
|
||||
for res := range resChan {
|
||||
result = append(result, res...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
121
pkg/services/dashboards/service/zanzana_integration_test.go
Normal file
121
pkg/services/dashboards/service/zanzana_integration_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/authz"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestIntegrationDashboardServiceZanzana(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
t.Run("Zanzana enabled", func(t *testing.T) {
|
||||
// t.Helper()
|
||||
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagZanzana)
|
||||
|
||||
db, cfg := db.InitTestDBWithCfg(t)
|
||||
|
||||
// Enable zanzana and run in embedded mode (part of grafana server)
|
||||
cfg.Zanzana.ZanzanaOnlyEvaluation = true
|
||||
cfg.Zanzana.Mode = setting.ZanzanaModeEmbedded
|
||||
cfg.Zanzana.ConcurrentChecks = 10
|
||||
|
||||
_, err := cfg.Raw.Section("rbac").NewKey("resources_with_managed_permissions_on_creation", "dashboard, folder")
|
||||
require.NoError(t, err)
|
||||
|
||||
quotaService := quotatest.New(false, nil)
|
||||
tagService := tagimpl.ProvideService(db)
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(db)
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
dashboardStore, err := database.ProvideDashboardStore(db, cfg, features, tagService, quotaService)
|
||||
require.NoError(t, err)
|
||||
|
||||
zclient, err := authz.ProvideZanzana(cfg, db, features)
|
||||
require.NoError(t, err)
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zclient)
|
||||
|
||||
service, err := ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
featuremgmt.WithFeatures(),
|
||||
accesscontrolmock.NewMockedPermissionsService(),
|
||||
accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac,
|
||||
foldertest.NewFakeService(),
|
||||
fStore,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
guardianMock := &guardian.FakeDashboardGuardian{
|
||||
CanSaveValue: true,
|
||||
}
|
||||
guardian.MockDashboardGuardian(guardianMock)
|
||||
|
||||
createDashboards(t, service, 100, "test-a")
|
||||
createDashboards(t, service, 100, "test-b")
|
||||
|
||||
// Sync Grafana DB with zanzana (migrate data)
|
||||
zanzanaSyncronizer := migrator.NewZanzanaSynchroniser(zclient, db)
|
||||
err = zanzanaSyncronizer.Sync(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
query := &dashboards.FindPersistedDashboardsQuery{
|
||||
Title: "test-a",
|
||||
Limit: 1000,
|
||||
SignedInUser: &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
UserID: 1,
|
||||
},
|
||||
}
|
||||
res, err := service.FindDashboardsZanzana(context.Background(), query)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(res))
|
||||
})
|
||||
}
|
||||
|
||||
func createDashboard(t *testing.T, service dashboards.DashboardService, uid, title string) {
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
OrgID: 1,
|
||||
// User: user,
|
||||
User: &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
UserID: 1,
|
||||
},
|
||||
}
|
||||
dto.Dashboard = dashboards.NewDashboard(title)
|
||||
dto.Dashboard.SetUID(uid)
|
||||
|
||||
_, err := service.SaveDashboard(context.Background(), dto, false)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func createDashboards(t *testing.T, service dashboards.DashboardService, number int, prefix string) {
|
||||
for i := 0; i < number; i++ {
|
||||
title := fmt.Sprintf("%s-%d", prefix, i)
|
||||
uid := fmt.Sprintf("dash-%s", title)
|
||||
createDashboard(t, service, uid, title)
|
||||
}
|
||||
}
|
@ -675,8 +675,9 @@ var (
|
||||
Name: "externalServiceAccounts",
|
||||
Description: "Automatic service account and token setup for plugins",
|
||||
HideFromAdminPage: true,
|
||||
Stage: FeatureStagePublicPreview,
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: identityAccessTeam,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
{
|
||||
Name: "panelMonitoring",
|
||||
|
@ -88,7 +88,7 @@ wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false
|
||||
alertingInsights,GA,@grafana/alerting-squad,false,false,true
|
||||
externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false
|
||||
externalServiceAccounts,GA,@grafana/identity-access-team,false,false,false
|
||||
panelMonitoring,GA,@grafana/dataviz-squad,false,false,true
|
||||
enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
|
||||
disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
|
||||
|
|
@ -1234,14 +1234,18 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "externalServiceAccounts",
|
||||
"resourceVersion": "1718727528075",
|
||||
"creationTimestamp": "2023-09-28T07:26:37Z"
|
||||
"resourceVersion": "1726562284896",
|
||||
"creationTimestamp": "2023-09-28T07:26:37Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-09-17 08:38:04.896869045 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Automatic service account and token setup for plugins",
|
||||
"stage": "preview",
|
||||
"stage": "GA",
|
||||
"codeowner": "@grafana/identity-access-team",
|
||||
"hideFromAdminPage": true
|
||||
"hideFromAdminPage": true,
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -387,7 +387,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
|
||||
})
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestoreUI) && c.SignedInUser.GetOrgRole() == org.RoleAdmin {
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestoreUI) && (c.SignedInUser.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
|
||||
Text: "Recently deleted",
|
||||
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
|
||||
|
@ -36,4 +36,12 @@ func (a *recordingAccessControlFake) WithoutResolvers() accesscontrol.AccessCont
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) Check(ctx context.Context, in accesscontrol.CheckRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) ListObjects(ctx context.Context, in accesscontrol.ListObjectsRequest) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ accesscontrol.AccessControl = &recordingAccessControlFake{}
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||
@ -138,6 +137,14 @@ func (a *recordingAccessControlFake) IsDisabled() bool {
|
||||
return a.Disabled
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) Check(ctx context.Context, in ac.CheckRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) ListObjects(ctx context.Context, in ac.ListObjectsRequest) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ ac.AccessControl = &recordingAccessControlFake{}
|
||||
|
||||
type fakeRuleAccessControlService struct {
|
||||
|
@ -98,6 +98,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Category: "cloud",
|
||||
Annotations: true,
|
||||
@ -141,6 +150,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Executable: "test",
|
||||
Backend: true,
|
||||
@ -195,6 +213,9 @@ func TestLoader_Load(t *testing.T) {
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
@ -228,6 +249,12 @@ func TestLoader_Load(t *testing.T) {
|
||||
Slug: "nginx-datasource",
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-app/module.js",
|
||||
@ -266,6 +293,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
@ -318,6 +354,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
@ -423,6 +468,15 @@ func TestLoader_Load(t *testing.T) {
|
||||
GrafanaDependency: ">=8.0.0",
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-memory"},
|
||||
@ -497,6 +551,15 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
IAM: &pfs.IAM{
|
||||
Permissions: []pfs.Permission{
|
||||
@ -599,6 +662,15 @@ func TestLoader_Load_CustomSource(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "cdn/plugin")),
|
||||
@ -671,6 +743,15 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
@ -767,6 +848,15 @@ func TestLoader_Load_RBACReady(t *testing.T) {
|
||||
GrafanaVersion: "*",
|
||||
GrafanaDependency: ">=8.0.0",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Includes: []*plugins.Includes{},
|
||||
Roles: []plugins.RoleRegistration{
|
||||
@ -840,10 +930,18 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
|
||||
},
|
||||
Version: "1.0.0",
|
||||
},
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}, Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
}},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")),
|
||||
Class: plugins.ClassExternal,
|
||||
@ -913,6 +1011,15 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-connections"},
|
||||
@ -994,6 +1101,9 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-connections"},
|
||||
@ -1001,6 +1111,12 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
||||
{Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-panel"},
|
||||
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: plugins.ActionAppAccess, Slug: "nginx-datasource"},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, pluginDir1),
|
||||
@ -1208,6 +1324,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
@ -1241,6 +1366,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Module: "public/plugins/test-datasource/nested/module.js",
|
||||
@ -1336,6 +1470,9 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
GrafanaVersion: "7.0.0",
|
||||
GrafanaDependency: ">=7.0.0",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
@ -1382,6 +1519,12 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Slug: "lots-of-stats",
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
Module: "public/plugins/myorgid-simple-app/module.js",
|
||||
@ -1421,6 +1564,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
GrafanaDependency: ">=7.0.0",
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
Extensions: plugins.ExtensionsDependencies{
|
||||
ExposedComponents: []string{},
|
||||
},
|
||||
},
|
||||
Extensions: plugins.Extensions{
|
||||
AddedLinks: []plugins.AddedLink{},
|
||||
AddedComponents: []plugins.AddedComponent{},
|
||||
ExposedComponents: []plugins.ExposedComponent{},
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Module: "public/plugins/myorgid-simple-app/child/module.js",
|
||||
|
@ -26,7 +26,9 @@ const (
|
||||
)
|
||||
|
||||
type ServiceAccountsService struct {
|
||||
acService accesscontrol.Service
|
||||
acService accesscontrol.Service
|
||||
permissions accesscontrol.ServiceAccountPermissionsService
|
||||
|
||||
store store
|
||||
log log.Logger
|
||||
backgroundLog log.Logger
|
||||
@ -44,7 +46,8 @@ func ProvideServiceAccountsService(
|
||||
kvStore kvstore.KVStore,
|
||||
userService user.Service,
|
||||
orgService org.Service,
|
||||
accesscontrolService accesscontrol.Service,
|
||||
acService accesscontrol.Service,
|
||||
permissions accesscontrol.ServiceAccountPermissionsService,
|
||||
) (*ServiceAccountsService, error) {
|
||||
serviceAccountsStore := database.ProvideServiceAccountsStore(
|
||||
cfg,
|
||||
@ -55,13 +58,14 @@ func ProvideServiceAccountsService(
|
||||
orgService,
|
||||
)
|
||||
s := &ServiceAccountsService{
|
||||
acService: accesscontrolService,
|
||||
acService: acService,
|
||||
permissions: permissions,
|
||||
store: serviceAccountsStore,
|
||||
log: log.New("serviceaccounts"),
|
||||
backgroundLog: log.New("serviceaccounts.background"),
|
||||
}
|
||||
|
||||
if err := RegisterRoles(accesscontrolService); err != nil {
|
||||
if err := RegisterRoles(acService); err != nil {
|
||||
s.log.Error("Failed to register roles", "error", err)
|
||||
}
|
||||
|
||||
@ -179,7 +183,10 @@ func (sa *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgI
|
||||
if err := sa.store.DeleteServiceAccount(ctx, orgID, serviceAccountID); err != nil {
|
||||
return err
|
||||
}
|
||||
return sa.acService.DeleteUserPermissions(ctx, orgID, serviceAccountID)
|
||||
if err := sa.acService.DeleteUserPermissions(ctx, orgID, serviceAccountID); err != nil {
|
||||
return err
|
||||
}
|
||||
return sa.permissions.DeleteResourcePermissions(ctx, orgID, fmt.Sprintf("%d", serviceAccountID))
|
||||
}
|
||||
|
||||
func (sa *ServiceAccountsService) EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error {
|
||||
|
@ -119,7 +119,8 @@ func (f *SecretsCheckerFake) CheckTokens(ctx context.Context) error {
|
||||
func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) {
|
||||
storeMock := newServiceAccountStoreFake()
|
||||
acSvc := actest.FakeService{}
|
||||
svc := ServiceAccountsService{acSvc, storeMock, log.New("test"), log.New("background.test"), &SecretsCheckerFake{}, false, 0}
|
||||
pSvc := &actest.FakePermissionsService{}
|
||||
svc := ServiceAccountsService{acSvc, pSvc, storeMock, log.NewNopLogger(), log.NewNopLogger(), &SecretsCheckerFake{}, false, 0}
|
||||
testOrgId := 1
|
||||
|
||||
t.Run("should create service account", func(t *testing.T) {
|
||||
|
@ -14,8 +14,9 @@ import (
|
||||
|
||||
func Test_UsageStats(t *testing.T) {
|
||||
acSvc := actest.FakeService{}
|
||||
pSvc := actest.FakePermissionsService{}
|
||||
storeMock := newServiceAccountStoreFake()
|
||||
svc := ServiceAccountsService{acSvc, storeMock, log.New("test"), log.New("background-test"), &SecretsCheckerFake{}, true, 5}
|
||||
svc := ServiceAccountsService{acSvc, &pSvc, storeMock, log.NewNopLogger(), log.NewNopLogger(), &SecretsCheckerFake{}, true, 5}
|
||||
err := svc.DeleteServiceAccount(context.Background(), 1, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
96
pkg/services/sqlstore/migrations/accesscontrol/orphaned.go
Normal file
96
pkg/services/sqlstore/migrations/accesscontrol/orphaned.go
Normal file
@ -0,0 +1,96 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
const (
|
||||
orphanedServiceAccountsPermissions = "delete orphaned service account permissions"
|
||||
)
|
||||
|
||||
func AddOrphanedMigrations(mg *migrator.Migrator) {
|
||||
mg.AddMigration(orphanedServiceAccountsPermissions, &orphanedServiceAccountPermissions{})
|
||||
}
|
||||
|
||||
var _ migrator.CodeMigration = new(alertingScopeRemovalMigrator)
|
||||
|
||||
type orphanedServiceAccountPermissions struct {
|
||||
migrator.MigrationBase
|
||||
}
|
||||
|
||||
func (m *orphanedServiceAccountPermissions) SQL(dialect migrator.Dialect) string {
|
||||
return CodeMigrationSQL
|
||||
}
|
||||
|
||||
func (m *orphanedServiceAccountPermissions) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
||||
var idents []string
|
||||
|
||||
// find all permissions that are scopes directly to a service account
|
||||
err := sess.SQL("SELECT DISTINCT p.identifier FROM permission AS p WHERE p.kind = 'serviceaccounts' AND NOT p.identifier = '*'").Find(&idents)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch permissinos scoped to service accounts: %w", err)
|
||||
}
|
||||
|
||||
ids := make([]int64, 0, len(idents))
|
||||
for _, id := range idents {
|
||||
id, err := strconv.ParseInt(id, 10, 64)
|
||||
if err == nil {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Then find all existing service accounts
|
||||
raw := "SELECT u.id FROM " + mg.Dialect.Quote("user") + " AS u WHERE u.is_service_account AND u.id IN(?" + strings.Repeat(",?", len(ids)-1) + ")"
|
||||
args := make([]any, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
var existingIDs []int64
|
||||
err = sess.SQL(raw, args...).Find(&existingIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch existing service accounts: %w", err)
|
||||
}
|
||||
|
||||
existing := make(map[int64]struct{}, len(existingIDs))
|
||||
for _, id := range existingIDs {
|
||||
existing[id] = struct{}{}
|
||||
}
|
||||
|
||||
// filter out orphaned permissions
|
||||
var orphaned []string
|
||||
for _, id := range ids {
|
||||
if _, ok := existing[id]; !ok {
|
||||
orphaned = append(orphaned, strconv.FormatInt(id, 10))
|
||||
}
|
||||
}
|
||||
|
||||
if len(orphaned) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete all orphaned permissions
|
||||
rawDelete := "DELETE FROM permission AS p WHERE p.kind = 'serviceaccounts' AND p.identifier IN(?" + strings.Repeat(",?", len(orphaned)-1) + ")"
|
||||
deleteArgs := make([]any, 0, len(orphaned)+1)
|
||||
deleteArgs = append(deleteArgs, rawDelete)
|
||||
for _, id := range orphaned {
|
||||
deleteArgs = append(deleteArgs, id)
|
||||
}
|
||||
|
||||
_, err = sess.Exec(deleteArgs...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete orphaned service accounts: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -131,6 +131,8 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) {
|
||||
ualert.AddReceiverActionScopesMigration(mg)
|
||||
|
||||
ualert.AddRuleMetadata(mg)
|
||||
|
||||
accesscontrol.AddOrphanedMigrations(mg)
|
||||
}
|
||||
|
||||
func addStarMigrations(mg *Migrator) {
|
||||
|
@ -289,7 +289,7 @@ func (ss *SQLStore) initEngine(engine *xorm.Engine) error {
|
||||
}
|
||||
if engine == nil {
|
||||
// Ensure that parseTime is enabled for MySQL
|
||||
if ss.features.IsEnabledGlobally(featuremgmt.FlagMysqlParseTime) && ss.dbCfg.Type == migrator.MySQL && !strings.Contains(ss.dbCfg.ConnectionString, "parseTime=") {
|
||||
if ss.features.IsEnabledGlobally(featuremgmt.FlagMysqlParseTime) && strings.Contains(ss.dbCfg.Type, migrator.MySQL) && !strings.Contains(ss.dbCfg.ConnectionString, "parseTime=") {
|
||||
if strings.Contains(ss.dbCfg.ConnectionString, "?") {
|
||||
ss.dbCfg.ConnectionString += "&parseTime=true"
|
||||
} else {
|
||||
|
@ -93,6 +93,13 @@ func TestInitEngine_ParseTimeInConnectionString(t *testing.T) {
|
||||
featureEnabled: true,
|
||||
expectedConnection: "user:password@tcp(localhost:3306)/existingparams?collation=utf8mb4_unicode_ci&allowNativePasswords=true&clientFoundRows=true&charset=utf8&parseTime=true",
|
||||
},
|
||||
{
|
||||
name: "MySQL with feature enabled",
|
||||
connectionString: "mysql://user:password@localhost:3306/existingparams?charset=utf8",
|
||||
dbType: "mysqlWithHooks",
|
||||
featureEnabled: true,
|
||||
expectedConnection: "user:password@tcp(localhost:3306)/existingparams?collation=utf8mb4_unicode_ci&allowNativePasswords=true&clientFoundRows=true&charset=utf8&parseTime=true",
|
||||
},
|
||||
{
|
||||
name: "MySQL with feature disabled",
|
||||
connectionString: "mysql://user:password@localhost:3306/disabled",
|
||||
|
@ -20,6 +20,11 @@ type ZanzanaSettings struct {
|
||||
ListenHTTP bool
|
||||
// OpenFGA http server address which allows to connect with fga cli
|
||||
HttpAddr string
|
||||
// Number of check requests running concurrently
|
||||
ConcurrentChecks int64
|
||||
// If enabled, authorization cheks will be only performed by zanzana.
|
||||
// This bypasses the performance comparison with the legacy system.
|
||||
ZanzanaOnlyEvaluation bool
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readZanzanaSettings() {
|
||||
@ -38,6 +43,8 @@ func (cfg *Cfg) readZanzanaSettings() {
|
||||
s.Addr = sec.Key("address").MustString("")
|
||||
s.ListenHTTP = sec.Key("listen_http").MustBool(false)
|
||||
s.HttpAddr = sec.Key("http_addr").MustString("127.0.0.1:8080")
|
||||
s.ConcurrentChecks = sec.Key("concurrent_checks").MustInt64(10)
|
||||
s.ZanzanaOnlyEvaluation = sec.Key("zanzana_only_evaluation").MustBool(false)
|
||||
|
||||
cfg.Zanzana = s
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ require (
|
||||
github.com/stretchr/testify v1.9.0
|
||||
gocloud.dev v0.39.0
|
||||
google.golang.org/grpc v1.66.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apiserver v0.31.0
|
||||
k8s.io/client-go v0.31.0
|
||||
k8s.io/apimachinery v0.31.1
|
||||
k8s.io/apiserver v0.31.1
|
||||
k8s.io/client-go v0.31.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
)
|
||||
|
||||
@ -98,8 +98,8 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.31.0 // indirect
|
||||
k8s.io/component-base v0.31.0 // indirect
|
||||
k8s.io/api v0.31.1 // indirect
|
||||
k8s.io/component-base v0.31.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
|
||||
|
@ -467,16 +467,16 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
|
||||
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
|
||||
k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
|
||||
k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
|
||||
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
|
||||
k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
|
||||
k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
|
||||
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
|
||||
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
|
||||
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
|
||||
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
|
||||
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
|
||||
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
|
||||
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
|
||||
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
|
||||
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
|
||||
k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
|
||||
k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
|
@ -14,7 +14,7 @@ require (
|
||||
gocloud.dev v0.39.0
|
||||
google.golang.org/grpc v1.66.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apimachinery v0.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
@ -56,7 +56,7 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiserver v0.31.0 // indirect
|
||||
k8s.io/apiserver v0.31.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
|
@ -18,13 +18,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -60,6 +64,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"alerts",
|
||||
@ -73,7 +78,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -104,13 +112,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -163,6 +175,7 @@
|
||||
"path": "public/app/plugins/datasource/azuremonitor/img/azure_monitor_cpu.png"
|
||||
}
|
||||
],
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"azure",
|
||||
@ -175,7 +188,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -206,13 +222,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -243,13 +263,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -280,6 +304,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"financial",
|
||||
@ -291,7 +316,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -322,13 +350,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -359,6 +391,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"aws",
|
||||
@ -368,7 +401,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -399,13 +435,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -436,13 +476,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -478,15 +522,24 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"elasticsearch"
|
||||
"elasticsearch",
|
||||
"datasource",
|
||||
"database",
|
||||
"logs",
|
||||
"nosql",
|
||||
"traces"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -517,13 +570,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -554,13 +611,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -591,13 +652,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -628,13 +693,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -665,13 +734,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -707,6 +780,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@ -721,7 +795,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -752,13 +829,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -798,13 +879,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -835,13 +920,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -872,6 +961,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"distribution",
|
||||
@ -883,7 +973,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -914,13 +1007,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -960,13 +1057,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -997,13 +1098,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1043,13 +1148,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1080,13 +1189,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.4.0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1117,13 +1230,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.4.0",
|
||||
"grafanaDependency": "\u003e=10.4.0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1154,13 +1271,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1191,13 +1312,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1228,13 +1353,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1270,6 +1399,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@ -1281,7 +1411,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1312,13 +1445,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1349,13 +1486,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1391,13 +1532,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1428,13 +1573,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1465,13 +1614,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1502,13 +1655,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1539,13 +1696,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1576,13 +1737,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1618,13 +1783,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1655,13 +1824,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1692,13 +1865,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1729,13 +1906,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1766,13 +1947,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1803,13 +1988,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1840,13 +2029,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1877,6 +2070,7 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "",
|
||||
"updated": "",
|
||||
"keywords": [
|
||||
"scatter",
|
||||
@ -1886,7 +2080,10 @@
|
||||
"dependencies": {
|
||||
"grafanaDependency": "",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
@ -1922,13 +2119,17 @@
|
||||
},
|
||||
"build": {},
|
||||
"screenshots": null,
|
||||
"version": "11.3.0-pre",
|
||||
"updated": "",
|
||||
"keywords": null
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": "\u003e=10.3.0-0",
|
||||
"grafanaVersion": "*",
|
||||
"plugins": []
|
||||
"plugins": [],
|
||||
"extensions": {
|
||||
"exposedComponents": []
|
||||
}
|
||||
},
|
||||
"latestVersion": "",
|
||||
"hasUpdate": false,
|
||||
|
@ -3,7 +3,7 @@ package util
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jmespath/go-jmespath"
|
||||
"github.com/jmespath-community/go-jmespath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
)
|
||||
@ -86,7 +86,7 @@ func searchJSONForAttr(attributePath string, data any) (any, error) {
|
||||
}
|
||||
|
||||
// Copy the data to a new variable
|
||||
var jsonData = data
|
||||
jsonData := data
|
||||
|
||||
// If the data is a byte slice, try to unmarshal it into a JSON object
|
||||
if dataBytes, ok := data.([]byte); ok {
|
||||
|
@ -153,3 +153,34 @@ func TestSearchJSONForEmail(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchJSONForStringAttr(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
Name string
|
||||
SearchObject any
|
||||
AttributePath string
|
||||
ExpectedResult string
|
||||
}{
|
||||
{
|
||||
Name: "Case insensitive contains using lower function from works correctly",
|
||||
SearchObject: map[string]any{
|
||||
"groups": []string{
|
||||
"fOO",
|
||||
},
|
||||
},
|
||||
AttributePath: "contains(groups[*].lower(@) ,lower('FOO')) && 'success' || 'failure'",
|
||||
ExpectedResult: "success",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actualResult, err := util.SearchJSONForStringAttr(test.AttributePath, test.SearchObject)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.ExpectedResult, actualResult)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Menu, Dropdown, useStyles2, useTheme2, ToolbarButton } from '@grafana/ui';
|
||||
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
|
||||
import { useSelector } from 'app/types';
|
||||
@ -22,6 +22,8 @@ export const QuickAdd = ({}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(!window.matchMedia(`(min-width: ${breakpoint}px)`).matches);
|
||||
const createActions = useMemo(() => findCreateActions(navBarTree), [navBarTree]);
|
||||
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||
const showQuickAdd = createActions.length > 0 && (!isSingleTopNav || !isSmallScreen);
|
||||
|
||||
useMediaQueryChange({
|
||||
breakpoint,
|
||||
@ -45,7 +47,7 @@ export const QuickAdd = ({}: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
return createActions.length > 0 ? (
|
||||
return showQuickAdd ? (
|
||||
<>
|
||||
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={setIsOpen}>
|
||||
<ToolbarButton
|
||||
|
@ -23,7 +23,6 @@ import { TOP_BAR_LEVEL_HEIGHT } from '../types';
|
||||
import { SignInLink } from './SignInLink';
|
||||
import { TopNavBarMenu } from './TopNavBarMenu';
|
||||
import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
|
||||
import { TopSearchBarSection } from './TopSearchBarSection';
|
||||
|
||||
interface Props {
|
||||
sectionNav: NavModelItem;
|
||||
@ -52,7 +51,7 @@ export const SingleTopBar = memo(function SingleTopBar({
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<TopSearchBarSection>
|
||||
<Stack minWidth={0} gap={0.5} alignItems="center">
|
||||
{!menuDockedAndOpen && (
|
||||
<ToolbarButton narrow onClick={onToggleMegaMenu} tooltip={t('navigation.megamenu.open', 'Open menu')}>
|
||||
<Stack gap={0} alignItems="center">
|
||||
@ -63,9 +62,9 @@ export const SingleTopBar = memo(function SingleTopBar({
|
||||
)}
|
||||
<Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
|
||||
<ScopesSelector />
|
||||
</TopSearchBarSection>
|
||||
</Stack>
|
||||
|
||||
<TopSearchBarSection align="right">
|
||||
<Stack gap={0.5} alignItems="center">
|
||||
<TopSearchBarCommandPaletteTrigger />
|
||||
<QuickAdd />
|
||||
{enrichedHelpNode && (
|
||||
@ -88,7 +87,7 @@ export const SingleTopBar = memo(function SingleTopBar({
|
||||
<ToolbarButton className={styles.kioskToggle} onClick={onToggleKioskMode} narrow aria-label="Enable kiosk mode">
|
||||
<Icon name="angle-up" size="xl" />
|
||||
</ToolbarButton>
|
||||
</TopSearchBarSection>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -97,15 +96,15 @@ const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => ({
|
||||
layout: css({
|
||||
height: TOP_BAR_LEVEL_HEIGHT,
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
gap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(0, 1),
|
||||
paddingLeft: menuDockedAndOpen ? theme.spacing(3.5) : theme.spacing(0.75),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
justifyContent: 'space-between',
|
||||
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
gridTemplateColumns: '2fr minmax(240px, 1fr)', // TODO probably change these values
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
gridTemplateColumns: '2fr minmax(440px, 1fr)',
|
||||
display: 'grid',
|
||||
|
||||
justifyContent: 'flex-start',
|
||||
@ -115,7 +114,7 @@ const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => ({
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
minWidth: '50%',
|
||||
minWidth: '40%',
|
||||
},
|
||||
}),
|
||||
img: css({
|
||||
|
@ -3,6 +3,7 @@ import { useKBar, VisualState } from 'kbar';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getInputStyles, Icon, Text, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { focusCss } from '@grafana/ui/src/themes/mixins';
|
||||
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
|
||||
@ -11,12 +12,13 @@ import { getModKey } from 'app/core/utils/browser';
|
||||
|
||||
export function TopSearchBarCommandPaletteTrigger() {
|
||||
const theme = useTheme2();
|
||||
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||
const { query: kbar } = useKBar((kbarState) => ({
|
||||
kbarSearchQuery: kbarState.searchQuery,
|
||||
kbarIsOpen: kbarState.visualState === VisualState.showing,
|
||||
}));
|
||||
|
||||
const breakpoint = theme.breakpoints.values.sm;
|
||||
const breakpoint = isSingleTopNav ? theme.breakpoints.values.lg : theme.breakpoints.values.sm;
|
||||
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(!window.matchMedia(`(min-width: ${breakpoint}px)`).matches);
|
||||
|
||||
|
@ -127,7 +127,7 @@ export const LdapDrawerComponent = ({
|
||||
<Drawer title={t('ldap-drawer.title', 'Advanced settings')} onClose={onClose}>
|
||||
<CollapsableSection label={t('ldap-drawer.misc-section.label', 'Misc')} isOpen={true}>
|
||||
<Field
|
||||
label={t('ldap-drawer.misc-section.allow-sign-up-label', 'Allow sign up')}
|
||||
label={t('ldap-drawer.misc-section.allow-sign-up-label', 'Allow sign-up')}
|
||||
description={t(
|
||||
'ldap-drawer.misc-section.allow-sign-up-descrition',
|
||||
'If not enabled, only existing Grafana users can log in using LDAP'
|
||||
|
@ -19,6 +19,19 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
|
||||
version: info.version,
|
||||
angular: angular ?? { detected: false, hideDeprecation: false },
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
extensionPoints: [],
|
||||
exposedComponents: [],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaVersion: '',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -49,6 +49,19 @@ describe('getRuleOrigin', () => {
|
||||
preload: true,
|
||||
angular: { detected: false, hideDeprecation: false },
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
extensionPoints: [],
|
||||
exposedComponents: [],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaVersion: '',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const rule = mockCombinedRule({
|
||||
|
@ -4,11 +4,14 @@ import { useLocation, useParams } from 'react-router-dom-v5-compat';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { FilterInput, useStyles2 } from '@grafana/ui';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { LinkButton, FilterInput, useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { contextSrv } from '../../core/services/context_srv';
|
||||
import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel';
|
||||
import { useSearchStateManager } from '../search/state/SearchStateManager';
|
||||
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
|
||||
@ -81,6 +84,7 @@ const BrowseDashboardsPage = memo(() => {
|
||||
const { data: rootFolder } = useGetFolderQuery('general');
|
||||
let folder = folderDTO ? folderDTO : rootFolder;
|
||||
const { canEditFolders, canEditDashboards, canCreateDashboards, canCreateFolders } = getFolderPermissions(folder);
|
||||
const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
|
||||
|
||||
const showEditTitle = canEditFolders && folderUID;
|
||||
const canSelect = canEditFolders || canEditDashboards;
|
||||
@ -104,6 +108,12 @@ const BrowseDashboardsPage = memo(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClickToRecentlyDeleted = () => {
|
||||
reportInteraction('grafana_browse_dashboards_page_button_to_recently_deleted', {
|
||||
origin: window.location.pathname === getConfig().appSubUrl + '/dashboards' ? 'Dashboards' : 'Folder view',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="dashboards/browse"
|
||||
@ -111,6 +121,15 @@ const BrowseDashboardsPage = memo(() => {
|
||||
onEditTitle={showEditTitle ? onEditTitle : undefined}
|
||||
actions={
|
||||
<>
|
||||
{config.featureToggles.dashboardRestore && config.featureToggles.dashboardRestoreUI && hasAdminRights && (
|
||||
<LinkButton
|
||||
variant="secondary"
|
||||
href={getConfig().appSubUrl + '/dashboard/recently-deleted'}
|
||||
onClick={handleButtonClickToRecentlyDeleted}
|
||||
>
|
||||
<Trans i18nKey="browse-dashboards.actions.button-to-recently-deleted">Recently deleted</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
{folderDTO && <FolderActionsButton folder={folderDTO} />}
|
||||
{(canCreateDashboards || canCreateFolders) && (
|
||||
<CreateNewButton
|
||||
|
@ -30,6 +30,6 @@ function setup() {
|
||||
describe('SupportSnapshot', () => {
|
||||
it('Can render', async () => {
|
||||
setup();
|
||||
expect(await screen.findByRole('button', { name: /Dashboard \([\d\.]+ KiB\)/ })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: /Download snapshot \([\d\.]+ KiB\)/ })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { PanelPlugin, GrafanaTheme2, FeatureState } from '@grafana/data';
|
||||
import { PanelPlugin, GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
Drawer,
|
||||
@ -11,17 +11,16 @@ import {
|
||||
CodeEditor,
|
||||
useStyles2,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
InlineSwitch,
|
||||
Button,
|
||||
Spinner,
|
||||
Alert,
|
||||
FeatureBadge,
|
||||
Select,
|
||||
ClipboardButton,
|
||||
Icon,
|
||||
Stack,
|
||||
} from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -76,7 +75,6 @@ export function HelpWizard({ panel, plugin, onClose }: Props) {
|
||||
subtitle={
|
||||
<Stack direction="column" gap={1}>
|
||||
<Stack direction="row" gap={1}>
|
||||
<FeatureBadge featureState={FeatureState.beta} />
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/troubleshooting/"
|
||||
target="blank"
|
||||
@ -87,13 +85,17 @@ export function HelpWizard({ panel, plugin, onClose }: Props) {
|
||||
</a>
|
||||
</Stack>
|
||||
<span className="muted">
|
||||
To request troubleshooting help, send a snapshot of this panel to Grafana Labs Technical Support. The
|
||||
snapshot contains query response data and panel settings.
|
||||
<Trans i18nKey="help-wizard.troubleshooting-help">
|
||||
To request troubleshooting help, send a snapshot of this panel to Grafana Labs Technical Support. The
|
||||
snapshot contains query response data and panel settings.
|
||||
</Trans>
|
||||
</span>
|
||||
{hasSupportBundleAccess && (
|
||||
<span className="muted">
|
||||
You can also retrieve a support bundle containing information concerning your Grafana instance and
|
||||
configured datasources in the <a href="/support-bundles">support bundles section</a>.
|
||||
<Trans i18nKey="help-wizard.support-bundle">
|
||||
You can also retrieve a support bundle containing information concerning your Grafana instance and
|
||||
configured datasources in the <a href="/support-bundles">support bundles section</a>.
|
||||
</Trans>
|
||||
</span>
|
||||
)}
|
||||
</Stack>
|
||||
@ -150,10 +152,10 @@ export function HelpWizard({ panel, plugin, onClose }: Props) {
|
||||
{currentTab === SnapshotTab.Support && (
|
||||
<>
|
||||
<Field
|
||||
label="Randomize data"
|
||||
label="Obfuscate data"
|
||||
description="Modify the original data to hide sensitve information. Note the lengths will stay the same, and duplicate values will be equal."
|
||||
>
|
||||
<HorizontalGroup>
|
||||
<Stack direction="row" gap={1}>
|
||||
<InlineSwitch
|
||||
label="Labels"
|
||||
id="randomize-labels"
|
||||
@ -175,27 +177,27 @@ export function HelpWizard({ panel, plugin, onClose }: Props) {
|
||||
value={Boolean(randomize.values)}
|
||||
onChange={() => service.onToggleRandomize('values')}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Field>
|
||||
|
||||
<Field label="Support snapshot" description={`Panel: ${panelTitle}`}>
|
||||
<Stack>
|
||||
<Button icon="download-alt" onClick={service.onDownloadDashboard}>
|
||||
Dashboard ({snapshotSize})
|
||||
<Trans i18nKey="help-wizard.download-snapshot">Download snapshot</Trans> ({snapshotSize})
|
||||
</Button>
|
||||
<ClipboardButton
|
||||
icon="github"
|
||||
getText={service.onGetMarkdownForClipboard}
|
||||
title="Copy a complete GitHub comment to the clipboard"
|
||||
>
|
||||
Copy to clipboard
|
||||
<Trans i18nKey="help-wizard.github-comment">Copy Github comment</Trans>
|
||||
</ClipboardButton>
|
||||
<Button
|
||||
icon="eye"
|
||||
onClick={service.onPreviewDashboard}
|
||||
variant="secondary"
|
||||
title="Open support snapshot dashboard in a new tab"
|
||||
>
|
||||
Preview
|
||||
<Trans i18nKey="help-wizard.preview-snapshot">Preview snapshot</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Field>
|
||||
|
@ -43,6 +43,9 @@ export default {
|
||||
grafanaDependency: '>=7.3.0',
|
||||
grafanaVersion: '7.3',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
info: {
|
||||
links: [],
|
||||
|
@ -1,15 +1,62 @@
|
||||
import React from 'react';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
||||
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
|
||||
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
...jest.requireActual('../utils'),
|
||||
|
||||
// Manually set the dev mode to false
|
||||
// (to make sure that by default we are testing a production scneario)
|
||||
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
describe('AddedComponentsRegistry', () => {
|
||||
const consoleWarn = jest.fn();
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const appPluginConfig = {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
global.console.warn = consoleWarn;
|
||||
consoleWarn.mockReset();
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
@ -20,15 +67,14 @@ describe('AddedComponentsRegistry', () => {
|
||||
});
|
||||
|
||||
it('should be possible to register added components in the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const id = `${pluginId}/hello-world/v1`;
|
||||
const extensionPointId = `${pluginId}/hello-world/v1`;
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
targets: [id],
|
||||
targets: [extensionPointId],
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
component: () => React.createElement('div', null, 'Hello World'),
|
||||
@ -39,15 +85,17 @@ describe('AddedComponentsRegistry', () => {
|
||||
const registry = await reactiveRegistry.getState();
|
||||
|
||||
expect(Object.keys(registry)).toHaveLength(1);
|
||||
expect(registry[id][0]).toMatchObject({
|
||||
expect(registry[extensionPointId][0]).toMatchObject({
|
||||
pluginId,
|
||||
title: 'not important',
|
||||
description: 'not important',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible to asynchronously register component extensions for the same extension point (different plugins)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
@ -65,7 +113,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(Object.keys(registry1)).toHaveLength(1);
|
||||
expect(registry1['grafana/alerting/home'][0]).toMatchObject({
|
||||
expect(registry1[extensionPointId][0]).toMatchObject({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
@ -78,7 +126,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -86,7 +134,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
|
||||
const registry2 = await reactiveRegistry.getState();
|
||||
expect(Object.keys(registry2)).toHaveLength(1);
|
||||
expect(registry2['grafana/alerting/home']).toEqual(
|
||||
expect(registry2[extensionPointId]).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
@ -105,6 +153,8 @@ describe('AddedComponentsRegistry', () => {
|
||||
it('should be possible to asynchronously register component extensions for a different extension points (different plugin)', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'grafana-basic-app2';
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
|
||||
// Register extensions for the first plugin
|
||||
@ -114,7 +164,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId1],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -122,7 +172,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(registry1).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
[extensionPointId1]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
@ -138,7 +188,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/user/profile/tab'],
|
||||
targets: [extensionPointId2],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -147,14 +197,14 @@ describe('AddedComponentsRegistry', () => {
|
||||
const registry2 = await reactiveRegistry.getState();
|
||||
|
||||
expect(registry2).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
[extensionPointId1]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
'grafana/user/profile/tab': expect.arrayContaining([
|
||||
[extensionPointId2]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId2,
|
||||
title: 'Component 2 title',
|
||||
@ -165,8 +215,8 @@ describe('AddedComponentsRegistry', () => {
|
||||
});
|
||||
|
||||
it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
// Register extensions for the first extension point
|
||||
reactiveRegistry.register({
|
||||
@ -175,20 +225,20 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World2'),
|
||||
},
|
||||
],
|
||||
});
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(registry1).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
[extensionPointId]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 1 title',
|
||||
@ -204,8 +254,9 @@ describe('AddedComponentsRegistry', () => {
|
||||
});
|
||||
|
||||
it('should be possible to register one extension component targeting multiple extension points', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||
|
||||
reactiveRegistry.register({
|
||||
pluginId: pluginId,
|
||||
@ -213,21 +264,21 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home', 'grafana/user/profile/tab'],
|
||||
targets: [extensionPointId1, extensionPointId2],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
const registry1 = await reactiveRegistry.getState();
|
||||
expect(registry1).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
[extensionPointId1]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
'grafana/user/profile/tab': expect.arrayContaining([
|
||||
[extensionPointId2]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId,
|
||||
title: 'Component 1 title',
|
||||
@ -239,7 +290,9 @@ describe('AddedComponentsRegistry', () => {
|
||||
|
||||
it('should notify subscribers when the registry changes', async () => {
|
||||
const pluginId1 = 'grafana-basic-app';
|
||||
const pluginId2 = 'another-plugin';
|
||||
const pluginId2 = 'myorg-extensions-app';
|
||||
const extensionPointId1 = 'grafana/alerting/home';
|
||||
const extensionPointId2 = 'grafana/user/profile/tab';
|
||||
const reactiveRegistry = new AddedComponentsRegistry();
|
||||
const observable = reactiveRegistry.asObservable();
|
||||
const subscribeCallback = jest.fn();
|
||||
@ -252,7 +305,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId1],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -266,7 +319,7 @@ describe('AddedComponentsRegistry', () => {
|
||||
{
|
||||
title: 'Component 2 title',
|
||||
description: 'Component 2 description',
|
||||
targets: ['grafana/user/profile/tab'],
|
||||
targets: [extensionPointId2],
|
||||
component: () => React.createElement('div', null, 'Hello World2'),
|
||||
},
|
||||
],
|
||||
@ -277,14 +330,14 @@ describe('AddedComponentsRegistry', () => {
|
||||
const registry = subscribeCallback.mock.calls[2][0];
|
||||
|
||||
expect(registry).toEqual({
|
||||
'grafana/alerting/home': expect.arrayContaining([
|
||||
[extensionPointId1]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId1,
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
}),
|
||||
]),
|
||||
'grafana/user/profile/tab': expect.arrayContaining([
|
||||
[extensionPointId2]: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pluginId: pluginId2,
|
||||
title: 'Component 2 title',
|
||||
@ -294,43 +347,24 @@ describe('AddedComponentsRegistry', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip registering component and log a warning when id is not prefixed with plugin id or grafana', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalledWith(
|
||||
"[Plugin Extensions] Could not register added component with id 'alerting/home'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id or grafana. e.g '<grafana|myorg-basic-app>/my-component-id/v1'."
|
||||
);
|
||||
const currentState = await registry.getState();
|
||||
expect(Object.keys(currentState)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should log a warning when added component id is not suffixed with component version', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const extensionPointId = 'grafana/test/home';
|
||||
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: 'Component 1 description',
|
||||
targets: ['grafana/test/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(consoleWarn).toHaveBeenCalledWith(
|
||||
"[Plugin Extensions] Added component with id 'grafana/test/home' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'."
|
||||
`[Plugin Extensions] Added component "Component 1 title": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
|
||||
);
|
||||
const currentState = await registry.getState();
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
@ -338,13 +372,15 @@ describe('AddedComponentsRegistry', () => {
|
||||
|
||||
it('should not register component when description is missing', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: '',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -359,13 +395,15 @@ describe('AddedComponentsRegistry', () => {
|
||||
|
||||
it('should not register component when title is missing', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
registry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: '',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -382,15 +420,16 @@ describe('AddedComponentsRegistry', () => {
|
||||
it('should not be possible to register a component on a read-only registry', async () => {
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const readOnlyRegistry = registry.readOnly();
|
||||
const extensionPointId = 'grafana/alerting/home';
|
||||
|
||||
expect(() => {
|
||||
readOnlyRegistry.register({
|
||||
pluginId: 'grafana-basic-app',
|
||||
pluginId,
|
||||
configs: [
|
||||
{
|
||||
title: 'Component 1 title',
|
||||
description: '',
|
||||
targets: ['grafana/alerting/home'],
|
||||
targets: [extensionPointId],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
},
|
||||
],
|
||||
@ -434,4 +473,105 @@ describe('AddedComponentsRegistry', () => {
|
||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['grafana/alerting/home']);
|
||||
});
|
||||
|
||||
it('should not register a component added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedComponents = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [componentConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(0);
|
||||
expect(consoleWarn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register a component added by a core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
registry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: [componentConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
expect(consoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register a component added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||
// Production mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedComponents = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [componentConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
expect(consoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register a component added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedComponentsRegistry();
|
||||
const componentConfig = {
|
||||
title: 'Component title',
|
||||
description: 'Component description',
|
||||
targets: ['grafana/alerting/home'],
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedComponents = [componentConfig];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [componentConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
expect(consoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -2,13 +2,8 @@ import { ReplaySubject } from 'rxjs';
|
||||
|
||||
import { PluginExtensionAddedComponentConfig } from '@grafana/data';
|
||||
|
||||
import { logWarning, wrapWithPluginContext } from '../utils';
|
||||
import {
|
||||
extensionPointEndsWithVersion,
|
||||
isExtensionPointIdValid,
|
||||
isGrafanaCoreExtensionPoint,
|
||||
isReactComponent,
|
||||
} from '../validators';
|
||||
import { isAddedComponentMetaInfoMissing, isGrafanaDevMode, logWarning, wrapWithPluginContext } from '../utils';
|
||||
import { extensionPointEndsWithVersion, isGrafanaCoreExtensionPoint, isReactComponent } from '../validators';
|
||||
|
||||
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
|
||||
|
||||
@ -56,18 +51,15 @@ export class AddedComponentsRegistry extends Registry<
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedComponentMetaInfoMissing(pluginId, config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionPointIds = Array.isArray(config.targets) ? config.targets : [config.targets];
|
||||
for (const extensionPointId of extensionPointIds) {
|
||||
if (!isExtensionPointIdValid(pluginId, extensionPointId)) {
|
||||
logWarning(
|
||||
`Could not register added component with id '${extensionPointId}'. Reason: The component id does not match the id naming convention. Id should be prefixed with plugin id or grafana. e.g '<grafana|myorg-basic-app>/my-component-id/v1'.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) {
|
||||
logWarning(
|
||||
`Added component with id '${extensionPointId}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.`
|
||||
`Added component "${config.title}": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,61 @@
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { isGrafanaDevMode } from '../utils';
|
||||
|
||||
import { AddedLinksRegistry } from './AddedLinksRegistry';
|
||||
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
...jest.requireActual('../utils'),
|
||||
|
||||
// Manually set the dev mode to false
|
||||
// (to make sure that by default we are testing a production scneario)
|
||||
isGrafanaDevMode: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
describe('AddedLinksRegistry', () => {
|
||||
const originalApps = config.apps;
|
||||
const consoleWarn = jest.fn();
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const appPluginConfig = {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
global.console.warn = consoleWarn;
|
||||
consoleWarn.mockReset();
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
@ -19,7 +66,6 @@ describe('AddedLinksRegistry', () => {
|
||||
});
|
||||
|
||||
it('should be possible to register link extensions in the registry', async () => {
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const addedLinksRegistry = new AddedLinksRegistry();
|
||||
|
||||
addedLinksRegistry.register({
|
||||
@ -580,4 +626,109 @@ describe('AddedLinksRegistry', () => {
|
||||
expect(subscribeCallback).toHaveBeenCalledTimes(2);
|
||||
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['plugins/myorg-basic-app/start']);
|
||||
});
|
||||
|
||||
it('should not register a link added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
|
||||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
path: `/a/${pluginId}/declare-incident`,
|
||||
targets: 'grafana/dashboard/panel/menu',
|
||||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [linkConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(0);
|
||||
expect(consoleWarn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register a link added by core Grafana in dev-mode even if the meta-info is missing', async () => {
|
||||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
path: `/a/grafana/declare-incident`,
|
||||
targets: 'grafana/dashboard/panel/menu',
|
||||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
registry.register({
|
||||
pluginId: 'grafana',
|
||||
configs: [linkConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
expect(consoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register a link added by a plugin in production mode even if the meta-info is missing', async () => {
|
||||
// Production mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
path: `/a/${pluginId}/declare-incident`,
|
||||
targets: 'grafana/dashboard/panel/menu',
|
||||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [linkConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
expect(consoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register a link added by a plugin in dev-mode if the meta-info is present', async () => {
|
||||
// Enabling dev mode
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const registry = new AddedLinksRegistry();
|
||||
const linkConfig = {
|
||||
title: 'Link 1',
|
||||
description: 'Link 1 description',
|
||||
path: `/a/${pluginId}/declare-incident`,
|
||||
targets: ['grafana/dashboard/panel/menu'],
|
||||
configure: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [linkConfig];
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
configs: [linkConfig],
|
||||
});
|
||||
|
||||
const currentState = await registry.getState();
|
||||
|
||||
expect(Object.keys(currentState)).toHaveLength(1);
|
||||
expect(consoleWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -3,11 +3,10 @@ import { ReplaySubject } from 'rxjs';
|
||||
import { IconName, PluginExtensionAddedLinkConfig } from '@grafana/data';
|
||||
import { PluginAddedLinksConfigureFunc, PluginExtensionEventHelpers } from '@grafana/data/src/types/pluginExtensions';
|
||||
|
||||
import { logWarning } from '../utils';
|
||||
import { isAddedLinkMetaInfoMissing, isGrafanaDevMode, logWarning } from '../utils';
|
||||
import {
|
||||
extensionPointEndsWithVersion,
|
||||
isConfigureFnValid,
|
||||
isExtensionPointIdValid,
|
||||
isGrafanaCoreExtensionPoint,
|
||||
isLinkPathValid,
|
||||
} from '../validators';
|
||||
@ -73,18 +72,15 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedLinkMetaInfoMissing(pluginId, config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionPointIds = Array.isArray(targets) ? targets : [targets];
|
||||
for (const extensionPointId of extensionPointIds) {
|
||||
if (!isExtensionPointIdValid(pluginId, extensionPointId)) {
|
||||
logWarning(
|
||||
`Could not register added link with id '${extensionPointId}'. Reason: Target extension point id must start with grafana, plugins or plugin id.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isGrafanaCoreExtensionPoint(extensionPointId) && !extensionPointEndsWithVersion(extensionPointId)) {
|
||||
logWarning(
|
||||
`Added component with id '${extensionPointId}' does not match the convention. It's recommended to suffix the id with the component version. e.g 'myorg-basic-app/my-component-id/v1'.`
|
||||
`Added link "${config.title}: it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
|
||||
);
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user