Merge remote-tracking branch 'origin/main' into mgyongyosi/add-kind-for-team-sync

This commit is contained in:
Mihaly Gyongyosi
2025-11-03 15:27:46 +01:00
19 changed files with 807 additions and 379 deletions
@@ -40,8 +40,7 @@ To set up file sync with local with local files, you need to:
Local file provisioning using **Administration** > **Provisioning** will eventually replace the traditional methods Grafana has used for referencing local file systems for dashboard files.
{{< admonition type="note" >}}
For production system, we recommend using the `folderFromFilesStructure` capability instead of **Administration** > **Provisioning** to include dashboards from a local file system in your Grafana instance.
Refer to [Provision Grafana](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/provisioning/#provision-folders-structure-from-filesystem-to-grafana) for more information.
For production systems, use the `folderFromFilesStructure` capability instead of **Administration** > **Provisioning** to include dashboards from a local file system in your Grafana instance. Refer to [Provision Grafana](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/provisioning/#provision-folders-structure-from-filesystem-to-grafana) for more information.
{{< /admonition >}}
### Limitations
@@ -125,6 +124,18 @@ The set up process verifies the path and provides an error message if a problem
### Choose what to synchronize
#### Synchronization limitations
Full instance sync is not available in Grafana Cloud.
In Grafana OSS/Enterprise:
- If you try to perform a full instance sync with resources that contain alerts or panels, the connection will be blocked.
- You won't be able to create new alerts or library panels after setup is completed.
- If you opted for full instance sync and want to use alerts and library panels, you'll have to delete the provisioned repository and connect again with folder sync.
#### Set up synchronization
Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections).
- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to.
@@ -61,6 +61,8 @@ Refer to [Known limitations](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/
{{< /admonition >}}
### Requirements
To set up Git Sync, you need:
- Administration rights in your Grafana organization.
@@ -128,34 +130,37 @@ To connect your GitHub repository, follow these steps:
### Choose what to synchronize
In this step you can decide which elements to synchronize. Keep in mind the available options depend on the status of your Grafana instance.
- If the instance contains resources in an incompatible data format, you'll have to migrate all the data using instance sync. Folder sync won't be supported.
- If there is already another connection using folder sync, instance sync won't be offered.
#### Synchronization limitations
Git Sync only supports dashboards and folders. Alerts, panels, and other resources are not supported yet.
{{< admonition type="caution" >}}
Git Sync only works with specific folders for the moment. Full-instance sync is not currently supported. Refer to [Supported resources](/docs/grafana/<GRAFANA_VERSION>/observability-as-code/provision-resources/intro-git-sync#supported-resources) for more details about which resources you can sync.
Refer to [Known limitations](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/provision-resources/intro-git-sync#known-limitations/) before using Git Sync. Refer to [Supported resources](/docs/grafana/<GRAFANA_VERSION>/observability-as-code/provision-resources/intro-git-sync#supported-resources) for details about which resources you can sync.
{{< /admonition >}}
In this step you can decide which elements to synchronize. Keep in mind the available options depend on the status of your GitHub repository. The first time you connect Grafana with a GitHub repository, you need to synchronize with external storage. If you are syncing with a new or empty repository, you won't have an option to migrate dashboards.
Full instance sync is not available in Grafana Cloud.
1. Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections).
In Grafana OSS/Enterprise:
- If you try to perform a full instance sync with resources that contain alerts or panels, Git Sync will block the connection.
- You won't be able to create new alerts or library panels after the setup is completed.
- If you opted for full instance sync and want to use alerts and library panels, you'll have to delete the synced repository and connect again with folder sync.
#### Set up synchronization
To set up synchronization, choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections).
- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to.
- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections.
1. Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI.
1. Click **Synchronize** to continue.
<!-- ### Synchronize with external storage
{{< admonition type="note">}}
During the synchronization process, your dashboards will be temporarily unavailable.
No data or configuration will be lost.
However, no one will be able to create, edit, or delete resources during this process.
In the last step, the resources will disappear and will reappear and be managed through external storage.
{{< /admonition >}}
1. Select **History** to include commits for each historical value in the synchronized data.
1. Select **Begin synchronization** to continue. -->
Next, enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. Click **Synchronize** to continue.
### Choose additional settings
@@ -163,7 +168,6 @@ Finally, you can set up how often your configured storage is polled for updates.
1. For **Update instance interval (seconds)**, enter how often you want the instance to pull updates from GitHub. The default value is 60 seconds.
1. Optional: Select **Read only** to ensure resources can't be modified in Grafana.
<!-- No workflow option listed in the UI. 1. For **Workflows**, select the GitHub workflows that you want to allow to run in the repository. Both **Branch** and **Write** are selected by default. -->
1. Optional: If you have the Grafana Image Renderer plugin configured, you can **Enable dashboards previews in pull requests**. If image rendering is not available, then you can't select this option. For more information, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer).
1. Select **Finish** to proceed.
@@ -179,23 +183,11 @@ You can extend Git Sync by getting instant updates and pull requests using webho
### Set up webhooks for realtime notification and pull request integration
When connecting to a GitHub repository, Git Sync use webhooks to enable real-time updates from GitHub public repositories or enable the pull request integration.
Without webhooks, the polling interval is set in the final configuration screen (default is 60 seconds).
Your Grafana instance must be exposed to the public internet.
You can do this via port forwarding and DNS, a tool such as `ngrok`, or any other method you prefer.
When connecting to a GitHub repository, Git Sync uses webhooks to enable real-time updates from GitHub public repositories or enable pull request integrations. Without webhooks, the polling interval is set in the final configuration screen, and the default is 60 seconds. If you use local storage, then Git Sync only provides periodic pulling.
The permissions set in your GitHub access token provide the authorization for this communication.
You can set up webhooks with whichever service or tooling you prefer. You can use Cloudflare Tunnels with a Cloudflare-managed domain, port-forwarding and DNS options, or a tool such as `ngrok`.
If you use local storage, then Git Sync only provides periodic pulling.
<!-- Grafana Cloud support not available yet
{{< admonition type="note" >}}
Webhooks are automatically available for Grafana Cloud users.
{{< /admonition >}}
-->
Set up webhooks with whichever service or tooling you prefer.
For example, you can use Cloudflare Tunnels with a Cloudflare-managed domain, port-forwarding and DNS options, or a tool such as `ngrok`.
To set up webhooks you need to expose your Grafana instance to the public Internet. You can do this via port forwarding and DNS, a tool such as `ngrok`, or any other method you prefer. The permissions set in your GitHub access token provide the authorization for this communication.
After you have the public URL, you can add it to your Grafana configuration file:
@@ -204,23 +196,22 @@ After you have the public URL, you can add it to your Grafana configuration file
root_url = https://PUBLIC_DOMAIN.HERE
```
You can check the configured webhooks in the **View** link for your GitHub repository from **Administration** > **Provisioning**.
To check the configured webhooks, go to **Administration** > **Provisioning** and click the **View** link for your GitHub repository.
#### Necessary paths
#### Expose necessary paths only
If your security setup does not permit publicly exposing the Grafana instance, you can either choose to allowlist the GitHub IP addresses, or expose only the necessary paths.
If your security setup does not permit publicly exposing the Grafana instance, you can either choose to `allowlist` the GitHub IP addresses, or expose only the necessary paths.
The necessary paths required to be exposed are (RegExp):
The necessary paths required to be exposed are, in RegExp:
- `/apis/provisioning\.grafana\.app/v0(alpha1)?/namespaces/[^/]+/repositories/[^/]+/(webhook|render/.*)$`
<!-- TODO: Path for the blob storage for image rendering? @ryantxu would know this best. -->
### Set up image rendering for dashboard previews
By setting up image rendering, you can add visual previews of dashboard updates directly in pull requests.
Image rendering also requires webhooks.
Set up image rendering to add visual previews of dashboard updates directly in pull requests. Image rendering also requires webhooks.
You can enable this capability by installing the Grafana Image Renderer in your Grafana instance. For more information and installation instructions, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer).
To enable this capability, install the Grafana Image Renderer in your Grafana instance. For more information and installation instructions, refer to the [Image Renderer service](https://github.com/grafana/grafana-image-renderer).
## Modify configurations after set up is complete
@@ -63,14 +63,31 @@ With Git Sync, you can make changes to the files in the provisioned folder in Gi
## Known limitations
Git Sync is under development and the following limitations apply:
{{< admonition type="caution" >}}
Refer to [Requirements](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/observability-as-code/provision-resources/git-sync-setup#requirements/) to learn what you need to use Git Sync.
{{< /admonition >}}
**Git Sync is under development and the following limitations apply.**
**Synced resources**
- You can only sync dashboards and folders. Refer to [Supported resources](#supported-resources) for more information.
- If you're using Git Sync in Grafana OSS and Grafana Enterprise, some resources might be in an incompatible data format and can't be synced.
- You can only authenticate in GitHub using your Personal Access Token token.
- Support for native Git, Git app, and other providers, such as GitLab or Bitbucket, is on the roadmap.
- If you're using Git Sync in Grafana OSS and Grafana Enterprise, some resources might be in an incompatible data format and won't be synced.
- Full-instance sync is not available in Grafana Cloud and has limitations in Grafana OSS and Grafana Enterprise. Refer to [Choose what to synchronize](../git-sync-setup.md#choose-what-to-synchronize) for more details.
- When migrating to full instance sync, during the synchronization process your resources will be temporarily unavailable. No one will be able to create, edit, or delete resources during this process.
- If you want to manage existing resources with Git Sync, you need to save them as JSON files and commit them to the synced repository. Open a PR to import, copy, move, or save a dashboard.
- Restoring resources from the UI is currently not possible. As an alternative, you can restore dashboards directly in your GitHub repository by raising a PR, and they will be updated in Grafana.
**Authentication**
- You can only authenticate in GitHub using your Personal Access Token token.
**Compatibility**
- Support for native Git, Git app, and other providers, such as GitLab or Bitbucket, is on the roadmap.
## Supported resources
Git Sync only supports dashboards and folders. Alerts, panels, and other resources are not supported yet.
@@ -86,9 +103,9 @@ A resource can be:
| **Supported** | The resource can be managed with Git Sync. | The resource is supported but has compatibility issues. It **cannot** be managed with Git Sync. |
| **Unsupported** | The resource is **not** supported and **cannot** be managed with Git Sync. | Not applicable. |
### Instance states
### Git Sync instance states
An instance can be in one of the following states:
An instance can be in one of the following Git Sync states:
- **Unprovisioned**: None of the instance's resources are being managed by Git Sync.
- **Partially provisioned**: Some of the resources are controlled by Git Sync.
@@ -74,6 +74,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch data source | |
| `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes |
| `improvedExternalSessionHandlingSAML` | Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. | Yes |
| `newLogsPanel` | Enables the new logs panel | Yes |
| `alertingMigrationUI` | Enables the alerting migration UI, to migrate data source-managed rules to Grafana-managed rules | Yes |
| `alertingImportYAMLUI` | Enables a UI feature for importing rules from a Prometheus file to Grafana-managed rules | Yes |
| `unifiedNavbars` | Enables unified navbars | |
@@ -883,7 +883,8 @@ export interface FeatureToggles {
*/
fetchRulesUsingPost?: boolean;
/**
* Enables the new logs panel in Explore
* Enables the new logs panel
* @default true
*/
newLogsPanel?: boolean;
/**
File diff suppressed because it is too large Load Diff
@@ -30,6 +30,8 @@ message MutateOperation {
DeleteFolderOperation delete_folder = 2;
CreatePermissionOperation create_permission = 3;
DeletePermissionOperation delete_permission = 4;
UpdateUserOrgRoleOperation update_user_org_role = 5;
DeleteUserOrgRoleOperation delete_user_org_role = 6;
}
}
@@ -61,6 +63,20 @@ message DeletePermissionOperation {
Permission permission = 2;
}
message UpdateUserOrgRoleOperation {
// User UID
string user = 1;
// Role name (e.g: "Admin", "Editor", "Viewer")
string role = 2;
}
message DeleteUserOrgRoleOperation {
// User UID
string user = 1;
// Role name (e.g: "Admin", "Editor", "Viewer")
string role = 2;
}
message Resource {
// group of the resource (e.g: "dashboard.grafana.app")
string group = 1;
@@ -67,6 +67,13 @@ var resourceTranslations = map[string]resourceTranslation{
"dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource, ""),
"dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource, ""),
"dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource, ""),
// Action sets
"folders:view": newMapping(RelationSetView, ""),
"folders:edit": newMapping(RelationSetEdit, ""),
"folders:admin": newMapping(RelationSetAdmin, ""),
"dashboards:view": newScopedMapping(RelationSetView, dashboardGroup, dashboardResource, ""),
"dashboards:edit": newScopedMapping(RelationSetEdit, dashboardGroup, dashboardResource, ""),
"dashboards:admin": newScopedMapping(RelationSetAdmin, dashboardGroup, dashboardResource, ""),
},
},
KindDashboards: {
@@ -78,6 +85,10 @@ var resourceTranslations = map[string]resourceTranslation{
"dashboards:write": newMapping(RelationUpdate, ""),
"dashboards:create": newMapping(RelationCreate, ""),
"dashboards:delete": newMapping(RelationDelete, ""),
// Action sets
"dashboards:view": newMapping(RelationSetView, ""),
"dashboards:edit": newMapping(RelationSetEdit, ""),
"dashboards:admin": newMapping(RelationSetAdmin, ""),
},
},
}
@@ -350,6 +350,14 @@ func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey {
}
}
func NewTuple(subject, relation, object string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
Object: object,
}
}
func ToAuthzExtTupleKey(t *openfgav1.TupleKey) *authzextv1.TupleKey {
tupleKey := &authzextv1.TupleKey{
User: t.GetUser(),
@@ -12,8 +12,9 @@ import (
type OperationGroup string
const (
OperationGroupFolder OperationGroup = "folder"
OperationGroupPermission OperationGroup = "permission"
OperationGroupFolder OperationGroup = "folder"
OperationGroupPermission OperationGroup = "permission"
OperationGroupUserOrgRole OperationGroup = "user_org_role"
)
func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) {
@@ -58,6 +59,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
if err := s.mutateResourcePermissions(ctx, storeInf, operations); err != nil {
return nil, fmt.Errorf("failed to mutate resource permissions: %w", err)
}
case OperationGroupUserOrgRole:
if err := s.mutateOrgRoles(ctx, storeInf, operations); err != nil {
return nil, fmt.Errorf("failed to mutate org roles: %w", err)
}
default:
s.logger.Warn("unsupported operation group", "operationGroup", operationGroup)
}
@@ -72,6 +77,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e
return OperationGroupFolder, nil
case *authzextv1.MutateOperation_CreatePermission, *authzextv1.MutateOperation_DeletePermission:
return OperationGroupPermission, nil
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole:
return OperationGroupUserOrgRole, nil
}
return OperationGroup(""), errors.New("unsupported mutate operation type")
}
@@ -0,0 +1,84 @@
package server
import (
"context"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error {
ctx, span := s.tracer.Start(ctx, "server.mutateOrgRoles")
defer span.End()
writeTuples := make([]*openfgav1.TupleKey, 0)
deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
for _, operation := range operations {
switch op := operation.Operation.(type) {
case *authzextv1.MutateOperation_UpdateUserOrgRole:
tuple, err := s.getUserOrgRoleWriteTuple(ctx, store, op.UpdateUserOrgRole)
if err != nil {
return err
}
writeTuples = append(writeTuples, tuple)
case *authzextv1.MutateOperation_DeleteUserOrgRole:
tuple, err := s.getUserOrgRoleDeleteTuple(ctx, store, op.DeleteUserOrgRole)
if err != nil {
return err
}
deleteTuples = append(deleteTuples, tuple)
default:
s.logger.Debug("unsupported mutate operation", "operation", op)
}
}
if len(writeTuples) == 0 && len(deleteTuples) == 0 {
return nil
}
writeReq := &openfgav1.WriteRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
}
if len(writeTuples) > 0 {
writeReq.Writes = &openfgav1.WriteRequestWrites{
TupleKeys: writeTuples,
OnDuplicate: "ignore",
}
}
if len(deleteTuples) > 0 {
writeReq.Deletes = &openfgav1.WriteRequestDeletes{
TupleKeys: deleteTuples,
OnMissing: "ignore",
}
}
_, err := s.openfga.Write(ctx, writeReq)
if err != nil {
s.logger.Error("failed to write user org role tuples", "error", err)
return err
}
return nil
}
func (s *Server) getUserOrgRoleWriteTuple(ctx context.Context, store *storeInfo, req *authzextv1.UpdateUserOrgRoleOperation) (*openfgav1.TupleKey, error) {
basicRole := zanzana.TranslateBasicRole(req.GetRole())
return &openfgav1.TupleKey{
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
}, nil
}
func (s *Server) getUserOrgRoleDeleteTuple(ctx context.Context, store *storeInfo, req *authzextv1.DeleteUserOrgRoleOperation) (*openfgav1.TupleKeyWithoutCondition, error) {
basicRole := zanzana.TranslateBasicRole(req.GetRole())
return &openfgav1.TupleKeyWithoutCondition{
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
Relation: zanzana.RelationAssignee,
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
}, nil
}
@@ -0,0 +1,72 @@
package server
import (
"testing"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/stretchr/testify/require"
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
func setupMutateOrgRoles(t *testing.T, srv *Server) *Server {
t.Helper()
// seed tuples
tuples := []*openfgav1.TupleKey{
common.NewTuple("user:1", common.RelationAssignee, "role:basic_editor"),
}
return setupOpenFGADatabase(t, srv, tuples)
}
func testMutateOrgRoles(t *testing.T, srv *Server) {
setupMutateOrgRoles(t, srv)
t.Run("should update user org role and delete old role", func(t *testing.T) {
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
Namespace: "default",
Operations: []*v1.MutateOperation{
{
Operation: &v1.MutateOperation_UpdateUserOrgRole{
UpdateUserOrgRole: &v1.UpdateUserOrgRoleOperation{
User: "1",
Role: "Admin",
},
},
},
{
Operation: &v1.MutateOperation_DeleteUserOrgRole{
DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{
User: "1",
Role: "Editor",
},
},
},
},
})
require.NoError(t, err)
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Relation: common.RelationAssignee,
Object: "role:basic_admin",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 1)
require.Equal(t, "user:1", res.Tuples[0].Key.User)
res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Relation: common.RelationAssignee,
Object: "role:basic_editor",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 0)
})
}
@@ -128,6 +128,10 @@ func TestIntegrationServer(t *testing.T) {
t.Run("test mutate resource permissions", func(t *testing.T) {
testMutateResourcePermissions(t, srv)
})
t.Run("test mutate org roles", func(t *testing.T) {
testMutateOrgRoles(t, srv)
})
}
func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
+3 -2
View File
@@ -1516,10 +1516,11 @@ var (
},
{
Name: "newLogsPanel",
Description: "Enables the new logs panel in Explore",
Stage: FeatureStageExperimental,
Description: "Enables the new logs panel",
Stage: FeatureStageGeneralAvailability,
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
Expression: "true",
},
{
Name: "grafanaconThemes",
+1 -1
View File
@@ -198,7 +198,7 @@ grafanaAdvisor,privatePreview,@grafana/plugins-platform-backend,false,false,fals
elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false
datasourceConnectionsTab,privatePreview,@grafana/plugins-platform-backend,false,false,true
fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false
newLogsPanel,experimental,@grafana/observability-logs,false,false,true
newLogsPanel,GA,@grafana/observability-logs,false,false,true
grafanaconThemes,GA,@grafana/grafana-frontend-platform,false,true,false
alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true
alertingUseNewSimplifiedRoutingHashAlgorithm,preview,@grafana/alerting-squad,false,true,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
198 elasticsearchImprovedParsing experimental @grafana/aws-datasources false false false
199 datasourceConnectionsTab privatePreview @grafana/plugins-platform-backend false false true
200 fetchRulesUsingPost experimental @grafana/alerting-squad false false false
201 newLogsPanel experimental GA @grafana/observability-logs false false true
202 grafanaconThemes GA @grafana/grafana-frontend-platform false true false
203 alertingJiraIntegration experimental @grafana/alerting-squad false false true
204 alertingUseNewSimplifiedRoutingHashAlgorithm preview @grafana/alerting-squad false true false
+1 -1
View File
@@ -804,7 +804,7 @@ const (
FlagFetchRulesUsingPost = "fetchRulesUsingPost"
// FlagNewLogsPanel
// Enables the new logs panel in Explore
// Enables the new logs panel
FlagNewLogsPanel = "newLogsPanel"
// FlagGrafanaconThemes
+9 -5
View File
@@ -2775,14 +2775,18 @@
{
"metadata": {
"name": "newLogsPanel",
"resourceVersion": "1753448760331",
"creationTimestamp": "2025-02-04T17:40:17Z"
"resourceVersion": "1762166984808",
"creationTimestamp": "2025-02-04T17:40:17Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-11-03 10:49:44.808226 +0000 UTC"
}
},
"spec": {
"description": "Enables the new logs panel in Explore",
"stage": "experimental",
"description": "Enables the new logs panel",
"stage": "GA",
"codeowner": "@grafana/observability-logs",
"frontend": true
"frontend": true,
"expression": "true"
}
},
{
@@ -105,4 +105,32 @@ describe('ScopesNavigationTreeLink', () => {
expect(matchingLink).toHaveAttribute('aria-current', 'page');
expect(otherLink).not.toHaveAttribute('aria-current');
});
it('shows correct icon for grafana-metricsdrilldown-app with query parameters', () => {
renderWithRouter(
<ScopesNavigationTreeLink
to="/a/grafana-metricsdrilldown-app?from=now-1h&to=now"
title="Metrics Drilldown"
id="metrics-drilldown"
/>
);
const link = screen.getByTestId('scopes-dashboards-metrics-drilldown');
// Icon should be rendered (check for SVG element which is how Icon renders)
const icon = link.querySelector('svg');
expect(icon).toBeInTheDocument();
// The link should contain the title text
expect(link).toHaveTextContent('Metrics Drilldown');
});
it('shows correct icon for grafana-metricsdrilldown-app without trailing slash', () => {
renderWithRouter(
<ScopesNavigationTreeLink to="/a/grafana-metricsdrilldown-app" title="Metrics Drilldown" id="metrics-drilldown" />
);
const link = screen.getByTestId('scopes-dashboards-metrics-drilldown');
const icon = link.querySelector('svg');
expect(icon).toBeInTheDocument();
expect(link).toHaveTextContent('Metrics Drilldown');
});
});
@@ -5,7 +5,7 @@ import { Link, useLocation } from 'react-router-dom-v5-compat';
import { GrafanaTheme2, IconName, locationUtil } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { isCurrentPath } from './scopeNavgiationUtils';
import { isCurrentPath, normalizePath } from './scopeNavgiationUtils';
export interface ScopesNavigationTreeLinkProps {
to: string;
@@ -36,25 +36,30 @@ export function ScopesNavigationTreeLink({ to, title, id }: ScopesNavigationTree
}
function getLinkIcon(to: string) {
// Strip base URL and normalize path
const normalizedPath = locationUtil.stripBaseFromUrl(to);
for (const [key, value] of linkMap.entries()) {
if (normalizedPath.startsWith(key)) {
return value;
}
// Check for external links before stripping base (stripBaseFromUrl removes http:// for same-origin URLs)
if (to.startsWith('http')) {
return 'external-link-alt';
}
return 'link';
// Strip base URL and normalize path (remove query params and hash)
const baseStripped = locationUtil.stripBaseFromUrl(to);
const normalizedPath = normalizePath(baseStripped);
// Check for dashboard paths with startsWith (e.g., /d/dashboard-id)
if (normalizedPath.startsWith('/d')) {
return 'apps';
}
// Use direct Map lookup for exact path matches
return linkMap.get(normalizedPath) ?? 'link';
}
const linkMap = new Map<string, IconName>([
['http', 'external-link-alt'],
['/d', 'apps'],
['/explore/metrics', 'drilldown'],
['/a/grafana-metricsdrilldown-app/', 'drilldown'],
['/a/grafana-lokiexplore-app/', 'drilldown'],
['/a/grafana-exploretraces-app/', 'drilldown'],
['/a/grafana-pyroscope-app/', 'drilldown'],
['/a/grafana-metricsdrilldown-app', 'drilldown'],
['/a/grafana-lokiexplore-app', 'drilldown'],
['/a/grafana-exploretraces-app', 'drilldown'],
['/a/grafana-pyroscope-app', 'drilldown'],
]);
const getStyles = (theme: GrafanaTheme2) => {