Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Mariell Hoversholm
2025-02-14 07:38:22 +01:00
40 changed files with 3034 additions and 604 deletions
+11 -2
View File
@@ -71,7 +71,14 @@ refs:
# Tempo data source
Grafana ships with built-in support for [Tempo](https://grafana.com/docs/tempo/latest/), a high-volume, minimal-dependency trace storage, open source tracing solution from Grafana Labs. This topic explains configuration and queries specific to the Tempo data source.
Grafana ships with built-in support for [Tempo](https://grafana.com/docs/tempo/<TEMPO_VERSION>/), a high-volume, minimal-dependency trace storage, open source tracing solution from Grafana Labs.
To learn more about traces, refer to [Introduction to tracing](https://grafana.com/docs/tempo/<TEMPO_VERSION>/introduction/).
To use traces, you need you have an application or service that is instrumented to emit traces.
Refer to the [Instrument for tracing](https://grafana.com/docs/tempo/<TEMPO_VERSION>/getting-started/instrumentation/) for more information.
## Add a data source
For instructions on how to add a data source to Grafana, refer to the [administration documentation](ref:data-source-management).
Only users with the organization administrator role can add data sources.
@@ -81,6 +88,8 @@ This video explains how to add data sources, including Loki, Tempo, and Mimir, t
{{< youtube id="cqHO0oYW6Ic" start="298" >}}
Once you've added the data source, you can [configure it]({{< relref "./configure-tempo-data-source/" >}}) so that your Grafana instance's users can create queries in its [query editor]({{< relref "./query-editor/" >}}) when they [build dashboards](ref:build-dashboards) and use [Explore](ref:explore).
## Learn more
After you've added the data source, you can [configure it](./configure-tempo-data-source/) so that your Grafana instance's users can create queries in its [query editor](./query-editor/) when they [build dashboards](ref:build-dashboards) and use [Explore](ref:explore).
{{< section withDescriptions="true">}}
@@ -131,7 +131,7 @@ To use streaming, you need to:
- Run Tempo version 2.2 or later, or Grafana Enterprise Traces (GET) version 2.2 or later, or use Grafana Cloud Traces.
- Tempo must have `stream_over_http_enabled: true` for streaming to work.
For more information, refer to [Tempo GRPC API](https://grafana.com/docs/tempo/<TEMPO_VERSION>/api_docs/#tempo-grpc-api).
For more information, refer to [Tempo gRPC API](https://grafana.com/docs/tempo/<TEMPO_VERSION>/api_docs/#tempo-grpc-api).
- For self-managed Tempo or GET instances: If your Tempo or GET instance is behind a load balancer or proxy that doesn't supporting gRPC or HTTP2, streaming may not work and should be deactivated.
@@ -105,7 +105,7 @@ Refer to the [Search using the TraceQL query builder documentation]({{< relref "
The **TraceQL** query editor lets you search by trace ID and write TraceQL queries using autocomplete.
Refer to the [TraceQL query editor documentation]({{< relref "./traceql-editor" >}}) to learn more about constructing queries using a code-editor-like experience.
Refer to the [TraceQL query editor documentation](./traceql-editor) to learn more about constructing queries using a code-editor-like experience.
![The TraceQL query editor](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-traceql-v11.png)
@@ -113,8 +113,8 @@ You can also search for a trace ID by entering it into the query field.
### Service graph view
Grafana’s **Service Graph** view uses metrics to display span request rates, error rates, and durations, as well as service graphs.
Once the requirements are set up, this preconfigured view is immediately available.
The **Service Graph** view uses metrics to display span request rates, error rates, and durations, as well as service graphs.
After the requirements are set up, this preconfigured view is immediately available.
Using the service graph view, you can:
@@ -12,7 +12,7 @@ labels:
- oss
menuTitle: Write TraceQL queries
title: Write TraceQL queries with the editor
weight: 300
weight: 400
refs:
explore:
- pattern: /docs/grafana/
@@ -41,4 +41,4 @@ refs:
[//]: # 'Shared content for the TraceQL query editor'
[//]: # 'This content is located in /docs/sources/shared/datasources/tempo-editor-traceql.md'
{{< docs/shared source="grafana" lookup="datasources/tempo-editor-traceql.md" version="<GRAFANA VERSION>" >}}
{{< docs/shared source="grafana" lookup="datasources/tempo-editor-traceql.md" version="<GRAFANA_VERSION>" >}}
@@ -56,4 +56,4 @@ To enable the TraceQL query builder in self-hosted Grafana through version 10.1,
[//]: # 'Shared content for the Search - TraceQL query builder'
{{< docs/shared source="grafana" lookup="datasources/tempo-search-traceql.md" leveloffset="+1" version="<GRAFANA VERSION>" >}}
{{< docs/shared source="grafana" lookup="datasources/tempo-search-traceql.md" leveloffset="+1" version="<GRAFANA_VERSION>" >}}
@@ -0,0 +1,21 @@
---
description: Learn how to create TraceQL queries are structured.
keywords:
- queries
- TraceQL
labels:
products:
- cloud
- enterprise
- oss
menuTitle: TraceQL query structure
title: TraceQL query structure
weight: 200
---
# TraceQL query structure
[//]: # 'Shared content for best practices for traces'
[//]: # 'This content is located in /tempo/docs/sources/shared/trace-structure.md'
{{< docs/shared source="tempo" lookup="traceql-query-structure.md" version="<TEMPO_VERSION>" >}}
@@ -72,8 +72,8 @@ You use the Service Graph to detect performance issues; track increases in error
## Display the Service Graph
1. [Configure Grafana Alloy](https://grafana.com/docs/tempo/latest/configuration/grafana-alloy/) or [Tempo or GET](https://grafana.com/docs/tempo/latest/metrics-generator/service_graphs/#tempo) to generate Service Graph data.
1. Link a Prometheus data source in the Tempo data source's [Service Graph]({{< relref "./configure-tempo-data-source#configure-service-graph" >}}) settings.
1. [Configure Grafana Alloy](https://grafana.com/docs/tempo/<TEMPO_VERSION>/configuration/grafana-alloy/) or [Tempo or GET](https://grafana.com/docs/tempo/<TEMPO_VERSION>/metrics-generator/service_graphs/#tempo) to generate Service Graph data.
1. Link a Prometheus data source in the Tempo data source's [Service Graph](./configure-tempo-data-source#configure-service-graph) settings.
1. Navigate to [Explore](ref:explore).
1. Select the Tempo data source.
1. Select the **Service Graph** query type.
@@ -113,10 +113,10 @@ To open the Service Graph view:
1. Run the query.
1. _(Optional)_ Filter your results.
{{% admonition type="note" %}}
{{< admonition type="note" >}}
Grafana uses the `traces_spanmetrics_calls_total` metric to display the name, rate, and error rate columns, and `traces_spanmetrics_latency_bucket` to display the duration column.
These metrics must exist in your Prometheus data source.
{{% /admonition %}}
{{< /admonition >}}
To open a query in Prometheus with the span name of that row automatically set in the query, click a row in the **rate**, **error rate**, or **duration** columns.
@@ -0,0 +1,23 @@
---
description: Learn about how you can use tracing data in Grafana Cloud to query data,
generate metrics, and link your tracing data with logs, metrics, and profiles.
keywords:
- Grafana
- traces
- tracing
title: Use traces in Grafana
weight: 275
---
# Use traces in Grafana
Using traces, you can search for traces, generate metrics from spans, and link your tracing data with logs, metrics, and profiles.
This page provides a summary of how you can use tracing data in Grafana.
For general documentation on querying data sources in Grafana, refer to [Query and transform data](https://grafana.com/docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/).
[//]: # 'Shared content for best practices for traces'
[//]: # 'This content is located in /tempo/docs/sources/shared/tempo-in-grafana.md'
{{< docs/shared source="tempo" lookup="tempo-in-grafana.md" version="<TEMPO_VERSION>" >}}
@@ -12,7 +12,9 @@ labels:
- oss
menuTitle: Upload JSON trace file
title: Upload a JSON trace file
weight: 400
weight: 900
aliases:
- ../json-trace-file/ # /docs/grafana/latest/datasources/tempo/json-trace-file
---
# Upload a JSON trace file
@@ -20,7 +22,15 @@ weight: 400
You can upload a JSON file that contains a single trace and visualize it.
If the file has multiple traces, Grafana visualizes the first trace.
**To download a trace or Service Graph through the inspector:**
To upload a trace file:
1. Select **Explore** in Grafana.
1. Select **Import trace** in the right corner.
1. Upload your JSON trace file.
## Download a trace or service graph
To download a trace or Service Graph through the [Inspector panel](https://grafana.com/docs/grafana/<TEMPO_VERSION>/explore/explore-inspector/):
1. Open the inspector.
1. Navigate to the **Data** tab.
@@ -12,7 +12,10 @@ labels:
- oss
menuTitle: Link to a trace ID
title: Link to a trace ID
weight: 700
weight: 800
aliases:
- ../link-trace-id/ # /docs/grafana/latest/datasources/tempo/link-trace-id/
refs:
configure-grafana-feature-toggles:
- pattern: /docs/grafana/
@@ -69,10 +72,10 @@ You can link to Tempo traces from logs or metrics.
You can link to Tempo traces from logs in Loki, Elasticsearch, Splunk, and other logs data sources by configuring an internal link.
To configure this feature, see the [Derived fields]({{< relref "../loki#configure-derived-fields" >}}) section of the Loki data source docs or the [Data links]({{< relref "../elasticsearch#data-links" >}}) section of the Elasticsearch or Splunk data source docs.
To configure this feature, refer to the [Derived fields](../../loki#configure-derived-fields) section of the Loki data source docs or the [Data links](../../elasticsearch#data-links) section of the Elasticsearch or Splunk data source docs.
## Link to a trace ID from metrics
You can link to Tempo traces from metrics in Prometheus data sources by configuring an exemplar.
To configure this feature, see the [introduction to exemplars](ref:exemplars) documentation.
To configure this feature, refer to the [Exemplars](ref:exemplars) documentation.
+2 -1
View File
@@ -157,7 +157,8 @@ The node graph requires data to be returned from the data source in a specific f
## Service graph
A service graph visualizes span metrics, including rates, error rates, and durations (RED), along with service relationships. Once the requirements are configured, this pre-configured view is immediately available.
A service graph visualizes rates, error rates, and durations (RED), along with service relationships.
After the requirements are configured, this pre-configured view is immediately available.
For additional information refer to the following documentation:
@@ -113,8 +113,26 @@ Sign in to Grafana and navigate to **Administration > Authentication > Configure
1. If you wish to [map user information from SAML assertions]({{< relref "../saml#assertion-mapping" >}}), complete the **Assertion attributes mappings** section.
You also need to configure the **Groups attribute** field if you want to use group synchronization. Group sync allows you to automatically map users to Grafana teams or role-based access control roles based on their SAML group membership.
To learn more about how to configure group synchronization, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}) and [Configure group attribute sync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-group-attribute-sync) documentation.
If Azure is the Identity Provider over SAML there are caveats for the assertion attribute mappings. Due to how Azure interprets these attributes the full URL will need to be entered in the corresponding fields within the UI, which should match the URLs from the metadata XML. There are differences depending on whether it's a Role or Group claim vs other assertions which Microsoft has [documented](https://learn.microsoft.com/en-us/entra/identity-platform/reference-claims-customization#table-2-saml-restricted-claim-set).
Group and Role:
```
http://schemas.microsoft.com/ws/2008/06/identity/claims/role
http://schemas.microsoft.com/ws/2008/06/identity/claims/groups
http://schemas.microsoft.com/identity/claims/displayname
```
Other Assertions:
```
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
```
![image](https://github.com/user-attachments/assets/23910ab8-20ec-4dfd-8ef6-7dbaec51ac90)
You also need to configure the **Groups attribute** field if you want to use group synchronization. Group sync allows you to automatically map users to Grafana teams or role-based access control roles based on their SAML group membership.
To learn more about how to configure group synchronization, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}) and [Configure group attribute sync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-group-attribute-sync) documentation.
1. If you want to automatically assign users' roles based on their SAML roles, complete the **Role mapping** section.
+2 -2
View File
@@ -22,7 +22,7 @@ describe('Solo Route', () => {
cy.contains('uplot-main-div').should('not.exist');
});
it('Can view solo repeated panel in scenes', () => {
/*it('Can view solo repeated panel in scenes', () => {
// open Panel Tests - Graph NG
e2e.pages.SoloPanel.visit(
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true'
@@ -30,7 +30,7 @@ describe('Solo Route', () => {
e2e.components.Panels.Panel.title('server=A').should('exist');
cy.contains('uplot-main-div').should('not.exist');
});
});*/
it('Can view solo in repeated row and panel in scenes', () => {
// open Panel Tests - Graph NG
+51
View File
@@ -0,0 +1,51 @@
package dashboard
import (
"strings"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
var PluginIDRepoName = "plugin"
var fileProvisionedRepoPrefix = "file:"
// ProvisionedFileNameWithPrefix adds the `file:` prefix to the
// provisioner name, to be used as the annotation for dashboards
// provisioned from files
func ProvisionedFileNameWithPrefix(name string) string {
if name == "" {
return ""
}
return fileProvisionedRepoPrefix + name
}
// GetProvisionedFileNameFromMeta returns the provisioner name
// from a given annotation string, which is in the form file:<name>
func GetProvisionedFileNameFromMeta(annotation string) (string, bool) {
return strings.CutPrefix(annotation, fileProvisionedRepoPrefix)
}
// SetPluginIDMeta sets the repo name to "plugin" and the path to the plugin ID
func SetPluginIDMeta(obj unstructured.Unstructured, pluginID string) {
if pluginID == "" {
return
}
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[utils.AnnoKeyRepoName] = PluginIDRepoName
annotations[utils.AnnoKeyRepoPath] = pluginID
obj.SetAnnotations(annotations)
}
// GetPluginIDFromMeta returns the plugin ID from the meta if the repo name is "plugin"
func GetPluginIDFromMeta(obj utils.GrafanaMetaAccessor) string {
if obj.GetRepositoryName() == PluginIDRepoName {
return obj.GetRepositoryPath()
}
return ""
}
@@ -23,7 +23,6 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -312,7 +311,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
ts := time.Unix(origin_ts.Int64, 0)
repo := &utils.ResourceRepositoryInfo{
Name: origin_name.String,
Name: dashboard.ProvisionedFileNameWithPrefix(origin_name.String),
Hash: origin_hash.String,
Timestamp: &ts,
}
@@ -331,7 +330,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
meta.SetRepositoryInfo(repo)
} else if plugin_id.String != "" {
meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
Name: "plugin",
Name: dashboard.PluginIDRepoName,
Path: plugin_id.String,
})
}
@@ -427,7 +426,7 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das
out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{
OrgID: orgId,
Message: meta.GetMessage(),
PluginID: service.GetPluginIDFromMeta(meta),
PluginID: dashboard.GetPluginIDFromMeta(meta),
Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()),
FolderUID: meta.GetFolder(),
Overwrite: true, // already passed the revisionVersion checks!
@@ -0,0 +1,110 @@
package legacy
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestScanRow(t *testing.T) {
mockDB, mock, err := sqlmock.New()
require.NoError(t, err)
defer mockDB.Close() // nolint:errcheck
pathToFile := "path/to/file"
provisioner := provisioning.NewProvisioningServiceMock(context.Background())
provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" }
store := &dashboardSqlAccess{
namespacer: func(_ int64) string { return "default" },
provisioning: provisioner,
}
columns := []string{"orgId", "dashboard_id", "name", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data"}
id := int64(100)
title := "Test Dashboard"
folderUID := "folder123"
timestamp := time.Now()
k8sTimestamp := v1.Time{Time: timestamp}
version := int64(2)
message := "updated message"
createdUser := "creator"
updatedUser := "updator"
t.Run("Should scan a valid row correctly", func(t *testing.T) {
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`))
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
defer resultRows.Close() // nolint:errcheck
resultRows.Next()
row, err := store.scanRow(resultRows, false)
require.NoError(t, err)
require.NotNil(t, row)
require.Equal(t, "Test Dashboard", row.Dash.Name)
require.Equal(t, version, row.RV) // rv should be the dashboard version
require.Equal(t, v0alpha1.Unstructured{
Object: map[string]interface{}{"key": "value"},
}, row.Dash.Spec)
require.Equal(t, "default", row.Dash.Namespace)
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
meta, err := utils.MetaAccessor(row.Dash)
require.NoError(t, err)
require.Equal(t, id, meta.GetDeprecatedInternalID()) // nolint:staticcheck
require.Equal(t, version, meta.GetGeneration()) // generation should be dash version
require.Equal(t, k8sTimestamp, meta.GetCreationTimestamp())
require.Equal(t, "user:"+createdUser, meta.GetCreatedBy()) // should be prefixed by user:
require.Equal(t, "user:"+updatedUser, meta.GetUpdatedBy()) // should be prefixed by user:
require.Equal(t, message, meta.GetMessage())
require.Equal(t, folderUID, meta.GetFolder())
})
t.Run("File provisioned dashboard should have annotations", func(t *testing.T) {
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`))
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
defer resultRows.Close() // nolint:errcheck
resultRows.Next()
row, err := store.scanRow(resultRows, false)
require.NoError(t, err)
require.NotNil(t, row)
meta, err := utils.MetaAccessor(row.Dash)
require.NoError(t, err)
require.Equal(t, "file:provisioner", meta.GetRepositoryName()) // should be prefixed by file:
require.Equal(t, "../"+pathToFile, meta.GetRepositoryPath()) // relative to provisioner
require.Equal(t, "hashing", meta.GetRepositoryHash())
ts, err := meta.GetRepositoryTimestamp()
require.NoError(t, err)
require.Equal(t, int64(100000), ts.Unix())
})
t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) {
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`))
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
defer resultRows.Close() // nolint:errcheck
resultRows.Next()
row, err := store.scanRow(resultRows, false)
require.NoError(t, err)
require.NotNil(t, row)
meta, err := utils.MetaAccessor(row.Dash)
require.NoError(t, err)
require.Equal(t, "plugin", meta.GetRepositoryName())
require.Equal(t, "slo", meta.GetRepositoryPath()) // the ID of the plugin
require.Equal(t, "", meta.GetRepositoryHash()) // hash is not used on plugins
})
}
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/selection"
)
type DashboardSearchClient struct {
@@ -29,6 +30,7 @@ func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchC
return &DashboardSearchClient{dashboardStore: dashboardStore}
}
// nolint:gocyclo
func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
@@ -103,6 +105,8 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
}
for _, field := range req.Options.Fields {
vals := field.GetValues()
switch field.Key {
case resource.SEARCH_FIELD_TAGS:
query.Tags = field.GetValues()
@@ -110,7 +114,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
query.DashboardUIDs = field.GetValues()
query.DashboardIds = nil
case resource.SEARCH_FIELD_FOLDER:
vals := field.GetValues()
folders := make([]string, len(vals))
for i, val := range vals {
@@ -122,12 +125,28 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
}
query.FolderUIDs = folders
}
}
case resource.SEARCH_FIELD_REPOSITORY_PATH:
// only one value is supported in legacy search
if len(vals) != 1 {
return nil, fmt.Errorf("only one repo path query is supported")
}
query.ProvisionedPath = vals[0]
case resource.SEARCH_FIELD_REPOSITORY_NAME:
if field.Operator == string(selection.NotIn) {
for _, val := range vals {
name, _ := dashboard.GetProvisionedFileNameFromMeta(val)
query.ProvisionedReposNotIn = append(query.ProvisionedReposNotIn, name)
}
continue
}
res, err := c.dashboardStore.FindDashboards(ctx, query)
if err != nil {
return nil, err
// only one value is supported in legacy search
if len(vals) != 1 {
return nil, fmt.Errorf("only one repo name is supported")
}
query.ProvisionedRepo, _ = dashboard.GetProvisionedFileNameFromMeta(vals[0])
}
}
searchFields := resource.StandardSearchFields()
@@ -141,6 +160,41 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
},
}
// if we are querying for provisioning information, we need to use a different
// legacy sql query, since legacy search does not support this
if query.ProvisionedRepo != "" || len(query.ProvisionedReposNotIn) > 0 {
var dashes []*dashboards.Dashboard
if query.ProvisionedRepo == dashboard.PluginIDRepoName {
dashes, err = c.dashboardStore.GetDashboardsByPluginID(ctx, &dashboards.GetDashboardsByPluginIDQuery{
PluginID: query.ProvisionedPath,
OrgID: user.GetOrgID(),
})
} else if query.ProvisionedRepo != "" {
dashes, err = c.dashboardStore.GetProvisionedDashboardsByName(ctx, query.ProvisionedRepo)
} else if len(query.ProvisionedReposNotIn) > 0 {
dashes, err = c.dashboardStore.GetOrphanedProvisionedDashboards(ctx, query.ProvisionedReposNotIn)
}
if err != nil {
return nil, err
}
for _, dashboard := range dashes {
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
Key: getResourceKey(&dashboards.DashboardSearchProjection{
UID: dashboard.UID,
}, req.Options.Key.Namespace),
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), []byte{}},
})
}
return list, nil
}
res, err := c.dashboardStore.FindDashboards(ctx, query)
if err != nil {
return nil, err
}
hits := formatQueryResult(res)
for _, dashboard := range hits {
@@ -155,6 +209,8 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
})
}
list.TotalHits = int64(len(list.Results.Rows))
return list, nil
}
@@ -0,0 +1,264 @@
package legacysearcher
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/dashboard"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/selection"
)
func TestDashboardSearchClient_Search(t *testing.T) {
mockStore := dashboards.NewFakeDashboardStore(t)
client := NewDashboardSearchClient(mockStore)
ctx := context.Background()
user := &user.SignedInUser{OrgID: 2}
ctx = identity.WithRequester(ctx, user)
dashboardKey := &resource.ResourceKey{
Name: "uid",
Resource: dashboard.DASHBOARD_RESOURCE,
}
t.Run("Should parse results into GRPC", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Term: "term"},
{UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
tags, err := json.Marshal([]string{"term"})
require.NoError(t, err)
emptyTags, err := json.Marshal([]string{})
require.NoError(t, err)
require.NotNil(t, resp)
searchFields := resource.StandardSearchFields()
require.Equal(t, &resource.ResourceSearchResponse{
TotalHits: 2,
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard"),
[]byte("folder1"),
tags,
},
},
{
Key: &resource.ResourceKey{
Name: "uid2",
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
},
Cells: [][]byte{
[]byte("Test Dashboard2"),
[]byte("folder2"),
emptyTags,
},
},
},
},
}, resp)
mockStore.AssertExpectations(t)
})
t.Run("Query should be set as the title, and * should be removed", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
Title: "test",
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
},
Query: "*test*",
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should read labels for the dashboard ids", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
DashboardIds: []int64{1, 2},
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Labels: []*resource.Requirement{
{
Key: utils.LabelKeyDeprecatedInternalID,
Operator: "in",
Values: []string{"1", "2"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should modify fields to legacy compatible queries", func(t *testing.T) {
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
DashboardUIDs: []string{"uid1", "uid2"},
Tags: []string{"tag1", "tag2"},
FolderUIDs: []string{"general", "folder1"},
SignedInUser: user, // user from context should be used
Type: "dash-db", // should set type based off of key
}).Return([]dashboards.DashboardSearchProjection{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_TAGS,
Operator: "in",
Values: []string{"tag1", "tag2"},
},
{
Key: resource.SEARCH_FIELD_NAME, // name should be used as uid
Operator: "in",
Values: []string{"uid1", "uid2"},
},
{
Key: resource.SEARCH_FIELD_FOLDER,
Operator: "in",
Values: []string{"", "folder1"}, // empty folder should be general
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should retrieve dashboards by plugin through a different function", func(t *testing.T) {
mockStore.On("GetDashboardsByPluginID", mock.Anything, &dashboards.GetDashboardsByPluginIDQuery{
PluginID: "slo",
OrgID: 2, // retrieved from the signed in user
}).Return([]*dashboards.Dashboard{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_REPOSITORY_PATH,
Operator: "in",
Values: []string{"slo"},
},
{
Key: resource.SEARCH_FIELD_REPOSITORY_NAME,
Operator: "in",
Values: []string{"plugin"},
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should retrieve dashboards by provisioner name through a different function", func(t *testing.T) {
mockStore.On("GetProvisionedDashboardsByName", mock.Anything, "test").Return([]*dashboards.Dashboard{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_REPOSITORY_NAME,
Operator: "in",
Values: []string{"file:test"}, // file prefix should be removed before going to legacy
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
t.Run("Should retrieve orphaned dashboards if provisioner not in is specified", func(t *testing.T) {
mockStore.On("GetOrphanedProvisionedDashboards", mock.Anything, []string{"test", "test2"}).Return([]*dashboards.Dashboard{
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1"},
}, nil).Once()
req := &resource.ResourceSearchRequest{
Options: &resource.ListOptions{
Key: dashboardKey,
Fields: []*resource.Requirement{
{
Key: resource.SEARCH_FIELD_REPOSITORY_NAME,
Operator: string(selection.NotIn),
Values: []string{"file:test", "file:test2"}, // file prefix should be removed before going to legacy
},
},
},
}
resp, err := client.Search(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
mockStore.AssertExpectations(t)
})
}
+145 -21
View File
@@ -3,6 +3,7 @@ package dashboard
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"slices"
@@ -25,7 +26,10 @@ import (
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
"github.com/grafana/grafana/pkg/services/featuremgmt"
foldermodel "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/util/errhttp"
@@ -33,17 +37,19 @@ import (
// The DTO returns everything the UI needs in a single request
type SearchHandler struct {
log log.Logger
client func(context.Context) resource.ResourceIndexClient
tracer trace.Tracer
log log.Logger
client func(context.Context) resource.ResourceIndexClient
tracer trace.Tracer
features featuremgmt.FeatureToggles
}
func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler {
func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient, features featuremgmt.FeatureToggles) *SearchHandler {
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, unified.GetResourceClient, legacyDashboardSearcher)
return &SearchHandler{
client: searchClient,
log: log.New("grafana-apiserver.dashboards.search"),
tracer: tracer,
client: searchClient,
log: log.New("grafana-apiserver.dashboards.search"),
tracer: tracer,
features: features,
}
}
@@ -252,19 +258,6 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
}
searchRequest.Fields = fields
// Add the folder constraint. Note this does not do recursive search
folder := queryParams.Get("folder")
if folder != "" {
if folder == rootFolder {
folder = "" // root folder is empty in the search index
}
searchRequest.Options.Fields = []*resource.Requirement{{
Key: "folder",
Operator: "=",
Values: []string{folder},
}}
}
types := queryParams["type"]
var federate *resource.ResourceKey
switch len(types) {
@@ -329,7 +322,33 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
}
// The names filter
if names, ok := queryParams["name"]; ok {
names := queryParams["name"]
// Add the folder constraint. Note this does not do recursive search
folder := queryParams.Get("folder")
if folder == foldermodel.SharedWithMeFolderUID {
dashboardUIDs, err := s.getDashboardsUIDsSharedWithUser(ctx, user)
if err != nil {
errhttp.Write(ctx, err, w)
return
}
// hijacks the "name" query param to only search for shared dashboard UIDs
if len(dashboardUIDs) > 0 {
names = append(names, dashboardUIDs...)
}
} else if folder != "" {
if folder == rootFolder {
folder = "" // root folder is empty in the search index
}
searchRequest.Options.Fields = []*resource.Requirement{{
Key: "folder",
Operator: "=",
Values: []string{folder},
}}
}
if len(names) > 0 {
if searchRequest.Options.Fields == nil {
searchRequest.Options.Fields = []*resource.Requirement{}
}
@@ -378,3 +397,108 @@ func asResourceKey(ns string, k string) (*resource.ResourceKey, error) {
return key, nil
}
func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, user identity.Requester) ([]string, error) {
if !s.features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering) {
return []string{}, nil
}
// gets dashboards that the user was granted read access to
permissions := user.GetPermissions()
dashboardPermissions := permissions[dashboards.ActionDashboardsRead]
dashboardUids := make([]string, 0)
sharedDashboards := make([]string, 0)
for _, dashboardPermission := range dashboardPermissions {
if dashboardUid, found := strings.CutPrefix(dashboardPermission, dashboards.ScopeDashboardsPrefix); found {
if !slices.Contains(dashboardUids, dashboardUid) {
dashboardUids = append(dashboardUids, dashboardUid)
}
}
}
if len(dashboardUids) == 0 {
return sharedDashboards, nil
}
key, err := asResourceKey(user.GetNamespace(), dashboard.DASHBOARD_RESOURCE)
if err != nil {
return sharedDashboards, err
}
dashboardSearchRequest := &resource.ResourceSearchRequest{
Fields: []string{"folder"},
Limit: int64(len(dashboardUids)),
Options: &resource.ListOptions{
Key: key,
Fields: []*resource.Requirement{{
Key: "name",
Operator: "in",
Values: dashboardUids,
}},
},
}
// get all dashboards user has access to, along with their parent folder uid
dashboardResult, err := s.client(ctx).Search(ctx, dashboardSearchRequest)
if err != nil {
return sharedDashboards, err
}
folderUidIdx := -1
for i, col := range dashboardResult.Results.Columns {
if col.Name == "folder" {
folderUidIdx = i
}
}
if folderUidIdx == -1 {
return sharedDashboards, fmt.Errorf("Error retrieving folder information")
}
// populate list of unique folder UIDs in the list of dashboards user has read permissions
allFolders := make([]string, 0)
for _, dash := range dashboardResult.Results.Rows {
folderUid := string(dash.Cells[folderUidIdx])
if folderUid != "" && !slices.Contains(allFolders, folderUid) {
allFolders = append(allFolders, folderUid)
}
}
// only folders the user has access to will be returned here
folderKey, err := asResourceKey(user.GetNamespace(), folderv0alpha1.RESOURCE)
if err != nil {
return sharedDashboards, err
}
folderSearchRequest := &resource.ResourceSearchRequest{
Fields: []string{"folder"},
Limit: int64(len(allFolders)),
Options: &resource.ListOptions{
Key: folderKey,
Fields: []*resource.Requirement{{
Key: "name",
Operator: "in",
Values: allFolders,
}},
},
}
foldersResult, err := s.client(ctx).Search(ctx, folderSearchRequest)
if err != nil {
return sharedDashboards, err
}
foldersWithAccess := make([]string, 0, len(foldersResult.Results.Rows))
for _, fold := range foldersResult.Results.Rows {
foldersWithAccess = append(foldersWithAccess, fold.Key.Name)
}
// add to sharedDashboards dashboards user has access to, but does NOT have access to it's parent folder
for _, dash := range dashboardResult.Results.Rows {
dashboardUid := dash.Key.Name
folderUid := string(dash.Cells[folderUidIdx])
if folderUid != "" && !slices.Contains(foldersWithAccess, folderUid) {
sharedDashboards = append(sharedDashboards, dashboardUid)
}
}
return sharedDashboards, nil
}
+209 -35
View File
@@ -12,6 +12,8 @@ import (
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -31,7 +33,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0},
},
}
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
@@ -59,7 +61,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1},
},
}
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
@@ -87,7 +89,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2},
},
}
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
@@ -115,7 +117,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3},
},
}
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
@@ -143,7 +145,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4},
},
}
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
@@ -171,7 +173,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5},
},
}
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, nil)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
@@ -191,17 +193,19 @@ func TestSearchFallback(t *testing.T) {
}
func TestSearchHandler(t *testing.T) {
// Create a mock client
mockClient := &MockClient{}
// Initialize the search handler with the mock client
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
}
t.Run("Multiple comma separated fields will be appended to default dashboard search fields", func(t *testing.T) {
// Create a mock client
mockClient := &MockClient{}
features := featuremgmt.WithFeatures()
// Initialize the search handler with the mock client
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
features: features,
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search?field=field1&field=field2&field=field3", nil)
req.Header.Add("content-type", "application/json")
@@ -219,6 +223,18 @@ func TestSearchHandler(t *testing.T) {
})
t.Run("Single field will be appended to default dashboard search fields", func(t *testing.T) {
// Create a mock client
mockClient := &MockClient{}
features := featuremgmt.WithFeatures()
// Initialize the search handler with the mock client
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
features: features,
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search?field=field1", nil)
req.Header.Add("content-type", "application/json")
@@ -236,6 +252,18 @@ func TestSearchHandler(t *testing.T) {
})
t.Run("Passing no fields will search using default dashboard fields", func(t *testing.T) {
// Create a mock client
mockClient := &MockClient{}
features := featuremgmt.WithFeatures()
// Initialize the search handler with the mock client
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
features: features,
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
@@ -253,6 +281,41 @@ func TestSearchHandler(t *testing.T) {
})
t.Run("Sort - default sort by resource then title", func(t *testing.T) {
rows := make([]*resource.ResourceTableRow, len(mockResults))
for i, r := range mockResults {
rows[i] = &resource.ResourceTableRow{
Key: &resource.ResourceKey{
Name: r.Name,
Resource: r.Resource,
},
Cells: [][]byte{
[]byte(r.Value),
},
}
}
mockResponse := &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{Name: resource.SEARCH_FIELD_TITLE},
},
Rows: rows,
},
}
// Create a mock client
mockClient := &MockClient{
MockResponses: []*resource.ResourceSearchResponse{mockResponse},
}
features := featuremgmt.WithFeatures()
// Initialize the search handler with the mock client
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
features: features,
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
@@ -280,6 +343,125 @@ func TestSearchHandler(t *testing.T) {
})
}
func TestSearchHandlerSharedDashboards(t *testing.T) {
t.Run("should bail out if FlagUnifiedStorageSearchPermissionFiltering is not enabled globally", func(t *testing.T) {
mockClient := &MockClient{}
features := featuremgmt.WithFeatures()
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
features: features,
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search?folder=sharedwithme", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
assert.Equal(t, mockClient.CallCount, 1)
})
t.Run("should return the dashboards shared with the user", func(t *testing.T) {
// dashboardSearchRequest
mockResponse1 := &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "folder",
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "dashboardinroot",
Resource: "dashboard",
},
Cells: [][]byte{[]byte("")}, // root folder doesn't have uid
},
{
Key: &resource.ResourceKey{
Name: "dashboardinprivatefolder",
Resource: "dashboard",
},
Cells: [][]byte{
[]byte("privatefolder"), // folder uid
},
},
{
Key: &resource.ResourceKey{
Name: "dashboardinpublicfolder",
Resource: "dashboard",
},
Cells: [][]byte{
[]byte("publicfolder"), // folder uid
},
},
},
},
}
// folderSearchRequest
mockResponse2 := &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "folder",
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "publicfolder",
Resource: "folder",
},
Cells: [][]byte{
[]byte(""), // root folder uid
},
},
},
},
}
mockClient := &MockClient{
MockResponses: []*resource.ResourceSearchResponse{mockResponse1, mockResponse2},
}
features := featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearchPermissionFiltering)
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
features: features,
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search?folder=sharedwithme", nil)
req.Header.Add("content-type", "application/json")
allPermissions := make(map[int64]map[string][]string)
permissions := make(map[string][]string)
permissions[dashboards.ActionDashboardsRead] = []string{"dashboards:uid:dashboardinroot", "dashboards:uid:dashboardinprivatefolder", "dashboards:uid:dashboardinpublicfolder"}
allPermissions[1] = permissions
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions}))
searchHandler.DoSearch(rr, req)
assert.Equal(t, mockClient.CallCount, 3)
// first call gets all dashboards user has permission for
firstCall := mockClient.MockCalls[0]
assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder"})
// second call gets folders associated with the previous dashboards
secondCall := mockClient.MockCalls[1]
assert.Equal(t, secondCall.Options.Fields[0].Values, []string{"privatefolder", "publicfolder"})
// lastly, search ONLY for dashboards user has permission to read that are within folders the user does NOT have
// permission to read
thirdCall := mockClient.MockCalls[2]
assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder"})
})
}
// MockClient implements the ResourceIndexClient interface for testing
type MockClient struct {
resource.ResourceIndexClient
@@ -287,6 +469,10 @@ type MockClient struct {
// Capture the last SearchRequest for assertions
LastSearchRequest *resource.ResourceSearchRequest
MockResponses []*resource.ResourceSearchResponse
MockCalls []*resource.ResourceSearchRequest
CallCount int
}
type MockResult struct {
@@ -320,28 +506,16 @@ var mockResults = []MockResult{
func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
m.LastSearchRequest = in
m.MockCalls = append(m.MockCalls, in)
rows := make([]*resource.ResourceTableRow, len(mockResults))
for i, r := range mockResults {
rows[i] = &resource.ResourceTableRow{
Key: &resource.ResourceKey{
Name: r.Name,
Resource: r.Resource,
},
Cells: [][]byte{
[]byte(r.Value),
},
}
var response *resource.ResourceSearchResponse
if m.CallCount < len(m.MockResponses) {
response = m.MockResponses[m.CallCount]
}
return &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{Name: resource.SEARCH_FIELD_TITLE},
},
Rows: rows,
},
}, nil
m.CallCount = m.CallCount + 1
return response, nil
}
func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
return nil, nil
+1 -1
View File
@@ -125,7 +125,7 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
if err != nil {
return nil, err
}
if repo != nil && repo.Name == "plugin" {
if repo != nil && repo.Name == dashboard.PluginIDRepoName {
dto.PluginID = repo.Path
}
@@ -82,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
features: features,
accessControl: accessControl,
unified: unified,
search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher),
search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher, features),
legacy: &dashboard.DashboardStorage{
Resource: dashboardv0alpha1.DashboardResourceInfo,
+2
View File
@@ -81,6 +81,8 @@ type Store interface {
GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error)
GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error)
GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error)
GetProvisionedDashboardsByName(ctx context.Context, name string) ([]*Dashboard, error)
GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string) ([]*Dashboard, error)
SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error)
SaveProvisionedDashboard(ctx context.Context, dash *Dashboard, provisioning *DashboardProvisioning) error
UnprovisionDashboard(ctx context.Context, id int64) error
@@ -1,4 +1,4 @@
// Code generated by mockery v2.32.0. DO NOT EDIT.
// Code generated by mockery v2.52.2. DO NOT EDIT.
package dashboards
@@ -18,6 +18,10 @@ type FakeDashboardProvisioning struct {
func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error {
ret := _m.Called(ctx, cmd)
if len(ret) == 0 {
panic("no return value specified for DeleteOrphanedProvisionedDashboards")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok {
r0 = rf(ctx, cmd)
@@ -32,6 +36,10 @@ func (_m *FakeDashboardProvisioning) DeleteOrphanedProvisionedDashboards(ctx con
func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error {
ret := _m.Called(ctx, dashboardID, orgID)
if len(ret) == 0 {
panic("no return value specified for DeleteProvisionedDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
r0 = rf(ctx, dashboardID, orgID)
@@ -46,6 +54,10 @@ func (_m *FakeDashboardProvisioning) DeleteProvisionedDashboard(ctx context.Cont
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardData")
}
var r0 []*DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*DashboardProvisioning, error)); ok {
@@ -72,6 +84,10 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardData(ctx context.Con
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, dashboardID)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardDataByDashboardID")
}
var r0 *DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*DashboardProvisioning, error)); ok {
@@ -98,6 +114,10 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardID(ct
func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, orgID, dashboardUID)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardDataByDashboardUID")
}
var r0 *DashboardProvisioning
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*DashboardProvisioning, error)); ok {
@@ -124,6 +144,10 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c
func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *folder.CreateFolderCommand) (*folder.Folder, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for SaveFolderForProvisionedDashboards")
}
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *folder.CreateFolderCommand) (*folder.Folder, error)); ok {
@@ -150,6 +174,10 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont
func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) {
ret := _m.Called(ctx, dto, provisioning)
if len(ret) == 0 {
panic("no return value specified for SaveProvisionedDashboard")
}
var r0 *Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) (*Dashboard, error)); ok {
@@ -176,6 +204,10 @@ func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Contex
func (_m *FakeDashboardProvisioning) UnprovisionDashboard(ctx context.Context, dashboardID int64) error {
ret := _m.Called(ctx, dashboardID)
if len(ret) == 0 {
panic("no return value specified for UnprovisionDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, dashboardID)
@@ -1,4 +1,4 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Code generated by mockery v2.52.2. DO NOT EDIT.
package dashboards
@@ -74,6 +74,34 @@ func (_m *FakeDashboardService) CleanUpDeletedDashboards(ctx context.Context) (i
return r0, r1
}
// CountDashboardsInOrg provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
ret := _m.Called(ctx, orgID)
if len(ret) == 0 {
panic("no return value specified for CountDashboardsInOrg")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
return rf(ctx, orgID)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
r0 = rf(ctx, orgID)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, orgID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountInFolders provides a mock function with given fields: ctx, orgID, folderUIDs, user
func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, user identity.Requester) (int64, error) {
ret := _m.Called(ctx, orgID, folderUIDs, user)
@@ -102,6 +130,24 @@ func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64,
return r0, r1
}
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardService) DeleteAllDashboards(ctx context.Context, orgID int64) error {
ret := _m.Called(ctx, orgID)
if len(ret) == 0 {
panic("no return value specified for DeleteAllDashboards")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, orgID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteDashboard provides a mock function with given fields: ctx, dashboardId, dashboardUID, orgId
func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
ret := _m.Called(ctx, dashboardId, dashboardUID, orgId)
@@ -120,24 +166,6 @@ func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId
return r0
}
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardService) DeleteAllDashboards(ctx context.Context, orgID int64) error {
ret := _m.Called(ctx, orgID)
if len(ret) == 0 {
panic("no return value specified for DeleteDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, orgID)
} else {
r0 = ret.Error(0)
}
return r0
}
// FindDashboards provides a mock function with given fields: ctx, query
func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
ret := _m.Called(ctx, query)
@@ -168,36 +196,6 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindP
return r0, r1
}
// CountDashboardsInOrg provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
ret := _m.Called(ctx, orgID)
if len(ret) == 0 {
panic("no return value specified for CountDashboardsInOrg")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
return rf(ctx, orgID)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
r0 = rf(ctx, orgID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(int64)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, orgID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllDashboards provides a mock function with given fields: ctx
func (_m *FakeDashboardService) GetAllDashboards(ctx context.Context) ([]*Dashboard, error) {
ret := _m.Called(ctx)
@@ -228,6 +226,7 @@ func (_m *FakeDashboardService) GetAllDashboards(ctx context.Context) ([]*Dashbo
return r0, r1
}
// GetAllDashboardsByOrgId provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardService) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) {
ret := _m.Called(ctx, orgID)
@@ -146,6 +146,38 @@ func (d *dashboardStore) GetProvisionedDashboardData(ctx context.Context, name s
return result, err
}
func (d *dashboardStore) GetProvisionedDashboardsByName(ctx context.Context, name string) ([]*dashboards.Dashboard, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.GetProvisionedDashboardsByName")
defer span.End()
dashes := []*dashboards.Dashboard{}
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.Table(`dashboard`).
Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`).
Where(`dashboard_provisioning.name = ?`, name).Find(&dashes)
})
if err != nil {
return nil, err
}
return dashes, nil
}
func (d *dashboardStore) GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string) ([]*dashboards.Dashboard, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.GetOrphanedProvisionedDashboards")
defer span.End()
dashes := []*dashboards.Dashboard{}
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.Table(`dashboard`).
Join(`INNER`, `dashboard_provisioning`, `dashboard.id = dashboard_provisioning.dashboard_id`).
NotIn(`dashboard_provisioning.name`, notIn).Find(&dashes)
})
if err != nil {
return nil, err
}
return dashes, nil
}
func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, dash *dashboards.Dashboard, provisioning *dashboards.DashboardProvisioning) error {
ctx, span := tracer.Start(ctx, "dashboards.database.SaveProvisionedDashboard")
defer span.End()
@@ -251,26 +251,51 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
t.Run("Should delete associated provisioning info, even without the dashboard existing in the db", func(t *testing.T) {
setup()
dash1 := insertTestDashboard(t, dashboardStore, "provisioned", 1, 0, "", false, "provisioned")
dash2 := insertTestDashboard(t, dashboardStore, "orphaned", 1, 0, "", false, "orphaned")
provisioningData := &dashboards.DashboardProvisioning{
ID: 1,
DashboardID: 200,
DashboardID: dash1.ID,
Name: "test",
CheckSum: "123",
Updated: 54321,
ExternalID: "/path/to/dashboard",
}
err := dashboardStore.SaveProvisionedDashboard(context.Background(), &dashboards.Dashboard{
ID: 200,
}, provisioningData)
err := dashboardStore.SaveProvisionedDashboard(context.Background(), dash1, provisioningData)
require.NoError(t, err)
provisioningData2 := &dashboards.DashboardProvisioning{
ID: 1,
DashboardID: dash2.ID,
Name: "orphaned",
CheckSum: "123",
Updated: 54321,
ExternalID: "/path/to/dashboard",
}
err = dashboardStore.SaveProvisionedDashboard(context.Background(), dash2, provisioningData2)
require.NoError(t, err)
// get provisioning data
res, err := dashboardStore.GetProvisionedDashboardData(context.Background(), "test")
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, res[0], provisioningData)
// get dashboards within the provisioner
dashs, err := dashboardStore.GetProvisionedDashboardsByName(context.Background(), "test")
require.NoError(t, err)
require.Len(t, dashs, 1)
// find dashboards not within that provisioner
dashs, err = dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test"})
require.NoError(t, err)
require.Len(t, dashs, 1)
// if both are provided, nothing should be returned
dashs, err = dashboardStore.GetOrphanedProvisionedDashboards(context.Background(), []string{"test", "orphaned"})
require.NoError(t, err)
require.Len(t, dashs, 0)
err = dashboardStore.CleanupAfterDelete(context.Background(), &dashboards.DeleteDashboardCommand{
ID: 200,
ID: dash1.ID,
OrgID: 1,
UID: "test",
})
@@ -958,7 +958,7 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: query.OrgID,
ProvisionedRepo: pluginIDRepoName,
ProvisionedRepo: dashboard.PluginIDRepoName,
ProvisionedPath: query.PluginID,
})
if err != nil {
@@ -1559,7 +1559,7 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C
delete(annotations, utils.AnnoKeyRepoHash)
delete(annotations, utils.AnnoKeyRepoTimestamp)
} else {
annotations[utils.AnnoKeyRepoName] = provisionedFileNameWithPrefix(provisioning.Name)
annotations[utils.AnnoKeyRepoName] = dashboard.ProvisionedFileNameWithPrefix(provisioning.Name)
annotations[utils.AnnoKeyRepoPath] = provisioning.ExternalID
annotations[utils.AnnoKeyRepoHash] = provisioning.CheckSum
annotations[utils.AnnoKeyRepoTimestamp] = time.Unix(provisioning.Updated, 0).UTC().Format(time.RFC3339)
@@ -1580,7 +1580,7 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd
return nil, err
}
setPluginID(obj, cmd.PluginID)
dashboard.SetPluginIDMeta(obj, cmd.PluginID)
out, err := dr.createOrUpdateDash(ctx, obj, orgID)
if err != nil {
@@ -1827,13 +1827,13 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
ctx, _ = identity.WithServiceIdentity(ctx, query.OrgId)
if query.ProvisionedRepo != "" {
query.ProvisionedRepo = provisionedFileNameWithPrefix(query.ProvisionedRepo)
query.ProvisionedRepo = dashboard.ProvisionedFileNameWithPrefix(query.ProvisionedRepo)
}
if len(query.ProvisionedReposNotIn) > 0 {
repos := make([]string, len(query.ProvisionedReposNotIn))
for i, v := range query.ProvisionedReposNotIn {
repos[i] = provisionedFileNameWithPrefix(v)
repos[i] = dashboard.ProvisionedFileNameWithPrefix(v)
}
query.ProvisionedReposNotIn = repos
}
@@ -1865,7 +1865,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
}
// ensure the repo is set due to file provisioning, otherwise skip it
fileRepo, found := getProvisionedFileNameFromMeta(meta)
fileRepo, found := dashboard.GetProvisionedFileNameFromMeta(meta.GetRepositoryName())
if !found {
return nil
}
@@ -1967,7 +1967,7 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
out.Deleted = obj.GetDeletionTimestamp().Time
}
out.PluginID = GetPluginIDFromMeta(obj)
out.PluginID = dashboard.GetPluginIDFromMeta(obj)
creator, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy())
if err != nil {
@@ -2012,42 +2012,6 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
return &out, nil
}
var pluginIDRepoName = "plugin"
var fileProvisionedRepoPrefix = "file:"
func setPluginID(obj unstructured.Unstructured, pluginID string) {
if pluginID == "" {
return
}
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[utils.AnnoKeyRepoName] = pluginIDRepoName
annotations[utils.AnnoKeyRepoPath] = pluginID
obj.SetAnnotations(annotations)
}
func provisionedFileNameWithPrefix(name string) string {
if name == "" {
return ""
}
return fileProvisionedRepoPrefix + name
}
func getProvisionedFileNameFromMeta(obj utils.GrafanaMetaAccessor) (string, bool) {
return strings.CutPrefix(obj.GetRepositoryName(), fileProvisionedRepoPrefix)
}
func GetPluginIDFromMeta(obj utils.GrafanaMetaAccessor) string {
if obj.GetRepositoryName() == pluginIDRepoName {
return obj.GetRepositoryPath()
}
return ""
}
func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, namespace string) (unstructured.Unstructured, error) {
uid := cmd.GetDashboardModel().UID
if uid == "" {
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/dashboard"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/client"
@@ -547,7 +548,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
utils.LabelKeyDeprecatedInternalID: "1", // nolint:staticcheck
},
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
@@ -563,7 +564,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
k8sCliMock.On("Search", mock.Anything, int64(1),
mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
// ensure the prefix is added to the query
return req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix(repo)
return req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix(repo)
})).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{},
@@ -573,7 +574,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
}, nil).Once()
k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
// ensure the prefix is added to the query
return req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix(repo)
return req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix(repo)
})).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -646,7 +647,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) {
utils.LabelKeyDeprecatedInternalID: "1", // nolint:staticcheck
},
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
@@ -736,7 +737,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) {
utils.LabelKeyDeprecatedInternalID: "1", // nolint:staticcheck
},
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
@@ -825,7 +826,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
"metadata": map[string]any{
"name": "uid",
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "orphaned",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("orphaned"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
@@ -838,7 +839,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
"metadata": map[string]any{
"name": "uid2",
"annotations": map[string]any{
utils.AnnoKeyRepoName: "plugin",
utils.AnnoKeyRepoName: dashboard.PluginIDRepoName,
utils.AnnoKeyRepoHash: "app",
},
},
@@ -849,7 +850,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
"metadata": map[string]any{
"name": "uid3",
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "orphaned",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("orphaned"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
@@ -858,7 +859,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
"spec": map[string]any{},
}}, nil).Once()
k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
})).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -888,7 +889,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
}, nil).Once()
k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == provisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == dashboard.ProvisionedFileNameWithPrefix("test") && req.Options.Fields[0].Operator == "notin"
})).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -959,7 +960,7 @@ func TestUnprovisionDashboard(t *testing.T) {
"metadata": map[string]any{
"name": "uid",
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
@@ -1053,7 +1054,7 @@ func TestGetDashboardsByPluginID(t *testing.T) {
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == "plugin" &&
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == dashboard.PluginIDRepoName &&
req.Options.Fields[1].Key == "repo.path" && req.Options.Fields[1].Values[0] == "testing"
})).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
@@ -1983,7 +1984,7 @@ func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
"metadata": map[string]any{
"name": "uid",
"annotations": map[string]any{
utils.AnnoKeyRepoName: fileProvisionedRepoPrefix + "test",
utils.AnnoKeyRepoName: dashboard.ProvisionedFileNameWithPrefix("test"),
utils.AnnoKeyRepoHash: "hash",
utils.AnnoKeyRepoPath: "path/to/file",
utils.AnnoKeyRepoTimestamp: "2025-01-01T00:00:00Z",
+90 -31
View File
@@ -1,4 +1,4 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Code generated by mockery v2.52.2. DO NOT EDIT.
package dashboards
@@ -66,36 +66,6 @@ func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParamet
return r0, r1
}
// CountInOrg provides a mock function with given fields: _a0, _a1
func (_m *FakeDashboardStore) CountInOrg(_a0 context.Context, _a1 int64) (int64, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Count")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(int64)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountDashboardsInFolders provides a mock function with given fields: ctx, request
func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) {
ret := _m.Called(ctx, request)
@@ -124,6 +94,35 @@ func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, requ
return r0, r1
}
// CountInOrg provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64) (int64, error) {
ret := _m.Called(ctx, orgID)
if len(ret) == 0 {
panic("no return value specified for Count")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (int64, error)); ok {
return rf(ctx, orgID)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
r0 = rf(ctx, orgID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(int64)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, orgID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardStore) DeleteAllDashboards(ctx context.Context, orgID int64) error {
ret := _m.Called(ctx, orgID)
@@ -436,6 +435,36 @@ func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query
return r0, r1
}
// GetOrphanedProvisionedDashboards provides a mock function with given fields: ctx, notIn
func (_m *FakeDashboardStore) GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string) ([]*Dashboard, error) {
ret := _m.Called(ctx, notIn)
if len(ret) == 0 {
panic("no return value specified for GetOrphanedProvisionedDashboards")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, []string) ([]*Dashboard, error)); ok {
return rf(ctx, notIn)
}
if rf, ok := ret.Get(0).(func(context.Context, []string) []*Dashboard); ok {
r0 = rf(ctx, notIn)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*Dashboard)
}
}
if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok {
r1 = rf(ctx, notIn)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProvisionedDashboardData provides a mock function with given fields: ctx, name
func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) {
ret := _m.Called(ctx, name)
@@ -466,6 +495,36 @@ func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, n
return r0, r1
}
// GetProvisionedDashboardsByName provides a mock function with given fields: ctx, name
func (_m *FakeDashboardStore) GetProvisionedDashboardsByName(ctx context.Context, name string) ([]*Dashboard, error) {
ret := _m.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for GetProvisionedDashboardsByName")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) ([]*Dashboard, error)); ok {
return rf(ctx, name)
}
if rf, ok := ret.Get(0).(func(context.Context, string) []*Dashboard); ok {
r0 = rf(ctx, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*Dashboard)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProvisionedDataByDashboardID provides a mock function with given fields: ctx, dashboardID
func (_m *FakeDashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) {
ret := _m.Called(ctx, dashboardID)
@@ -194,17 +194,34 @@ export const generateColumns = (
if (!info && p === 'general') {
info = { kind: 'folder', url: '/dashboards', name: 'Dashboards' };
}
return info ? (
<a key={p} href={info.url} className={styles.locationItem}>
<Icon name={getIconForKind(info.kind)} />
<Text variant="body" truncate>
{info.name}
</Text>
</a>
) : (
<span key={p}>{p}</span>
);
if (info) {
const content = (
<>
<Icon name={getIconForKind(info.kind)} />
<Text variant="body" truncate>
{info.name}
</Text>
</>
);
if (info.url) {
return (
<a key={p} href={info.url} className={styles.locationItem}>
{content}
</a>
);
}
return (
<div key={p} className={styles.locationItem}>
{content}
</div>
);
}
return <span key={p}>{p}</span>;
})}
</div>
)}
@@ -115,9 +115,6 @@ describe('Unified Storage Searcher', () => {
.mockResolvedValueOnce(mockResults)
.mockResolvedValueOnce(mockFolders);
const consoleWarn = jest.fn();
jest.spyOn(console, 'warn').mockImplementationOnce(consoleWarn);
const query: SearchQuery = {
query: 'test',
limit: 50,
@@ -127,14 +124,15 @@ describe('Unified Storage Searcher', () => {
const response = await searcher.search(query);
expect(response.view.length).toBe(1);
expect(response.view.get(0).title).toBe('DB 2');
expect(response.view.length).toBe(2);
expect(response.view.get(0).title).toBe('DB 1');
expect(response.view.get(0).folder).toBe('sharedwithme');
expect(response.view.get(1).title).toBe('DB 2');
const df = response.view.dataFrame;
const locationInfo = df.meta?.custom?.locationInfo;
expect(locationInfo).toBeDefined();
expect(locationInfo?.folder2.name).toBe('Folder 2');
expect(consoleWarn).toHaveBeenCalled();
expect(mockSearcher.search).toHaveBeenCalledTimes(3);
});
+16 -7
View File
@@ -204,15 +204,19 @@ export class UnifiedSearcher implements GrafanaSearcher {
if (!hasMissing) {
return rsp;
}
// we still have results here with folders we can't find
// filter the results since we probably don't have access to that folder
const locationInfo = await this.locationInfo;
const hits = rsp.hits.filter((hit) => {
if (hit.folder === undefined || locationInfo[hit.folder] !== undefined) {
return true;
const hits = rsp.hits.map((hit) => {
if (hit.folder === undefined) {
return { ...hit, location: 'general', folder: 'general' };
}
console.warn('Dropping search hit with missing folder', hit);
return false;
// this means user has permission to see this dashboard, but not the folder contents
if (locationInfo[hit.folder] === undefined) {
return { ...hit, location: 'sharedwithme', folder: 'sharedwithme' };
}
return hit;
});
const totalHits = rsp.totalHits - (rsp.hits.length - hits.length);
return { ...rsp, hits, totalHits };
@@ -370,6 +374,11 @@ async function loadLocationInfo(): Promise<Record<string, LocationInfo>> {
name: 'Dashboards',
url: '/dashboards',
}, // share location info with everyone
sharedwithme: {
kind: 'sharedwithme',
name: 'Shared with me',
url: '',
},
};
for (const hit of rsp.hits) {
locationInfo[hit.name] = {
@@ -49,6 +49,10 @@ export function getIconForKind(kind: string, isOpen?: boolean): IconName {
return isOpen ? 'folder-open' : 'folder';
}
if (kind === 'sharedwithme') {
return 'users-alt';
}
return 'question-circle';
}
+3 -8
View File
@@ -650,10 +650,9 @@ function getVariableSet(
addFilterButtonText: 'Select resource attributes',
datasource: trailDS,
hide: VariableHide.hideVariable,
layout: 'vertical',
layout: 'combobox',
defaultKeys: [],
applyMode: 'manual',
supportsMultiValueOperators: true,
allowCustomValue: true,
}),
new AdHocFiltersVariable({
@@ -662,12 +661,10 @@ function getVariableSet(
datasource: trailDS,
// default to use var filters and have otel off
hide: VariableHide.hideLabel,
layout: 'vertical',
layout: 'combobox',
filters: initialFilters ?? [],
baseFilters: getBaseFiltersForMetric(metric),
applyMode: 'manual',
// since we only support prometheus datasources, this is always true
supportsMultiValueOperators: true,
allowCustomValue: true,
expressionBuilder: (filters: AdHocVariableFilter[]) => {
return [...getBaseFiltersForMetric(metric), ...filters]
@@ -691,12 +688,10 @@ function getVariableSet(
addFilterButtonText: 'Filter',
datasource: trailDS,
hide: VariableHide.hideVariable,
layout: 'vertical',
layout: 'combobox',
filters: initialFilters ?? [],
baseFilters: getBaseFiltersForMetric(metric),
applyMode: 'manual',
// since we only support prometheus datasources, this is always true
supportsMultiValueOperators: true,
allowCustomValue: true,
// skipUrlSync: true
}),
+354 -68
View File
@@ -34,12 +34,6 @@
"save-button": ""
}
},
"actions-editor": {
"inline": {
"add-button": "",
"one-click-link": ""
}
},
"admin": {
"anon-users": {
"not-found": ""
@@ -174,6 +168,24 @@
}
},
"alerting": {
"alert": {
"alert-state": "",
"annotations": "",
"evaluation": "",
"evaluation-paused": "",
"evaluation-paused-description": "",
"last-evaluated": "",
"last-evaluation-duration": "",
"last-updated-at": "",
"last-updated-by": "",
"no-annotations": "",
"pending-period": "",
"rule": "",
"rule-identifier": "",
"rule-type": "",
"state-error-timeout": "",
"state-no-data": ""
},
"alert-recording-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -283,6 +295,7 @@
"contactPointFilter": {
"label": ""
},
"copy-to-clipboard": "",
"export": {
"subtitle": {
"formats": "",
@@ -299,6 +312,13 @@
}
}
},
"group-actions": {
"actions-trigger": "",
"delete": "",
"edit": "",
"export": "",
"reorder": ""
},
"list-view": {
"empty": {
"new-alert-rule": "",
@@ -484,11 +504,23 @@
},
"rule-list": {
"configure-datasource": "",
"ds-error-boundary": {
"description": "",
"title": ""
},
"filter-view": {
"no-more-results": "",
"no-rules-found": ""
},
"new-alert-rule": ""
"new-alert-rule": "",
"pagination": {
"next-page": "",
"previous-page": ""
},
"return-button": {
"title": ""
},
"rulerrule-loading-error": ""
},
"rule-state": {
"creating": "",
@@ -690,7 +722,10 @@
"title": ""
},
"custom-value": {
"label": ""
"description": ""
},
"group": {
"undefined": ""
},
"options": {
"no-found": ""
@@ -863,6 +898,82 @@
"redirect-link": "Liste in Grafana Alerting",
"subtitle": "Benachrichtigungsregeln im Zusammenhang mit diesem Dashboard"
},
"default-layout": {
"description": "",
"item-options": {
"repeat": {
"direction": {
"horizontal": "",
"title": "",
"vertical": ""
},
"max": "",
"title": "",
"variable": {
"description": "",
"title": ""
}
}
},
"name": "",
"row-actions": {
"delete": "",
"modal": {
"alt-action": "",
"text": "",
"title": ""
}
},
"row-options": {
"button": {
"label": ""
},
"form": {
"cancel": "",
"repeat-for": {
"label": "",
"learn-more": "",
"warning": {
"text": ""
}
},
"title": "",
"update": ""
},
"modal": {
"title": ""
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": ""
}
},
"open": "",
"panels": {
"multi-select": {
"selection-number": ""
}
},
"row": {
"header": {
"hide": "",
"title": ""
},
"multi-select": {
"options-header": "",
"selection-number": ""
}
},
"tab": {
"multi-select": {
"options-header": "",
"selection-number": ""
}
}
},
"empty": {
"add-library-panel-body": "Visualisierungen hinzufügen, die mit anderen Dashboards geteilt werden.",
"add-library-panel-button": "Bibliotheksfenster hinzufügen",
@@ -874,6 +985,9 @@
"import-a-dashboard-header": "Dashboard importieren",
"import-dashboard-button": "Dashboard importieren"
},
"errors": {
"failed-to-load": ""
},
"inspect": {
"data-tab": "Daten",
"error-tab": "Fehler",
@@ -926,19 +1040,130 @@
"rows": "Gesamtanzahl an Zeilen",
"table-title": "Statistiken"
},
"options": {
"description": "",
"title": "",
"title-option": ""
},
"panel-edit": {
"alerting-tab": {
"dashboard-not-saved": "",
"no-rules": ""
}
},
"responsive-layout": {
"description": "",
"item-options": {
"hide-no-data": "",
"title": ""
},
"name": "",
"options": {
"columns": "",
"fixed": "",
"min": "",
"one-column": "",
"rows": "",
"three-columns": "",
"two-columns": ""
}
},
"rows-layout": {
"description": "",
"name": "",
"row": {
"collapse": "",
"expand": "",
"new": "",
"repeat": {
"learn-more": "",
"warning": ""
}
},
"row-options": {
"height": {
"expand": "",
"hide-row-header": "",
"min": "",
"title": ""
},
"repeat": {
"title": "",
"variable": {
"title": ""
}
},
"title": "",
"title-option": ""
}
},
"tabs-layout": {
"description": "",
"name": "",
"tab": {
"new": ""
},
"tab-options": {
"title": "",
"title-option": ""
}
},
"toolbar": {
"add": "Hinzufügen",
"add-panel": "",
"add-panel-lib": "",
"add-row": "",
"add-tab": "",
"alert-rules": "Warnregeln",
"back-to-dashboard": "",
"dashboard-settings": {
"label": "",
"tooltip": ""
},
"discard-library-panel-changes": "",
"discard-panel": "",
"discard-panel-new": "",
"edit": {
"label": "",
"tooltip": ""
},
"edit-dashboard-v2-schema": "",
"enter-edit-mode": {
"label": "",
"tooltip": ""
},
"exit-edit-mode": {
"label": "",
"tooltip": ""
},
"mark-favorite": "Als Favorit markieren",
"more-save-options": "",
"open-original": "Original-Dashboard öffnen",
"playlist-next": "Zum nächsten Dashboard",
"playlist-previous": "Zum vorherigen Dashboard",
"playlist-stop": "Wiedergabeliste stoppen",
"public-dashboard": "",
"refresh": "Dashboard aktualisieren",
"save": "Dashboard speichern",
"save-dashboard": {
"label": "",
"tooltip": ""
},
"save-dashboard-copy": {
"label": "",
"tooltip": ""
},
"save-dashboard-short": "",
"save-library-panel": "",
"settings": "Dashboard-Einstellungen",
"share": "Dashboard oder Panel teilen",
"share": {
"label": "",
"tooltip": ""
},
"share-button": "Teilen",
"show-hidden-elements": "",
"switch-old-dashboard": "",
"unlink-library-panel": "",
"unmark-favorite": "Markierung als Favorit entfernen"
},
"validation": {
@@ -946,6 +1171,14 @@
"invalid-json": "JSON ungültig",
"tags-expected-array": "Array der jeweiligen Tags",
"tags-expected-strings": "String-Array der jeweiligen Tags"
},
"viz-panel": {
"options": {
"description": "",
"title": "",
"title-option": "",
"transparent-background": ""
}
}
},
"dashboard-import": {
@@ -1048,6 +1281,12 @@
"angular-deprecation-description": "",
"angular-deprecation-heading": ""
},
"panel-queries": {
"add-query-from-library": ""
},
"query-library": {
"add-query-button": ""
},
"settings": {
"variables": {
"dependencies": {
@@ -1125,40 +1364,11 @@
"scan-for-older-logs": "",
"stop-scan": ""
},
"query-library": {
"add-edit-description": "",
"cancel": "",
"default-description": "",
"delete-query": "",
"delete-query-text": "",
"delete-query-title": "",
"private": "",
"public": "",
"query-deleted": "",
"query-template-add-error": "",
"query-template-added": "",
"query-template-edit-error": "",
"query-template-edited": "",
"save": ""
},
"query-template-modal": {
"add-info": "",
"add-title": "",
"auto-star": "",
"data-source-name": "",
"description": "",
"edit-info": "",
"edit-title": "",
"query": "",
"visibility": ""
},
"query-template-modall": {
"data-source-type": ""
},
"rich-history": {
"close-tooltip": "Abfrageverlauf schließen",
"datasource-a-z": "Datenquelle A-Z",
"datasource-z-a": "Datenquelle Z-A",
"library-history-dropdown": "",
"newest-first": "Neueste zuerst",
"oldest-first": "Älteste zuerst",
"query-history": "Abfrageverlauf",
@@ -1305,10 +1515,9 @@
"sortBy": ""
},
"related-logs": {
"docsLink": "",
"LrrDocsLink": "",
"openExploreLogs": "",
"relatedLogsUnavailableAfterDocsLink": "",
"relatedLogsUnavailableBeforeDocsLink": "",
"relatedLogsUnavailable": "",
"warnExperimentalFeature": ""
},
"viewBy": ""
@@ -1357,6 +1566,24 @@
"send-custom-feedback": ""
},
"grafana-ui": {
"action-editor": {
"button": {
"confirm": "",
"confirm-action": ""
},
"inline": {
"add-action": "",
"edit-action": ""
},
"modal": {
"action-body": "",
"action-method": "",
"action-query-params": "",
"action-title": "",
"action-title-placeholder": "",
"one-click-description": ""
}
},
"auto-save-field": {
"saved": "",
"saving": ""
@@ -1373,11 +1600,21 @@
},
"data-link-editor-modal": {
"cancel": "",
"one-click-description": "",
"save": ""
},
"data-link-inline-editor": {
"one-click": ""
},
"data-links-inline-editor": {
"add-link": "",
"one-click-link": ""
"edit-link": "",
"one-click": "",
"one-click-enabled": "",
"title-not-provided": "",
"tooltip-edit": "",
"tooltip-remove": "",
"url-not-provided": ""
},
"data-source-http-settings": {
"access-help": "",
@@ -1458,7 +1695,12 @@
"right-axis-indicator": ""
},
"viz-tooltip": {
"footer-add-annotation": ""
"actions-confirmation-input-placeholder": "",
"actions-confirmation-label": "",
"actions-confirmation-message": "",
"footer-add-annotation": "",
"footer-click-to-action": "",
"footer-click-to-navigate": ""
}
},
"graph": {
@@ -1780,7 +2022,8 @@
},
"log-row-message": {
"ellipsis": "",
"more": ""
"more": "",
"see-details": ""
},
"log-rows": {
"disable-popover": {
@@ -1792,6 +2035,13 @@
"shortcut": ""
}
},
"logs-navigation": {
"newer-logs": "",
"older-logs": "",
"scroll-bottom": "",
"scroll-top": "",
"start-of-range": ""
},
"popover-menu": {
"copy": "",
"disable-menu": "",
@@ -1881,6 +2131,7 @@
"title": ""
},
"migrated-counts": {
"alert_rule_groups": "",
"alert_rules": "",
"contact_points": "",
"dashboards": "",
@@ -1941,9 +2192,8 @@
"public-preview": {
"button-text": "",
"message": "",
"message-plugins": "",
"title": "",
"title-plugins": ""
"message-cloud": "",
"title": ""
},
"resource-details": {
"dismiss-button": "",
@@ -1986,6 +2236,7 @@
},
"resource-type": {
"alert_rule": "",
"alert_rule_group": "",
"contact_point": "",
"dashboard": "Dashboard",
"datasource": "Datenquelle",
@@ -2031,6 +2282,15 @@
"title": "Warum mit Grafana hosten?"
}
},
"multicombobox": {
"all": {
"title": "",
"title-filtered": ""
},
"clear": {
"title": ""
}
},
"nav": {
"add-new-connections": {
"title": "Neue Verbindung hinzufügen"
@@ -2384,7 +2644,8 @@
"list-label": "Navigation",
"open": "",
"undock": "Menü abdocken"
}
},
"rss-button": ""
},
"news": {
"drawer": {
@@ -2516,19 +2777,35 @@
}
},
"details": {
"connections-tab": {
"description": ""
},
"labels": {
"contactGrafanaLabs": "",
"customLinks": "",
"customLinksTooltip": "",
"dependencies": "",
"documentation": "",
"downloads": "",
"from": "",
"installedVersion": "",
"lastCommitDate": "",
"latestVersion": "",
"links": "",
"license": "",
"raiseAnIssue": "",
"reportAbuse": "",
"reportAbuseTooltip": "",
"repository": "",
"signature": "",
"status": "",
"updatedAt": ""
},
"modal": {
"cancel": "",
"copyEmail": "",
"description": "",
"node": "",
"title": ""
}
},
"empty-state": {
@@ -2743,14 +3020,6 @@
"role-label": "Rolle"
}
},
"query-library": {
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
"search": "",
"user-info-get-error": "",
"user-names": ""
},
"query-operation": {
"header": {
"collapse-row": "Abfragezeile einklappen",
@@ -2760,7 +3029,6 @@
"expand-row": "Suchzeile erweitern",
"hide-response": "",
"remove-query": "Abfrage entfernen",
"save-to-query-library": "",
"show-response": "",
"toggle-edit-mode": "Textbearbeitungsmodus umschalten"
},
@@ -2862,6 +3130,10 @@
"title": ""
},
"save-dashboards": {
"message-length": {
"info": "",
"title": ""
},
"name-exists": {
"message-info": "",
"message-suggestion": "",
@@ -3145,6 +3417,7 @@
"home-dashboard-placeholder": "Standard-Dashboard",
"locale-label": "Sprache",
"locale-placeholder": "Sprache wählen",
"theme-description": "",
"theme-label": "UI-Design",
"week-start-label": "Wochenbeginn"
},
@@ -3216,6 +3489,13 @@
"url-column-header": "Snapshot url",
"view-button": "Anzeigen"
},
"table": {
"container": {
"content": "",
"show-all-series": "",
"show-only-series": ""
}
},
"tag-filter": {
"clear-button": "",
"loading": "Wird geladen ...",
@@ -3309,20 +3589,26 @@
"start-your-metrics-exploration": "",
"subtitle": ""
},
"metric-overview": {
"description-label": "",
"labels": "",
"metric-attributes": "",
"no-description": "",
"type-label": "",
"unit-label": "",
"unknown-type": ""
},
"metric-select": {
"filter-by": "",
"native-histogram": "",
"new-badge": "",
"otel-switch": ""
},
"native-histogram-banner": {
"ch-heatmap": "",
"ch-histogram": "",
"click-histogram": "",
"hide-examples": "",
"learn-more": "",
"metric-examples": "",
"nh-heatmap": "",
"nh-histogram": "",
"now": "",
"previously": "",
"see-examples": "",
"sentence": ""
},
"recent-metrics": {
"or-view-a-recent-exploration": ""
},
+354 -68
View File
@@ -34,12 +34,6 @@
"save-button": ""
}
},
"actions-editor": {
"inline": {
"add-button": "",
"one-click-link": ""
}
},
"admin": {
"anon-users": {
"not-found": ""
@@ -174,6 +168,24 @@
}
},
"alerting": {
"alert": {
"alert-state": "",
"annotations": "",
"evaluation": "",
"evaluation-paused": "",
"evaluation-paused-description": "",
"last-evaluated": "",
"last-evaluation-duration": "",
"last-updated-at": "",
"last-updated-by": "",
"no-annotations": "",
"pending-period": "",
"rule": "",
"rule-identifier": "",
"rule-type": "",
"state-error-timeout": "",
"state-no-data": ""
},
"alert-recording-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -283,6 +295,7 @@
"contactPointFilter": {
"label": ""
},
"copy-to-clipboard": "",
"export": {
"subtitle": {
"formats": "",
@@ -299,6 +312,13 @@
}
}
},
"group-actions": {
"actions-trigger": "",
"delete": "",
"edit": "",
"export": "",
"reorder": ""
},
"list-view": {
"empty": {
"new-alert-rule": "",
@@ -484,11 +504,23 @@
},
"rule-list": {
"configure-datasource": "",
"ds-error-boundary": {
"description": "",
"title": ""
},
"filter-view": {
"no-more-results": "",
"no-rules-found": ""
},
"new-alert-rule": ""
"new-alert-rule": "",
"pagination": {
"next-page": "",
"previous-page": ""
},
"return-button": {
"title": ""
},
"rulerrule-loading-error": ""
},
"rule-state": {
"creating": "",
@@ -690,7 +722,10 @@
"title": ""
},
"custom-value": {
"label": ""
"description": ""
},
"group": {
"undefined": ""
},
"options": {
"no-found": ""
@@ -863,6 +898,82 @@
"redirect-link": "Lista en Alertas de Grafana",
"subtitle": "Reglas de alerta relacionadas con este tablero"
},
"default-layout": {
"description": "",
"item-options": {
"repeat": {
"direction": {
"horizontal": "",
"title": "",
"vertical": ""
},
"max": "",
"title": "",
"variable": {
"description": "",
"title": ""
}
}
},
"name": "",
"row-actions": {
"delete": "",
"modal": {
"alt-action": "",
"text": "",
"title": ""
}
},
"row-options": {
"button": {
"label": ""
},
"form": {
"cancel": "",
"repeat-for": {
"label": "",
"learn-more": "",
"warning": {
"text": ""
}
},
"title": "",
"update": ""
},
"modal": {
"title": ""
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": ""
}
},
"open": "",
"panels": {
"multi-select": {
"selection-number": ""
}
},
"row": {
"header": {
"hide": "",
"title": ""
},
"multi-select": {
"options-header": "",
"selection-number": ""
}
},
"tab": {
"multi-select": {
"options-header": "",
"selection-number": ""
}
}
},
"empty": {
"add-library-panel-body": "Añadir las visualizaciones que se comparten con otros tableros.",
"add-library-panel-button": "Añadir panel de biblioteca",
@@ -874,6 +985,9 @@
"import-a-dashboard-header": "Importar un tablero",
"import-dashboard-button": "Importar panel de control"
},
"errors": {
"failed-to-load": ""
},
"inspect": {
"data-tab": "Datos",
"error-tab": "Error",
@@ -926,19 +1040,130 @@
"rows": "Número total de filas",
"table-title": "Estadísticas"
},
"options": {
"description": "",
"title": "",
"title-option": ""
},
"panel-edit": {
"alerting-tab": {
"dashboard-not-saved": "",
"no-rules": ""
}
},
"responsive-layout": {
"description": "",
"item-options": {
"hide-no-data": "",
"title": ""
},
"name": "",
"options": {
"columns": "",
"fixed": "",
"min": "",
"one-column": "",
"rows": "",
"three-columns": "",
"two-columns": ""
}
},
"rows-layout": {
"description": "",
"name": "",
"row": {
"collapse": "",
"expand": "",
"new": "",
"repeat": {
"learn-more": "",
"warning": ""
}
},
"row-options": {
"height": {
"expand": "",
"hide-row-header": "",
"min": "",
"title": ""
},
"repeat": {
"title": "",
"variable": {
"title": ""
}
},
"title": "",
"title-option": ""
}
},
"tabs-layout": {
"description": "",
"name": "",
"tab": {
"new": ""
},
"tab-options": {
"title": "",
"title-option": ""
}
},
"toolbar": {
"add": "Añadir",
"add-panel": "",
"add-panel-lib": "",
"add-row": "",
"add-tab": "",
"alert-rules": "Reglas de alerta",
"back-to-dashboard": "",
"dashboard-settings": {
"label": "",
"tooltip": ""
},
"discard-library-panel-changes": "",
"discard-panel": "",
"discard-panel-new": "",
"edit": {
"label": "",
"tooltip": ""
},
"edit-dashboard-v2-schema": "",
"enter-edit-mode": {
"label": "",
"tooltip": ""
},
"exit-edit-mode": {
"label": "",
"tooltip": ""
},
"mark-favorite": "Marcar como favorito",
"more-save-options": "",
"open-original": "Abrir el panel de control original",
"playlist-next": "Ir al siguiente panel de control",
"playlist-previous": "Ir al panel de control anterior",
"playlist-stop": "Detener la lista de reproducción",
"public-dashboard": "",
"refresh": "Actualizar panel de control",
"save": "Guardar panel de control",
"save-dashboard": {
"label": "",
"tooltip": ""
},
"save-dashboard-copy": {
"label": "",
"tooltip": ""
},
"save-dashboard-short": "",
"save-library-panel": "",
"settings": "Ajustes del panel de control",
"share": "Compartir panel o panel de control",
"share": {
"label": "",
"tooltip": ""
},
"share-button": "Compartir",
"show-hidden-elements": "",
"switch-old-dashboard": "",
"unlink-library-panel": "",
"unmark-favorite": "Deshacer marca como favorito"
},
"validation": {
@@ -946,6 +1171,14 @@
"invalid-json": "JSON no válido",
"tags-expected-array": "etiquetas: matriz prevista",
"tags-expected-strings": "etiquetas: matriz de cadenas prevista"
},
"viz-panel": {
"options": {
"description": "",
"title": "",
"title-option": "",
"transparent-background": ""
}
}
},
"dashboard-import": {
@@ -1048,6 +1281,12 @@
"angular-deprecation-description": "",
"angular-deprecation-heading": ""
},
"panel-queries": {
"add-query-from-library": ""
},
"query-library": {
"add-query-button": ""
},
"settings": {
"variables": {
"dependencies": {
@@ -1125,40 +1364,11 @@
"scan-for-older-logs": "",
"stop-scan": ""
},
"query-library": {
"add-edit-description": "",
"cancel": "",
"default-description": "",
"delete-query": "",
"delete-query-text": "",
"delete-query-title": "",
"private": "",
"public": "",
"query-deleted": "",
"query-template-add-error": "",
"query-template-added": "",
"query-template-edit-error": "",
"query-template-edited": "",
"save": ""
},
"query-template-modal": {
"add-info": "",
"add-title": "",
"auto-star": "",
"data-source-name": "",
"description": "",
"edit-info": "",
"edit-title": "",
"query": "",
"visibility": ""
},
"query-template-modall": {
"data-source-type": ""
},
"rich-history": {
"close-tooltip": "Cerrar el historial de consultas",
"datasource-a-z": "Fuente de datos A-Z",
"datasource-z-a": "Fuente de datos Z-A",
"library-history-dropdown": "",
"newest-first": "El más reciente primero",
"oldest-first": "El más antiguo primero",
"query-history": "Historial de consultas",
@@ -1305,10 +1515,9 @@
"sortBy": ""
},
"related-logs": {
"docsLink": "",
"LrrDocsLink": "",
"openExploreLogs": "",
"relatedLogsUnavailableAfterDocsLink": "",
"relatedLogsUnavailableBeforeDocsLink": "",
"relatedLogsUnavailable": "",
"warnExperimentalFeature": ""
},
"viewBy": ""
@@ -1357,6 +1566,24 @@
"send-custom-feedback": ""
},
"grafana-ui": {
"action-editor": {
"button": {
"confirm": "",
"confirm-action": ""
},
"inline": {
"add-action": "",
"edit-action": ""
},
"modal": {
"action-body": "",
"action-method": "",
"action-query-params": "",
"action-title": "",
"action-title-placeholder": "",
"one-click-description": ""
}
},
"auto-save-field": {
"saved": "",
"saving": ""
@@ -1373,11 +1600,21 @@
},
"data-link-editor-modal": {
"cancel": "",
"one-click-description": "",
"save": ""
},
"data-link-inline-editor": {
"one-click": ""
},
"data-links-inline-editor": {
"add-link": "",
"one-click-link": ""
"edit-link": "",
"one-click": "",
"one-click-enabled": "",
"title-not-provided": "",
"tooltip-edit": "",
"tooltip-remove": "",
"url-not-provided": ""
},
"data-source-http-settings": {
"access-help": "",
@@ -1458,7 +1695,12 @@
"right-axis-indicator": ""
},
"viz-tooltip": {
"footer-add-annotation": ""
"actions-confirmation-input-placeholder": "",
"actions-confirmation-label": "",
"actions-confirmation-message": "",
"footer-add-annotation": "",
"footer-click-to-action": "",
"footer-click-to-navigate": ""
}
},
"graph": {
@@ -1780,7 +2022,8 @@
},
"log-row-message": {
"ellipsis": "",
"more": ""
"more": "",
"see-details": ""
},
"log-rows": {
"disable-popover": {
@@ -1792,6 +2035,13 @@
"shortcut": ""
}
},
"logs-navigation": {
"newer-logs": "",
"older-logs": "",
"scroll-bottom": "",
"scroll-top": "",
"start-of-range": ""
},
"popover-menu": {
"copy": "",
"disable-menu": "",
@@ -1881,6 +2131,7 @@
"title": ""
},
"migrated-counts": {
"alert_rule_groups": "",
"alert_rules": "",
"contact_points": "",
"dashboards": "",
@@ -1941,9 +2192,8 @@
"public-preview": {
"button-text": "",
"message": "",
"message-plugins": "",
"title": "",
"title-plugins": ""
"message-cloud": "",
"title": ""
},
"resource-details": {
"dismiss-button": "",
@@ -1986,6 +2236,7 @@
},
"resource-type": {
"alert_rule": "",
"alert_rule_group": "",
"contact_point": "",
"dashboard": "Panel de control",
"datasource": "Fuente de datos",
@@ -2031,6 +2282,15 @@
"title": "¿Por qué alojar con Grafana?"
}
},
"multicombobox": {
"all": {
"title": "",
"title-filtered": ""
},
"clear": {
"title": ""
}
},
"nav": {
"add-new-connections": {
"title": "Añadir nueva conexión"
@@ -2384,7 +2644,8 @@
"list-label": "Navegación",
"open": "",
"undock": "Desanclar el menú"
}
},
"rss-button": ""
},
"news": {
"drawer": {
@@ -2516,19 +2777,35 @@
}
},
"details": {
"connections-tab": {
"description": ""
},
"labels": {
"contactGrafanaLabs": "",
"customLinks": "",
"customLinksTooltip": "",
"dependencies": "",
"documentation": "",
"downloads": "",
"from": "",
"installedVersion": "",
"lastCommitDate": "",
"latestVersion": "",
"links": "",
"license": "",
"raiseAnIssue": "",
"reportAbuse": "",
"reportAbuseTooltip": "",
"repository": "",
"signature": "",
"status": "",
"updatedAt": ""
},
"modal": {
"cancel": "",
"copyEmail": "",
"description": "",
"node": "",
"title": ""
}
},
"empty-state": {
@@ -2743,14 +3020,6 @@
"role-label": "Rol"
}
},
"query-library": {
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
"search": "",
"user-info-get-error": "",
"user-names": ""
},
"query-operation": {
"header": {
"collapse-row": "Contraer la fila de la consulta",
@@ -2760,7 +3029,6 @@
"expand-row": "Expandir la fila de la consulta",
"hide-response": "",
"remove-query": "Eliminar consulta",
"save-to-query-library": "",
"show-response": "",
"toggle-edit-mode": "Alternar el modo de edición de texto"
},
@@ -2862,6 +3130,10 @@
"title": ""
},
"save-dashboards": {
"message-length": {
"info": "",
"title": ""
},
"name-exists": {
"message-info": "",
"message-suggestion": "",
@@ -3145,6 +3417,7 @@
"home-dashboard-placeholder": "Panel de control por defecto",
"locale-label": "Idioma",
"locale-placeholder": "Cambiar idioma",
"theme-description": "",
"theme-label": "Tema de interfaz de usuario",
"week-start-label": "Inicio de la semana"
},
@@ -3216,6 +3489,13 @@
"url-column-header": "URL de la instantánea",
"view-button": "Vista"
},
"table": {
"container": {
"content": "",
"show-all-series": "",
"show-only-series": ""
}
},
"tag-filter": {
"clear-button": "",
"loading": "Cargando...",
@@ -3309,20 +3589,26 @@
"start-your-metrics-exploration": "",
"subtitle": ""
},
"metric-overview": {
"description-label": "",
"labels": "",
"metric-attributes": "",
"no-description": "",
"type-label": "",
"unit-label": "",
"unknown-type": ""
},
"metric-select": {
"filter-by": "",
"native-histogram": "",
"new-badge": "",
"otel-switch": ""
},
"native-histogram-banner": {
"ch-heatmap": "",
"ch-histogram": "",
"click-histogram": "",
"hide-examples": "",
"learn-more": "",
"metric-examples": "",
"nh-heatmap": "",
"nh-histogram": "",
"now": "",
"previously": "",
"see-examples": "",
"sentence": ""
},
"recent-metrics": {
"or-view-a-recent-exploration": ""
},
+354 -68
View File
@@ -34,12 +34,6 @@
"save-button": ""
}
},
"actions-editor": {
"inline": {
"add-button": "",
"one-click-link": ""
}
},
"admin": {
"anon-users": {
"not-found": ""
@@ -174,6 +168,24 @@
}
},
"alerting": {
"alert": {
"alert-state": "",
"annotations": "",
"evaluation": "",
"evaluation-paused": "",
"evaluation-paused-description": "",
"last-evaluated": "",
"last-evaluation-duration": "",
"last-updated-at": "",
"last-updated-by": "",
"no-annotations": "",
"pending-period": "",
"rule": "",
"rule-identifier": "",
"rule-type": "",
"state-error-timeout": "",
"state-no-data": ""
},
"alert-recording-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -283,6 +295,7 @@
"contactPointFilter": {
"label": ""
},
"copy-to-clipboard": "",
"export": {
"subtitle": {
"formats": "",
@@ -299,6 +312,13 @@
}
}
},
"group-actions": {
"actions-trigger": "",
"delete": "",
"edit": "",
"export": "",
"reorder": ""
},
"list-view": {
"empty": {
"new-alert-rule": "",
@@ -484,11 +504,23 @@
},
"rule-list": {
"configure-datasource": "",
"ds-error-boundary": {
"description": "",
"title": ""
},
"filter-view": {
"no-more-results": "",
"no-rules-found": ""
},
"new-alert-rule": ""
"new-alert-rule": "",
"pagination": {
"next-page": "",
"previous-page": ""
},
"return-button": {
"title": ""
},
"rulerrule-loading-error": ""
},
"rule-state": {
"creating": "",
@@ -690,7 +722,10 @@
"title": ""
},
"custom-value": {
"label": ""
"description": ""
},
"group": {
"undefined": ""
},
"options": {
"no-found": ""
@@ -863,6 +898,82 @@
"redirect-link": "Liste dans Alertes Grafana",
"subtitle": "Règles d'alerte liées à ce tableau de bord"
},
"default-layout": {
"description": "",
"item-options": {
"repeat": {
"direction": {
"horizontal": "",
"title": "",
"vertical": ""
},
"max": "",
"title": "",
"variable": {
"description": "",
"title": ""
}
}
},
"name": "",
"row-actions": {
"delete": "",
"modal": {
"alt-action": "",
"text": "",
"title": ""
}
},
"row-options": {
"button": {
"label": ""
},
"form": {
"cancel": "",
"repeat-for": {
"label": "",
"learn-more": "",
"warning": {
"text": ""
}
},
"title": "",
"update": ""
},
"modal": {
"title": ""
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": ""
}
},
"open": "",
"panels": {
"multi-select": {
"selection-number": ""
}
},
"row": {
"header": {
"hide": "",
"title": ""
},
"multi-select": {
"options-header": "",
"selection-number": ""
}
},
"tab": {
"multi-select": {
"options-header": "",
"selection-number": ""
}
}
},
"empty": {
"add-library-panel-body": "Ajoutez des visualisations partagées avec d'autres tableaux de bord.",
"add-library-panel-button": "Ajouter un panneau Bibliothèque",
@@ -874,6 +985,9 @@
"import-a-dashboard-header": "Importer un tableau de bord",
"import-dashboard-button": "Importer un tableau de bord"
},
"errors": {
"failed-to-load": ""
},
"inspect": {
"data-tab": "Données",
"error-tab": "Erreur",
@@ -926,19 +1040,130 @@
"rows": "Nombre total de lignes",
"table-title": "Statistiques"
},
"options": {
"description": "",
"title": "",
"title-option": ""
},
"panel-edit": {
"alerting-tab": {
"dashboard-not-saved": "",
"no-rules": ""
}
},
"responsive-layout": {
"description": "",
"item-options": {
"hide-no-data": "",
"title": ""
},
"name": "",
"options": {
"columns": "",
"fixed": "",
"min": "",
"one-column": "",
"rows": "",
"three-columns": "",
"two-columns": ""
}
},
"rows-layout": {
"description": "",
"name": "",
"row": {
"collapse": "",
"expand": "",
"new": "",
"repeat": {
"learn-more": "",
"warning": ""
}
},
"row-options": {
"height": {
"expand": "",
"hide-row-header": "",
"min": "",
"title": ""
},
"repeat": {
"title": "",
"variable": {
"title": ""
}
},
"title": "",
"title-option": ""
}
},
"tabs-layout": {
"description": "",
"name": "",
"tab": {
"new": ""
},
"tab-options": {
"title": "",
"title-option": ""
}
},
"toolbar": {
"add": "Ajouter",
"add-panel": "",
"add-panel-lib": "",
"add-row": "",
"add-tab": "",
"alert-rules": "Règles d'alerte",
"back-to-dashboard": "",
"dashboard-settings": {
"label": "",
"tooltip": ""
},
"discard-library-panel-changes": "",
"discard-panel": "",
"discard-panel-new": "",
"edit": {
"label": "",
"tooltip": ""
},
"edit-dashboard-v2-schema": "",
"enter-edit-mode": {
"label": "",
"tooltip": ""
},
"exit-edit-mode": {
"label": "",
"tooltip": ""
},
"mark-favorite": "Marquer comme favori",
"more-save-options": "",
"open-original": "Ouvrir le tableau de bord d'origine",
"playlist-next": "Accéder au tableau de bord suivant",
"playlist-previous": "Accéder au tableau de bord précédent",
"playlist-stop": "Arrêter la liste de lecture",
"public-dashboard": "",
"refresh": "Actualiser le tableau de bord",
"save": "Enregistrer le tableau de bord",
"save-dashboard": {
"label": "",
"tooltip": ""
},
"save-dashboard-copy": {
"label": "",
"tooltip": ""
},
"save-dashboard-short": "",
"save-library-panel": "",
"settings": "Paramètres du tableau de bord",
"share": "Partager le tableau de bord ou le panneau",
"share": {
"label": "",
"tooltip": ""
},
"share-button": "Partager",
"show-hidden-elements": "",
"switch-old-dashboard": "",
"unlink-library-panel": "",
"unmark-favorite": "Supprimer des favoris"
},
"validation": {
@@ -946,6 +1171,14 @@
"invalid-json": "JSON non valide",
"tags-expected-array": "étiquettes attendues tableau",
"tags-expected-strings": "étiquettes attendues tableau de chaînes"
},
"viz-panel": {
"options": {
"description": "",
"title": "",
"title-option": "",
"transparent-background": ""
}
}
},
"dashboard-import": {
@@ -1048,6 +1281,12 @@
"angular-deprecation-description": "",
"angular-deprecation-heading": ""
},
"panel-queries": {
"add-query-from-library": ""
},
"query-library": {
"add-query-button": ""
},
"settings": {
"variables": {
"dependencies": {
@@ -1125,40 +1364,11 @@
"scan-for-older-logs": "",
"stop-scan": ""
},
"query-library": {
"add-edit-description": "",
"cancel": "",
"default-description": "",
"delete-query": "",
"delete-query-text": "",
"delete-query-title": "",
"private": "",
"public": "",
"query-deleted": "",
"query-template-add-error": "",
"query-template-added": "",
"query-template-edit-error": "",
"query-template-edited": "",
"save": ""
},
"query-template-modal": {
"add-info": "",
"add-title": "",
"auto-star": "",
"data-source-name": "",
"description": "",
"edit-info": "",
"edit-title": "",
"query": "",
"visibility": ""
},
"query-template-modall": {
"data-source-type": ""
},
"rich-history": {
"close-tooltip": "Fermer l'historique des requêtes",
"datasource-a-z": "Source de données A-Z",
"datasource-z-a": "Source de données Z-A",
"library-history-dropdown": "",
"newest-first": "Plus récent en premier",
"oldest-first": "Plus ancien en premier",
"query-history": "Historique des requêtes",
@@ -1305,10 +1515,9 @@
"sortBy": ""
},
"related-logs": {
"docsLink": "",
"LrrDocsLink": "",
"openExploreLogs": "",
"relatedLogsUnavailableAfterDocsLink": "",
"relatedLogsUnavailableBeforeDocsLink": "",
"relatedLogsUnavailable": "",
"warnExperimentalFeature": ""
},
"viewBy": ""
@@ -1357,6 +1566,24 @@
"send-custom-feedback": ""
},
"grafana-ui": {
"action-editor": {
"button": {
"confirm": "",
"confirm-action": ""
},
"inline": {
"add-action": "",
"edit-action": ""
},
"modal": {
"action-body": "",
"action-method": "",
"action-query-params": "",
"action-title": "",
"action-title-placeholder": "",
"one-click-description": ""
}
},
"auto-save-field": {
"saved": "",
"saving": ""
@@ -1373,11 +1600,21 @@
},
"data-link-editor-modal": {
"cancel": "",
"one-click-description": "",
"save": ""
},
"data-link-inline-editor": {
"one-click": ""
},
"data-links-inline-editor": {
"add-link": "",
"one-click-link": ""
"edit-link": "",
"one-click": "",
"one-click-enabled": "",
"title-not-provided": "",
"tooltip-edit": "",
"tooltip-remove": "",
"url-not-provided": ""
},
"data-source-http-settings": {
"access-help": "",
@@ -1458,7 +1695,12 @@
"right-axis-indicator": ""
},
"viz-tooltip": {
"footer-add-annotation": ""
"actions-confirmation-input-placeholder": "",
"actions-confirmation-label": "",
"actions-confirmation-message": "",
"footer-add-annotation": "",
"footer-click-to-action": "",
"footer-click-to-navigate": ""
}
},
"graph": {
@@ -1780,7 +2022,8 @@
},
"log-row-message": {
"ellipsis": "",
"more": ""
"more": "",
"see-details": ""
},
"log-rows": {
"disable-popover": {
@@ -1792,6 +2035,13 @@
"shortcut": ""
}
},
"logs-navigation": {
"newer-logs": "",
"older-logs": "",
"scroll-bottom": "",
"scroll-top": "",
"start-of-range": ""
},
"popover-menu": {
"copy": "",
"disable-menu": "",
@@ -1881,6 +2131,7 @@
"title": ""
},
"migrated-counts": {
"alert_rule_groups": "",
"alert_rules": "",
"contact_points": "",
"dashboards": "",
@@ -1941,9 +2192,8 @@
"public-preview": {
"button-text": "",
"message": "",
"message-plugins": "",
"title": "",
"title-plugins": ""
"message-cloud": "",
"title": ""
},
"resource-details": {
"dismiss-button": "",
@@ -1986,6 +2236,7 @@
},
"resource-type": {
"alert_rule": "",
"alert_rule_group": "",
"contact_point": "",
"dashboard": "Tableau de bord",
"datasource": "Source de données",
@@ -2031,6 +2282,15 @@
"title": "Pourquoi héberger avec Grafana ?"
}
},
"multicombobox": {
"all": {
"title": "",
"title-filtered": ""
},
"clear": {
"title": ""
}
},
"nav": {
"add-new-connections": {
"title": "Ajouter une nouvelle connexion"
@@ -2384,7 +2644,8 @@
"list-label": "Navigation",
"open": "",
"undock": "Ancrer le menu"
}
},
"rss-button": ""
},
"news": {
"drawer": {
@@ -2516,19 +2777,35 @@
}
},
"details": {
"connections-tab": {
"description": ""
},
"labels": {
"contactGrafanaLabs": "",
"customLinks": "",
"customLinksTooltip": "",
"dependencies": "",
"documentation": "",
"downloads": "",
"from": "",
"installedVersion": "",
"lastCommitDate": "",
"latestVersion": "",
"links": "",
"license": "",
"raiseAnIssue": "",
"reportAbuse": "",
"reportAbuseTooltip": "",
"repository": "",
"signature": "",
"status": "",
"updatedAt": ""
},
"modal": {
"cancel": "",
"copyEmail": "",
"description": "",
"node": "",
"title": ""
}
},
"empty-state": {
@@ -2743,14 +3020,6 @@
"role-label": "Rôle"
}
},
"query-library": {
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
"search": "",
"user-info-get-error": "",
"user-names": ""
},
"query-operation": {
"header": {
"collapse-row": "Réduire la ligne de requête",
@@ -2760,7 +3029,6 @@
"expand-row": "Développer la ligne de requête",
"hide-response": "",
"remove-query": "Supprimer la requête",
"save-to-query-library": "",
"show-response": "",
"toggle-edit-mode": "Activer/désactiver le mode édition de texte"
},
@@ -2862,6 +3130,10 @@
"title": ""
},
"save-dashboards": {
"message-length": {
"info": "",
"title": ""
},
"name-exists": {
"message-info": "",
"message-suggestion": "",
@@ -3145,6 +3417,7 @@
"home-dashboard-placeholder": "Tableau de bord par défaut",
"locale-label": "Langue",
"locale-placeholder": "Choisir une langue",
"theme-description": "",
"theme-label": "Thème de l'interface utilisateur",
"week-start-label": "Début de la semaine"
},
@@ -3216,6 +3489,13 @@
"url-column-header": "URL de l'instantané",
"view-button": "Afficher"
},
"table": {
"container": {
"content": "",
"show-all-series": "",
"show-only-series": ""
}
},
"tag-filter": {
"clear-button": "",
"loading": "Chargement en cours...",
@@ -3309,20 +3589,26 @@
"start-your-metrics-exploration": "",
"subtitle": ""
},
"metric-overview": {
"description-label": "",
"labels": "",
"metric-attributes": "",
"no-description": "",
"type-label": "",
"unit-label": "",
"unknown-type": ""
},
"metric-select": {
"filter-by": "",
"native-histogram": "",
"new-badge": "",
"otel-switch": ""
},
"native-histogram-banner": {
"ch-heatmap": "",
"ch-histogram": "",
"click-histogram": "",
"hide-examples": "",
"learn-more": "",
"metric-examples": "",
"nh-heatmap": "",
"nh-histogram": "",
"now": "",
"previously": "",
"see-examples": "",
"sentence": ""
},
"recent-metrics": {
"or-view-a-recent-exploration": ""
},
+354 -68
View File
@@ -34,12 +34,6 @@
"save-button": ""
}
},
"actions-editor": {
"inline": {
"add-button": "",
"one-click-link": ""
}
},
"admin": {
"anon-users": {
"not-found": ""
@@ -174,6 +168,24 @@
}
},
"alerting": {
"alert": {
"alert-state": "",
"annotations": "",
"evaluation": "",
"evaluation-paused": "",
"evaluation-paused-description": "",
"last-evaluated": "",
"last-evaluation-duration": "",
"last-updated-at": "",
"last-updated-by": "",
"no-annotations": "",
"pending-period": "",
"rule": "",
"rule-identifier": "",
"rule-type": "",
"state-error-timeout": "",
"state-no-data": ""
},
"alert-recording-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -283,6 +295,7 @@
"contactPointFilter": {
"label": ""
},
"copy-to-clipboard": "",
"export": {
"subtitle": {
"formats": "",
@@ -299,6 +312,13 @@
}
}
},
"group-actions": {
"actions-trigger": "",
"delete": "",
"edit": "",
"export": "",
"reorder": ""
},
"list-view": {
"empty": {
"new-alert-rule": "",
@@ -484,11 +504,23 @@
},
"rule-list": {
"configure-datasource": "",
"ds-error-boundary": {
"description": "",
"title": ""
},
"filter-view": {
"no-more-results": "",
"no-rules-found": ""
},
"new-alert-rule": ""
"new-alert-rule": "",
"pagination": {
"next-page": "",
"previous-page": ""
},
"return-button": {
"title": ""
},
"rulerrule-loading-error": ""
},
"rule-state": {
"creating": "",
@@ -690,7 +722,10 @@
"title": ""
},
"custom-value": {
"label": ""
"description": ""
},
"group": {
"undefined": ""
},
"options": {
"no-found": ""
@@ -863,6 +898,82 @@
"redirect-link": "Lista no alerta do Grafana",
"subtitle": "Regras de alerta relacionadas a este painel de controle"
},
"default-layout": {
"description": "",
"item-options": {
"repeat": {
"direction": {
"horizontal": "",
"title": "",
"vertical": ""
},
"max": "",
"title": "",
"variable": {
"description": "",
"title": ""
}
}
},
"name": "",
"row-actions": {
"delete": "",
"modal": {
"alt-action": "",
"text": "",
"title": ""
}
},
"row-options": {
"button": {
"label": ""
},
"form": {
"cancel": "",
"repeat-for": {
"label": "",
"learn-more": "",
"warning": {
"text": ""
}
},
"title": "",
"update": ""
},
"modal": {
"title": ""
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": ""
}
},
"open": "",
"panels": {
"multi-select": {
"selection-number": ""
}
},
"row": {
"header": {
"hide": "",
"title": ""
},
"multi-select": {
"options-header": "",
"selection-number": ""
}
},
"tab": {
"multi-select": {
"options-header": "",
"selection-number": ""
}
}
},
"empty": {
"add-library-panel-body": "Adicione visualizações que são compartilhadas com outros painéis de controle.",
"add-library-panel-button": "Adicionar painel de biblioteca",
@@ -874,6 +985,9 @@
"import-a-dashboard-header": "Importar um painel de controle",
"import-dashboard-button": "Importar painel de controle"
},
"errors": {
"failed-to-load": ""
},
"inspect": {
"data-tab": "Dados",
"error-tab": "Erro",
@@ -926,19 +1040,130 @@
"rows": "Número total de linhas",
"table-title": "Estatísticas"
},
"options": {
"description": "",
"title": "",
"title-option": ""
},
"panel-edit": {
"alerting-tab": {
"dashboard-not-saved": "",
"no-rules": ""
}
},
"responsive-layout": {
"description": "",
"item-options": {
"hide-no-data": "",
"title": ""
},
"name": "",
"options": {
"columns": "",
"fixed": "",
"min": "",
"one-column": "",
"rows": "",
"three-columns": "",
"two-columns": ""
}
},
"rows-layout": {
"description": "",
"name": "",
"row": {
"collapse": "",
"expand": "",
"new": "",
"repeat": {
"learn-more": "",
"warning": ""
}
},
"row-options": {
"height": {
"expand": "",
"hide-row-header": "",
"min": "",
"title": ""
},
"repeat": {
"title": "",
"variable": {
"title": ""
}
},
"title": "",
"title-option": ""
}
},
"tabs-layout": {
"description": "",
"name": "",
"tab": {
"new": ""
},
"tab-options": {
"title": "",
"title-option": ""
}
},
"toolbar": {
"add": "Adicionar",
"add-panel": "",
"add-panel-lib": "",
"add-row": "",
"add-tab": "",
"alert-rules": "Regras de alerta",
"back-to-dashboard": "",
"dashboard-settings": {
"label": "",
"tooltip": ""
},
"discard-library-panel-changes": "",
"discard-panel": "",
"discard-panel-new": "",
"edit": {
"label": "",
"tooltip": ""
},
"edit-dashboard-v2-schema": "",
"enter-edit-mode": {
"label": "",
"tooltip": ""
},
"exit-edit-mode": {
"label": "",
"tooltip": ""
},
"mark-favorite": "Marcar como favorito",
"more-save-options": "",
"open-original": "Abrir painel de controle original",
"playlist-next": "Ir para o próximo painel de controle",
"playlist-previous": "Ir para o painel de controle anterior",
"playlist-stop": "Parar lista de reprodução",
"public-dashboard": "",
"refresh": "Atualizar painel de controle",
"save": "Salvar painel de controle",
"save-dashboard": {
"label": "",
"tooltip": ""
},
"save-dashboard-copy": {
"label": "",
"tooltip": ""
},
"save-dashboard-short": "",
"save-library-panel": "",
"settings": "Configurações do painel de controle",
"share": "Compartilhar painel de controle",
"share": {
"label": "",
"tooltip": ""
},
"share-button": "Compartilhar",
"show-hidden-elements": "",
"switch-old-dashboard": "",
"unlink-library-panel": "",
"unmark-favorite": "Desmarcar como favorito"
},
"validation": {
@@ -946,6 +1171,14 @@
"invalid-json": "JSON inválido",
"tags-expected-array": "matriz de tags esperada",
"tags-expected-strings": "matriz de strings de tags esperada"
},
"viz-panel": {
"options": {
"description": "",
"title": "",
"title-option": "",
"transparent-background": ""
}
}
},
"dashboard-import": {
@@ -1048,6 +1281,12 @@
"angular-deprecation-description": "",
"angular-deprecation-heading": ""
},
"panel-queries": {
"add-query-from-library": ""
},
"query-library": {
"add-query-button": ""
},
"settings": {
"variables": {
"dependencies": {
@@ -1125,40 +1364,11 @@
"scan-for-older-logs": "",
"stop-scan": ""
},
"query-library": {
"add-edit-description": "",
"cancel": "",
"default-description": "",
"delete-query": "",
"delete-query-text": "",
"delete-query-title": "",
"private": "",
"public": "",
"query-deleted": "",
"query-template-add-error": "",
"query-template-added": "",
"query-template-edit-error": "",
"query-template-edited": "",
"save": ""
},
"query-template-modal": {
"add-info": "",
"add-title": "",
"auto-star": "",
"data-source-name": "",
"description": "",
"edit-info": "",
"edit-title": "",
"query": "",
"visibility": ""
},
"query-template-modall": {
"data-source-type": ""
},
"rich-history": {
"close-tooltip": "Fechar histórico de consultas",
"datasource-a-z": "Fonte de dados A-Z",
"datasource-z-a": "Fonte de dados Z-A",
"library-history-dropdown": "",
"newest-first": "Mais recentes primeiro",
"oldest-first": "Mais antigos primeiro",
"query-history": "Histórico de consultas",
@@ -1305,10 +1515,9 @@
"sortBy": ""
},
"related-logs": {
"docsLink": "",
"LrrDocsLink": "",
"openExploreLogs": "",
"relatedLogsUnavailableAfterDocsLink": "",
"relatedLogsUnavailableBeforeDocsLink": "",
"relatedLogsUnavailable": "",
"warnExperimentalFeature": ""
},
"viewBy": ""
@@ -1357,6 +1566,24 @@
"send-custom-feedback": ""
},
"grafana-ui": {
"action-editor": {
"button": {
"confirm": "",
"confirm-action": ""
},
"inline": {
"add-action": "",
"edit-action": ""
},
"modal": {
"action-body": "",
"action-method": "",
"action-query-params": "",
"action-title": "",
"action-title-placeholder": "",
"one-click-description": ""
}
},
"auto-save-field": {
"saved": "",
"saving": ""
@@ -1373,11 +1600,21 @@
},
"data-link-editor-modal": {
"cancel": "",
"one-click-description": "",
"save": ""
},
"data-link-inline-editor": {
"one-click": ""
},
"data-links-inline-editor": {
"add-link": "",
"one-click-link": ""
"edit-link": "",
"one-click": "",
"one-click-enabled": "",
"title-not-provided": "",
"tooltip-edit": "",
"tooltip-remove": "",
"url-not-provided": ""
},
"data-source-http-settings": {
"access-help": "",
@@ -1458,7 +1695,12 @@
"right-axis-indicator": ""
},
"viz-tooltip": {
"footer-add-annotation": ""
"actions-confirmation-input-placeholder": "",
"actions-confirmation-label": "",
"actions-confirmation-message": "",
"footer-add-annotation": "",
"footer-click-to-action": "",
"footer-click-to-navigate": ""
}
},
"graph": {
@@ -1780,7 +2022,8 @@
},
"log-row-message": {
"ellipsis": "",
"more": ""
"more": "",
"see-details": ""
},
"log-rows": {
"disable-popover": {
@@ -1792,6 +2035,13 @@
"shortcut": ""
}
},
"logs-navigation": {
"newer-logs": "",
"older-logs": "",
"scroll-bottom": "",
"scroll-top": "",
"start-of-range": ""
},
"popover-menu": {
"copy": "",
"disable-menu": "",
@@ -1881,6 +2131,7 @@
"title": ""
},
"migrated-counts": {
"alert_rule_groups": "",
"alert_rules": "",
"contact_points": "",
"dashboards": "",
@@ -1941,9 +2192,8 @@
"public-preview": {
"button-text": "",
"message": "",
"message-plugins": "",
"title": "",
"title-plugins": ""
"message-cloud": "",
"title": ""
},
"resource-details": {
"dismiss-button": "",
@@ -1986,6 +2236,7 @@
},
"resource-type": {
"alert_rule": "",
"alert_rule_group": "",
"contact_point": "",
"dashboard": "Painel de controle",
"datasource": "Fonte de dados",
@@ -2031,6 +2282,15 @@
"title": "Por que hospedar com o Grafana?"
}
},
"multicombobox": {
"all": {
"title": "",
"title-filtered": ""
},
"clear": {
"title": ""
}
},
"nav": {
"add-new-connections": {
"title": "Adicionar nova conexão"
@@ -2384,7 +2644,8 @@
"list-label": "Navegação",
"open": "",
"undock": "Desacoplar menu"
}
},
"rss-button": ""
},
"news": {
"drawer": {
@@ -2516,19 +2777,35 @@
}
},
"details": {
"connections-tab": {
"description": ""
},
"labels": {
"contactGrafanaLabs": "",
"customLinks": "",
"customLinksTooltip": "",
"dependencies": "",
"documentation": "",
"downloads": "",
"from": "",
"installedVersion": "",
"lastCommitDate": "",
"latestVersion": "",
"links": "",
"license": "",
"raiseAnIssue": "",
"reportAbuse": "",
"reportAbuseTooltip": "",
"repository": "",
"signature": "",
"status": "",
"updatedAt": ""
},
"modal": {
"cancel": "",
"copyEmail": "",
"description": "",
"node": "",
"title": ""
}
},
"empty-state": {
@@ -2743,14 +3020,6 @@
"role-label": "Função"
}
},
"query-library": {
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
"search": "",
"user-info-get-error": "",
"user-names": ""
},
"query-operation": {
"header": {
"collapse-row": "Recolher linha de consulta",
@@ -2760,7 +3029,6 @@
"expand-row": "Expandir linha de consulta",
"hide-response": "",
"remove-query": "Remover consulta",
"save-to-query-library": "",
"show-response": "",
"toggle-edit-mode": "Alternar modo de edição de texto"
},
@@ -2862,6 +3130,10 @@
"title": ""
},
"save-dashboards": {
"message-length": {
"info": "",
"title": ""
},
"name-exists": {
"message-info": "",
"message-suggestion": "",
@@ -3145,6 +3417,7 @@
"home-dashboard-placeholder": "Painel de controle padrão",
"locale-label": "Idioma",
"locale-placeholder": "Escolher idioma",
"theme-description": "",
"theme-label": "Tema da interface",
"week-start-label": "Início da semana"
},
@@ -3216,6 +3489,13 @@
"url-column-header": "URL da captura",
"view-button": "Visualizar"
},
"table": {
"container": {
"content": "",
"show-all-series": "",
"show-only-series": ""
}
},
"tag-filter": {
"clear-button": "",
"loading": "Carregando...",
@@ -3309,20 +3589,26 @@
"start-your-metrics-exploration": "",
"subtitle": ""
},
"metric-overview": {
"description-label": "",
"labels": "",
"metric-attributes": "",
"no-description": "",
"type-label": "",
"unit-label": "",
"unknown-type": ""
},
"metric-select": {
"filter-by": "",
"native-histogram": "",
"new-badge": "",
"otel-switch": ""
},
"native-histogram-banner": {
"ch-heatmap": "",
"ch-histogram": "",
"click-histogram": "",
"hide-examples": "",
"learn-more": "",
"metric-examples": "",
"nh-heatmap": "",
"nh-histogram": "",
"now": "",
"previously": "",
"see-examples": "",
"sentence": ""
},
"recent-metrics": {
"or-view-a-recent-exploration": ""
},
+354 -68
View File
@@ -34,12 +34,6 @@
"save-button": ""
}
},
"actions-editor": {
"inline": {
"add-button": "",
"one-click-link": ""
}
},
"admin": {
"anon-users": {
"not-found": ""
@@ -174,6 +168,24 @@
}
},
"alerting": {
"alert": {
"alert-state": "",
"annotations": "",
"evaluation": "",
"evaluation-paused": "",
"evaluation-paused-description": "",
"last-evaluated": "",
"last-evaluation-duration": "",
"last-updated-at": "",
"last-updated-by": "",
"no-annotations": "",
"pending-period": "",
"rule": "",
"rule-identifier": "",
"rule-type": "",
"state-error-timeout": "",
"state-no-data": ""
},
"alert-recording-rule-form": {
"evaluation-behaviour": {
"description": {
@@ -281,6 +293,7 @@
"contactPointFilter": {
"label": ""
},
"copy-to-clipboard": "",
"export": {
"subtitle": {
"formats": "",
@@ -297,6 +310,13 @@
}
}
},
"group-actions": {
"actions-trigger": "",
"delete": "",
"edit": "",
"export": "",
"reorder": ""
},
"list-view": {
"empty": {
"new-alert-rule": "",
@@ -480,11 +500,23 @@
},
"rule-list": {
"configure-datasource": "",
"ds-error-boundary": {
"description": "",
"title": ""
},
"filter-view": {
"no-more-results": "",
"no-rules-found": ""
},
"new-alert-rule": ""
"new-alert-rule": "",
"pagination": {
"next-page": "",
"previous-page": ""
},
"return-button": {
"title": ""
},
"rulerrule-loading-error": ""
},
"rule-state": {
"creating": "",
@@ -681,7 +713,10 @@
"title": ""
},
"custom-value": {
"label": ""
"description": ""
},
"group": {
"undefined": ""
},
"options": {
"no-found": ""
@@ -854,6 +889,82 @@
"redirect-link": "Grafana Alerting 中的列表",
"subtitle": "与此仪表板相关的警报规则"
},
"default-layout": {
"description": "",
"item-options": {
"repeat": {
"direction": {
"horizontal": "",
"title": "",
"vertical": ""
},
"max": "",
"title": "",
"variable": {
"description": "",
"title": ""
}
}
},
"name": "",
"row-actions": {
"delete": "",
"modal": {
"alt-action": "",
"text": "",
"title": ""
}
},
"row-options": {
"button": {
"label": ""
},
"form": {
"cancel": "",
"repeat-for": {
"label": "",
"learn-more": "",
"warning": {
"text": ""
}
},
"title": "",
"update": ""
},
"modal": {
"title": ""
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": ""
}
},
"open": "",
"panels": {
"multi-select": {
"selection-number": ""
}
},
"row": {
"header": {
"hide": "",
"title": ""
},
"multi-select": {
"options-header": "",
"selection-number": ""
}
},
"tab": {
"multi-select": {
"options-header": "",
"selection-number": ""
}
}
},
"empty": {
"add-library-panel-body": "添加与其他仪表板共享的可视化。",
"add-library-panel-button": "添加库面板",
@@ -865,6 +976,9 @@
"import-a-dashboard-header": "导入仪表板",
"import-dashboard-button": "导入仪表板"
},
"errors": {
"failed-to-load": ""
},
"inspect": {
"data-tab": "数据",
"error-tab": "错误",
@@ -917,19 +1031,130 @@
"rows": "总行数",
"table-title": "统计信息"
},
"options": {
"description": "",
"title": "",
"title-option": ""
},
"panel-edit": {
"alerting-tab": {
"dashboard-not-saved": "",
"no-rules": ""
}
},
"responsive-layout": {
"description": "",
"item-options": {
"hide-no-data": "",
"title": ""
},
"name": "",
"options": {
"columns": "",
"fixed": "",
"min": "",
"one-column": "",
"rows": "",
"three-columns": "",
"two-columns": ""
}
},
"rows-layout": {
"description": "",
"name": "",
"row": {
"collapse": "",
"expand": "",
"new": "",
"repeat": {
"learn-more": "",
"warning": ""
}
},
"row-options": {
"height": {
"expand": "",
"hide-row-header": "",
"min": "",
"title": ""
},
"repeat": {
"title": "",
"variable": {
"title": ""
}
},
"title": "",
"title-option": ""
}
},
"tabs-layout": {
"description": "",
"name": "",
"tab": {
"new": ""
},
"tab-options": {
"title": "",
"title-option": ""
}
},
"toolbar": {
"add": "添加",
"add-panel": "",
"add-panel-lib": "",
"add-row": "",
"add-tab": "",
"alert-rules": "警报规则",
"back-to-dashboard": "",
"dashboard-settings": {
"label": "",
"tooltip": ""
},
"discard-library-panel-changes": "",
"discard-panel": "",
"discard-panel-new": "",
"edit": {
"label": "",
"tooltip": ""
},
"edit-dashboard-v2-schema": "",
"enter-edit-mode": {
"label": "",
"tooltip": ""
},
"exit-edit-mode": {
"label": "",
"tooltip": ""
},
"mark-favorite": "标记为收藏",
"more-save-options": "",
"open-original": "打开原始仪表板",
"playlist-next": "前往下一个仪表板",
"playlist-previous": "前往上一个仪表板",
"playlist-stop": "停止播放列表",
"public-dashboard": "",
"refresh": "刷新仪表板",
"save": "保存仪表板",
"save-dashboard": {
"label": "",
"tooltip": ""
},
"save-dashboard-copy": {
"label": "",
"tooltip": ""
},
"save-dashboard-short": "",
"save-library-panel": "",
"settings": "仪表板设置",
"share": "分享仪表板或面板",
"share": {
"label": "",
"tooltip": ""
},
"share-button": "分享",
"show-hidden-elements": "",
"switch-old-dashboard": "",
"unlink-library-panel": "",
"unmark-favorite": "取消标记为收藏"
},
"validation": {
@@ -937,6 +1162,14 @@
"invalid-json": "无有效的 JSON",
"tags-expected-array": "标签预期数组",
"tags-expected-strings": "标签预期字符串数组"
},
"viz-panel": {
"options": {
"description": "",
"title": "",
"title-option": "",
"transparent-background": ""
}
}
},
"dashboard-import": {
@@ -1039,6 +1272,12 @@
"angular-deprecation-description": "",
"angular-deprecation-heading": ""
},
"panel-queries": {
"add-query-from-library": ""
},
"query-library": {
"add-query-button": ""
},
"settings": {
"variables": {
"dependencies": {
@@ -1116,40 +1355,11 @@
"scan-for-older-logs": "",
"stop-scan": ""
},
"query-library": {
"add-edit-description": "",
"cancel": "",
"default-description": "",
"delete-query": "",
"delete-query-text": "",
"delete-query-title": "",
"private": "",
"public": "",
"query-deleted": "",
"query-template-add-error": "",
"query-template-added": "",
"query-template-edit-error": "",
"query-template-edited": "",
"save": ""
},
"query-template-modal": {
"add-info": "",
"add-title": "",
"auto-star": "",
"data-source-name": "",
"description": "",
"edit-info": "",
"edit-title": "",
"query": "",
"visibility": ""
},
"query-template-modall": {
"data-source-type": ""
},
"rich-history": {
"close-tooltip": "关闭查询历史记录",
"datasource-a-z": "数据源 A-Z",
"datasource-z-a": "数据源 Z-A",
"library-history-dropdown": "",
"newest-first": "最新在前",
"oldest-first": "最早在前",
"query-history": "查询历史记录",
@@ -1296,10 +1506,9 @@
"sortBy": ""
},
"related-logs": {
"docsLink": "",
"LrrDocsLink": "",
"openExploreLogs": "",
"relatedLogsUnavailableAfterDocsLink": "",
"relatedLogsUnavailableBeforeDocsLink": "",
"relatedLogsUnavailable": "",
"warnExperimentalFeature": ""
},
"viewBy": ""
@@ -1348,6 +1557,24 @@
"send-custom-feedback": ""
},
"grafana-ui": {
"action-editor": {
"button": {
"confirm": "",
"confirm-action": ""
},
"inline": {
"add-action": "",
"edit-action": ""
},
"modal": {
"action-body": "",
"action-method": "",
"action-query-params": "",
"action-title": "",
"action-title-placeholder": "",
"one-click-description": ""
}
},
"auto-save-field": {
"saved": "",
"saving": ""
@@ -1364,11 +1591,21 @@
},
"data-link-editor-modal": {
"cancel": "",
"one-click-description": "",
"save": ""
},
"data-link-inline-editor": {
"one-click": ""
},
"data-links-inline-editor": {
"add-link": "",
"one-click-link": ""
"edit-link": "",
"one-click": "",
"one-click-enabled": "",
"title-not-provided": "",
"tooltip-edit": "",
"tooltip-remove": "",
"url-not-provided": ""
},
"data-source-http-settings": {
"access-help": "",
@@ -1449,7 +1686,12 @@
"right-axis-indicator": ""
},
"viz-tooltip": {
"footer-add-annotation": ""
"actions-confirmation-input-placeholder": "",
"actions-confirmation-label": "",
"actions-confirmation-message": "",
"footer-add-annotation": "",
"footer-click-to-action": "",
"footer-click-to-navigate": ""
}
},
"graph": {
@@ -1770,7 +2012,8 @@
},
"log-row-message": {
"ellipsis": "",
"more": ""
"more": "",
"see-details": ""
},
"log-rows": {
"disable-popover": {
@@ -1782,6 +2025,13 @@
"shortcut": ""
}
},
"logs-navigation": {
"newer-logs": "",
"older-logs": "",
"scroll-bottom": "",
"scroll-top": "",
"start-of-range": ""
},
"popover-menu": {
"copy": "",
"disable-menu": "",
@@ -1871,6 +2121,7 @@
"title": ""
},
"migrated-counts": {
"alert_rule_groups": "",
"alert_rules": "",
"contact_points": "",
"dashboards": "",
@@ -1931,9 +2182,8 @@
"public-preview": {
"button-text": "",
"message": "",
"message-plugins": "",
"title": "",
"title-plugins": ""
"message-cloud": "",
"title": ""
},
"resource-details": {
"dismiss-button": "",
@@ -1976,6 +2226,7 @@
},
"resource-type": {
"alert_rule": "",
"alert_rule_group": "",
"contact_point": "",
"dashboard": "仪表板",
"datasource": "数据源",
@@ -2021,6 +2272,15 @@
"title": "为什么使用 Grafana 托管?"
}
},
"multicombobox": {
"all": {
"title": "",
"title-filtered": ""
},
"clear": {
"title": ""
}
},
"nav": {
"add-new-connections": {
"title": "添加新连接"
@@ -2374,7 +2634,8 @@
"list-label": "导航",
"open": "",
"undock": "取消停靠菜单"
}
},
"rss-button": ""
},
"news": {
"drawer": {
@@ -2506,19 +2767,35 @@
}
},
"details": {
"connections-tab": {
"description": ""
},
"labels": {
"contactGrafanaLabs": "",
"customLinks": "",
"customLinksTooltip": "",
"dependencies": "",
"documentation": "",
"downloads": "",
"from": "",
"installedVersion": "",
"lastCommitDate": "",
"latestVersion": "",
"links": "",
"license": "",
"raiseAnIssue": "",
"reportAbuse": "",
"reportAbuseTooltip": "",
"repository": "",
"signature": "",
"status": "",
"updatedAt": ""
},
"modal": {
"cancel": "",
"copyEmail": "",
"description": "",
"node": "",
"title": ""
}
},
"empty-state": {
@@ -2732,14 +3009,6 @@
"role-label": "角色"
}
},
"query-library": {
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
"search": "",
"user-info-get-error": "",
"user-names": ""
},
"query-operation": {
"header": {
"collapse-row": "折叠查询行",
@@ -2749,7 +3018,6 @@
"expand-row": "展开查询行",
"hide-response": "",
"remove-query": "删除查询",
"save-to-query-library": "",
"show-response": "",
"toggle-edit-mode": "切换文本编辑模式"
},
@@ -2848,6 +3116,10 @@
"title": ""
},
"save-dashboards": {
"message-length": {
"info": "",
"title": ""
},
"name-exists": {
"message-info": "",
"message-suggestion": "",
@@ -3131,6 +3403,7 @@
"home-dashboard-placeholder": "默认仪表板",
"locale-label": "语言",
"locale-placeholder": "选择语言",
"theme-description": "",
"theme-label": "UI 主题",
"week-start-label": "每周开始日"
},
@@ -3202,6 +3475,13 @@
"url-column-header": "快照网址",
"view-button": "查看"
},
"table": {
"container": {
"content": "",
"show-all-series": "",
"show-only-series": ""
}
},
"tag-filter": {
"clear-button": "",
"loading": "加载中...",
@@ -3295,20 +3575,26 @@
"start-your-metrics-exploration": "",
"subtitle": ""
},
"metric-overview": {
"description-label": "",
"labels": "",
"metric-attributes": "",
"no-description": "",
"type-label": "",
"unit-label": "",
"unknown-type": ""
},
"metric-select": {
"filter-by": "",
"native-histogram": "",
"new-badge": "",
"otel-switch": ""
},
"native-histogram-banner": {
"ch-heatmap": "",
"ch-histogram": "",
"click-histogram": "",
"hide-examples": "",
"learn-more": "",
"metric-examples": "",
"nh-heatmap": "",
"nh-histogram": "",
"now": "",
"previously": "",
"see-examples": "",
"sentence": ""
},
"recent-metrics": {
"or-view-a-recent-exploration": ""
},