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

This commit is contained in:
Mariell Hoversholm
2025-02-11 14:42:49 +01:00
63 changed files with 1048 additions and 1928 deletions
+6 -2
View File
@@ -632,6 +632,9 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
],
"packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
@@ -3697,9 +3700,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"]
],
"public/app/features/dashboard/api/v0.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
+1
View File
@@ -1648,6 +1648,7 @@ github.com/ionos-cloud/sdk-go/v6 v6.1.11/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLj
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk=
github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo=
-51
View File
@@ -1,51 +0,0 @@
package kind
name: "AccessPolicy"
maturity: "merged"
description: "Access rules for a scope+role. NOTE there is a unique constraint on role+scope"
pluralName: "AccessPolicies"
machineName: "accesspolicy"
pluralMachineName: "accesspolicies"
lineage: schemas: [{
version: [0, 0]
schema: {
spec: {
// The scope where these policies should apply
scope: #ResourceRef
// The role that must apply this policy
role: #RoleRef
// The set of rules to apply. Note that * is required to modify
// access policy rules, and that "none" will reject all actions
rules: [...#AccessRule]
} @cuetsy(kind="interface")
#RoleRef: {
// Policies can apply to roles, teams, or users
// Applying policies to individual users is supported, but discouraged
kind: "Role" | "BuiltinRole" | "Team" | "User"
name: string
xname: string // temporary
} @cuetsy(kind="interface")
#ResourceRef: {
kind: string // explicit resource or folder will cascade
name: string
} @cuetsy(kind="interface")
#AccessRule: {
// The kind this rule applies to (dashboards, alert, etc)
kind: "*" | string
// READ, WRITE, CREATE, DELETE, ...
// should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete"
verb: "*" | "none" | string
// Specific sub-elements like "alert.rules" or "dashboard.permissions"????
target?: string
} @cuetsy(kind="interface")
}
},
]
-25
View File
@@ -1,25 +0,0 @@
package kind
name: "Role"
maturity: "merged"
description: "Roles represent a set of users+teams that should share similar access"
lineage: schemas: [{
version: [0, 0]
schema: {
spec: {
// The role identifier `managed:builtins:editor:permissions`
name: string
// Optional display
displayName?: string
// Name of the team.
groupName?: string
// Role description
description?: string
// Do not show this role
hidden: bool | false
} @cuetsy(kind="interface")
}
},
]
-36
View File
@@ -1,36 +0,0 @@
package kind
name: "RoleBinding"
maturity: "merged"
description: "Role bindings links a user|team to a configured role"
lineage: schemas: [{
version: [0, 0]
schema: {
spec: {
// The role we are discussing
role: #BuiltinRoleRef | #CustomRoleRef
// The team or user that has the specified role
subject: #RoleBindingSubject
} @cuetsy(kind="interface")
#CustomRoleRef: {
kind: "Role"
name: string
} @cuetsy(kind="interface")
#BuiltinRoleRef: {
kind: "BuiltinRole"
name: "viewer" | "editor" | "admin"
} @cuetsy(kind="interface")
#RoleBindingSubject: {
kind: "Team" | "User"
// The team/user identifier name
name: string
} @cuetsy(kind="interface")
}
},
]
+2 -2
View File
@@ -276,8 +276,8 @@
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/saga-icons": "workspace:*",
"@grafana/scenes": "5.41.2",
"@grafana/scenes-react": "5.41.2",
"@grafana/scenes": "5.42.0",
"@grafana/scenes-react": "5.42.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
-22
View File
@@ -7,17 +7,6 @@
//
// Run 'make gen-cue' from repository root to regenerate.
// Raw generated types from AccessPolicy kind.
export type {
AccessPolicy,
RoleRef,
ResourceRef,
AccessRule
} from './raw/accesspolicy/x/accesspolicy_types.gen';
// Raw generated enums and default consts from accesspolicy kind.
export { defaultAccessPolicy } from './raw/accesspolicy/x/accesspolicy_types.gen';
// Raw generated types from Dashboard kind.
export type {
AnnotationTarget,
@@ -135,14 +124,3 @@ export { defaultNavbarPreference } from './raw/preferences/x/preferences_types.g
// Raw generated types from PublicDashboard kind.
export type { PublicDashboard } from './raw/publicdashboard/x/publicdashboard_types.gen';
// Raw generated types from Role kind.
export type { Role } from './raw/role/x/role_types.gen';
// Raw generated types from RoleBinding kind.
export type {
RoleBinding,
CustomRoleRef,
BuiltinRoleRef,
RoleBindingSubject
} from './raw/rolebinding/x/rolebinding_types.gen';
@@ -1,60 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// TSTypesJenny
// LatestMajorsOrXJenny
//
// Run 'make gen-cue' from repository root to regenerate.
export interface RoleRef {
/**
* Policies can apply to roles, teams, or users
* Applying policies to individual users is supported, but discouraged
*/
kind: ('Role' | 'BuiltinRole' | 'Team' | 'User');
name: string;
xname: string; // temporary
}
export interface ResourceRef {
kind: string; // explicit resource or folder will cascade
name: string;
}
export interface AccessRule {
/**
* The kind this rule applies to (dashboards, alert, etc)
*/
kind: ('*' | string);
/**
* Specific sub-elements like "alert.rules" or "dashboard.permissions"????
*/
target?: string;
/**
* READ, WRITE, CREATE, DELETE, ...
* should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete"
*/
verb: ('*' | 'none' | string);
}
export interface AccessPolicy {
/**
* The role that must apply this policy
*/
role: RoleRef;
/**
* The set of rules to apply. Note that * is required to modify
* access policy rules, and that "none" will reject all actions
*/
rules: Array<AccessRule>;
/**
* The scope where these policies should apply
*/
scope: ResourceRef;
}
export const defaultAccessPolicy: Partial<AccessPolicy> = {
rules: [],
};
@@ -1,32 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// TSTypesJenny
// LatestMajorsOrXJenny
//
// Run 'make gen-cue' from repository root to regenerate.
export interface Role {
/**
* Role description
*/
description?: string;
/**
* Optional display
*/
displayName?: string;
/**
* Name of the team.
*/
groupName?: string;
/**
* Do not show this role
*/
hidden: (boolean | false);
/**
* The role identifier `managed:builtins:editor:permissions`
*/
name: string;
}
@@ -1,38 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// TSTypesJenny
// LatestMajorsOrXJenny
//
// Run 'make gen-cue' from repository root to regenerate.
export interface CustomRoleRef {
kind: 'Role';
name: string;
}
export interface BuiltinRoleRef {
kind: 'BuiltinRole';
name: ('viewer' | 'editor' | 'admin');
}
export interface RoleBindingSubject {
kind: ('Team' | 'User');
/**
* The team/user identifier name
*/
name: string;
}
export interface RoleBinding {
/**
* The role we are discussing
*/
role: (BuiltinRoleRef | CustomRoleRef);
/**
* The team or user that has the specified role
*/
subject: RoleBindingSubject;
}
@@ -467,7 +467,7 @@ TimeSettingsSpec: {
// Whether timepicker is visible or not.
hideTimepicker: bool // v1: timepicker.hidden
// Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday".
weekStart: string
weekStart?: "saturday" | "monday" | "sunday"
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth: int
// Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
@@ -644,7 +644,7 @@ export interface TimeSettingsSpec {
// v1: timepicker.hidden
hideTimepicker: boolean;
// Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday".
weekStart: string;
weekStart?: "saturday" | "monday" | "sunday";
// The month that the fiscal year starts on. 0 = January, 11 = December
fiscalYearStartMonth: number;
// Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.
@@ -681,7 +681,6 @@ export const defaultTimeSettingsSpec = (): TimeSettingsSpec => ({
"30d",
],
hideTimepicker: false,
weekStart: "",
fiscalYearStartMonth: 0,
});
@@ -243,7 +243,19 @@ describe('Combobox', () => {
await userEvent.keyboard('{Enter}');
expect(screen.getByDisplayValue('Use custom value')).toBeInTheDocument();
expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: 'Use custom value' }));
expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ description: 'Use custom value' }));
});
it('should not allow creating a custom value when it is an existing value', async () => {
const onChangeHandler = jest.fn();
render(<Combobox options={options} value={null} onChange={onChangeHandler} createCustomValue />);
const input = screen.getByRole('combobox');
await userEvent.type(input, '4');
await userEvent.keyboard('{Enter}');
expect(screen.queryByDisplayValue('Use custom value')).not.toBeInTheDocument();
expect(screen.getByDisplayValue('Option 4')).toBeInTheDocument();
expect(onChangeHandler).toHaveBeenCalledWith(expect.objectContaining({ value: '4' }));
expect(onChangeHandler).not.toHaveBeenCalledWith(expect.objectContaining({ description: 'Use custom value' }));
});
it('should provide custom string when all options are numbers', async () => {
@@ -129,9 +129,9 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
let itemsToSet = items;
logOptions(itemsToSet.length, RECOMMENDED_ITEMS_AMOUNT, id, ariaLabelledBy);
if (inputValue && createCustomValue) {
const optionMatchingInput = items.find(
(opt) => opt.label === 'Custom value: ' + inputValue || opt.value === inputValue
);
//Since the label of a normal option does not have to match its value and a custom option has the same value and label,
//we just focus on the value to check if the option already exists
const optionMatchingInput = items.find((opt) => opt.value === inputValue);
if (!optionMatchingInput) {
const customValueOption = {
@@ -62,6 +62,8 @@ export function useOptions<T extends string | number>(rawOptions: AsyncOptions<T
(opts: Array<ComboboxOption<T>>) => {
let currentOptions: Array<ComboboxOption<T>> = opts;
if (createCustomValue && userTypedSearch) {
//Since the label of a normal option does not have to match its value and a custom option has the same value and label,
//we just focus on the value to check if the option already exists
const customValueExists = opts.some((opt) => opt.value === userTypedSearch);
if (!customValueExists) {
currentOptions = [
@@ -7,7 +7,7 @@ import { Combobox } from '../Combobox/Combobox';
import { ComboboxOption } from '../Combobox/types';
export interface Props {
onChange: (weekStart: string) => void;
onChange: (weekStart: WeekStart) => void;
value: string;
width?: number;
autoFocus?: boolean;
@@ -57,7 +57,7 @@ export const WeekStartPicker = (props: Props) => {
const onChangeWeekStart = useCallback(
(selectable: ComboboxOption | null) => {
if (selectable && selectable.value !== undefined) {
onChange(selectable.value);
onChange(selectable.value as WeekStart);
}
},
[onChange]
+1 -2
View File
@@ -27,7 +27,6 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -465,7 +464,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
acSvc := acimpl.ProvideOSSService(
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn,
features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn,
)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets)
@@ -1,43 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package accesspolicy
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/kinds"
)
// Resource is the kubernetes style representation of AccessPolicy. (TODO be better)
type K8sResource = kinds.GrafanaResource[Spec, Status]
// NewResource creates a new instance of the resource with a given name (UID)
func NewK8sResource(name string, s *Spec) K8sResource {
return K8sResource{
TypeMeta: v1.TypeMeta{
Kind: "AccessPolicy",
APIVersion: "v0-0-alpha",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Annotations: make(map[string]string),
Labels: make(map[string]string),
},
Spec: s,
}
}
// Resource is the wire representation of AccessPolicy.
// It currently will soon be merged into the k8s flavor (TODO be better)
type Resource struct {
Metadata Metadata `json:"metadata"`
Spec Spec `json:"spec"`
Status Status `json:"status"`
}
@@ -1,42 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package accesspolicy
import (
"time"
)
// Metadata defines model for Metadata.
type Metadata struct {
CreatedBy string `json:"createdBy"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
// extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata
ExtraFields map[string]any `json:"extraFields"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
UpdateTimestamp time.Time `json:"updateTimestamp"`
UpdatedBy string `json:"updatedBy"`
}
// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field.
// It is not exhaustive and only includes fields which may be relevant to a kind's implementation,
// As it is also intended to be generic enough to function with any API Server.
type KubeObjectMetadata struct {
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
}
@@ -1,79 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoResourceTypes
//
// Run 'make gen-cue' from repository root to regenerate.
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package accesspolicy
type ResourceRef struct {
// explicit resource or folder will cascade
Kind string `json:"kind"`
Name string `json:"name"`
}
// NewResourceRef creates a new ResourceRef object.
func NewResourceRef() *ResourceRef {
return &ResourceRef{}
}
type RoleRef struct {
// Policies can apply to roles, teams, or users
// Applying policies to individual users is supported, but discouraged
Kind RoleRefKind `json:"kind"`
Name string `json:"name"`
// temporary
Xname string `json:"xname"`
}
// NewRoleRef creates a new RoleRef object.
func NewRoleRef() *RoleRef {
return &RoleRef{}
}
type AccessRule struct {
// The kind this rule applies to (dashboards, alert, etc)
Kind string `json:"kind"`
// READ, WRITE, CREATE, DELETE, ...
// should move to k8s style verbs like: "get", "list", "watch", "create", "update", "patch", "delete"
Verb string `json:"verb"`
// Specific sub-elements like "alert.rules" or "dashboard.permissions"????
Target *string `json:"target,omitempty"`
}
// NewAccessRule creates a new AccessRule object.
func NewAccessRule() *AccessRule {
return &AccessRule{}
}
type Spec struct {
// The scope where these policies should apply
Scope ResourceRef `json:"scope"`
// The role that must apply this policy
Role RoleRef `json:"role"`
// The set of rules to apply. Note that * is required to modify
// access policy rules, and that "none" will reject all actions
Rules []AccessRule `json:"rules"`
}
// NewSpec creates a new Spec object.
func NewSpec() *Spec {
return &Spec{
Scope: *NewResourceRef(),
Role: *NewRoleRef(),
}
}
type RoleRefKind string
const (
RoleRefKindRole RoleRefKind = "Role"
RoleRefKindBuiltinRole RoleRefKind = "BuiltinRole"
RoleRefKindTeam RoleRefKind = "Team"
RoleRefKindUser RoleRefKind = "User"
)
@@ -1,74 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package accesspolicy
// Defines values for OperatorStateState.
const (
OperatorStateStateFailed OperatorStateState = "failed"
OperatorStateStateInProgress OperatorStateState = "in_progress"
OperatorStateStateSuccess OperatorStateState = "success"
)
// Defines values for StatusOperatorStateState.
const (
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
)
// OperatorState defines model for OperatorState.
type OperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State OperatorStateState `json:"state"`
}
// OperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type OperatorStateState string
// Status defines model for Status.
type Status struct {
// additionalFields is reserved for future use
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
}
// StatusOperatorState defines model for status.#OperatorState.
type StatusOperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StatusOperatorStateState `json:"state"`
}
// StatusOperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type StatusOperatorStateState string
-99
View File
@@ -1,99 +0,0 @@
package accesspolicy
import (
"sort"
"github.com/grafana/grafana/pkg/util"
)
const PermissionsTarget = "permissions"
const AllowAll = "*"
const AllowNone = "none"
func ReduceRules(rules []AccessRule) []AccessRule {
type verbs struct {
Verb map[string][]string
Terminal string
}
kinds := make(map[string]*verbs)
for _, rule := range rules {
if rule.Kind == "" || rule.Verb == "" {
continue // invalid
}
// flip write permission to *
if rule.Target != nil && *rule.Target == PermissionsTarget {
if rule.Verb == "write" {
rule.Verb = AllowAll
}
}
kind, ok := kinds[rule.Kind]
if !ok {
kind = &verbs{
Verb: make(map[string][]string),
}
kinds[rule.Kind] = kind
}
terminal := rule.Verb == AllowAll || rule.Verb == AllowNone
if terminal {
if rule.Kind == AllowAll {
return []AccessRule{rule}
}
kind.Terminal = rule.Verb
} else if kind.Terminal == "" {
targets, ok := kind.Verb[rule.Verb]
if !ok {
targets = []string{}
}
if rule.Target != nil && !contains(targets, *rule.Target) {
targets = append(targets, *rule.Target)
sort.Strings(targets)
}
kind.Verb[rule.Verb] = targets
}
}
results := make([]AccessRule, 0)
for _, kind := range getSortedKeys(kinds) {
verb := kinds[kind]
if verb.Terminal != "" {
results = append(results, AccessRule{Kind: kind, Verb: verb.Terminal})
} else {
for _, v := range getSortedKeys(verb.Verb) {
targets := verb.Verb[v]
if len(targets) == 0 {
results = append(results, AccessRule{Kind: kind, Verb: v})
} else {
for _, t := range targets {
results = append(results, AccessRule{
Kind: kind,
Verb: v,
Target: util.Pointer(t),
})
}
}
}
}
}
return results
}
func getSortedKeys[T any](vals map[string]T) []string {
keys := make([]string, 0, len(vals))
for k := range vals {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func contains[T comparable](s []T, e T) bool {
for _, v := range s {
if v == e {
return true
}
}
return false
}
-68
View File
@@ -1,68 +0,0 @@
package accesspolicy
import (
"encoding/json"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/require"
)
func TestRuleReducer(t *testing.T) {
t.Run("Check write pointer becomes star", func(t *testing.T) {
rules := ReduceRules([]AccessRule{
{Kind: "dashboard", Verb: "read"},
{Kind: "dashboard", Verb: "write", Target: util.Pointer("permissions")},
{Kind: "dashboard", Verb: "read"},
})
require.Len(t, rules, 1)
require.Equal(t, rules[0], AccessRule{Kind: "dashboard", Verb: "*"})
})
t.Run("Check sort", func(t *testing.T) {
rules := ReduceRules([]AccessRule{
{Kind: "x", Verb: "b"},
{Kind: "x", Verb: "a"},
{Kind: "x", Verb: "a"}, // ignore duplicates
{Kind: "x", Verb: "a"}, // ignore duplicates
{Kind: "x", Verb: "a"}, // ignore duplicates
{Kind: "x", Verb: "a"},
{Kind: "z", Verb: "b"},
{Kind: "AAA", Verb: ""}, // ignore
{Kind: "", Verb: "XXX"}, // ignore
{Kind: "z", Verb: "a"},
{Kind: "y", Verb: "b"},
{Kind: "y", Verb: "a"},
})
out, err := json.MarshalIndent(rules, "", " ")
fmt.Printf("%s", string(out))
require.NoError(t, err)
require.JSONEq(t, `[
{
"kind": "x",
"verb": "a"
},
{
"kind": "x",
"verb": "b"
},
{
"kind": "y",
"verb": "a"
},
{
"kind": "y",
"verb": "b"
},
{
"kind": "z",
"verb": "a"
},
{
"kind": "z",
"verb": "b"
}
]`, string(out))
})
}
-43
View File
@@ -1,43 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package role
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/kinds"
)
// Resource is the kubernetes style representation of Role. (TODO be better)
type K8sResource = kinds.GrafanaResource[Spec, Status]
// NewResource creates a new instance of the resource with a given name (UID)
func NewK8sResource(name string, s *Spec) K8sResource {
return K8sResource{
TypeMeta: v1.TypeMeta{
Kind: "Role",
APIVersion: "v0-0-alpha",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Annotations: make(map[string]string),
Labels: make(map[string]string),
},
Spec: s,
}
}
// Resource is the wire representation of Role.
// It currently will soon be merged into the k8s flavor (TODO be better)
type Resource struct {
Metadata Metadata `json:"metadata"`
Spec Spec `json:"spec"`
Status Status `json:"status"`
}
-42
View File
@@ -1,42 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package role
import (
"time"
)
// Metadata defines model for Metadata.
type Metadata struct {
CreatedBy string `json:"createdBy"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
// extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata
ExtraFields map[string]any `json:"extraFields"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
UpdateTimestamp time.Time `json:"updateTimestamp"`
UpdatedBy string `json:"updatedBy"`
}
// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field.
// It is not exhaustive and only includes fields which may be relevant to a kind's implementation,
// As it is also intended to be generic enough to function with any API Server.
type KubeObjectMetadata struct {
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
}
-30
View File
@@ -1,30 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoResourceTypes
//
// Run 'make gen-cue' from repository root to regenerate.
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package role
type Spec struct {
// The role identifier `managed:builtins:editor:permissions`
Name string `json:"name"`
// Optional display
DisplayName *string `json:"displayName,omitempty"`
// Name of the team.
GroupName *string `json:"groupName,omitempty"`
// Role description
Description *string `json:"description,omitempty"`
// Do not show this role
Hidden bool `json:"hidden"`
}
// NewSpec creates a new Spec object.
func NewSpec() *Spec {
return &Spec{}
}
-74
View File
@@ -1,74 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package role
// Defines values for OperatorStateState.
const (
OperatorStateStateFailed OperatorStateState = "failed"
OperatorStateStateInProgress OperatorStateState = "in_progress"
OperatorStateStateSuccess OperatorStateState = "success"
)
// Defines values for StatusOperatorStateState.
const (
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
)
// OperatorState defines model for OperatorState.
type OperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State OperatorStateState `json:"state"`
}
// OperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type OperatorStateState string
// Status defines model for Status.
type Status struct {
// additionalFields is reserved for future use
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
}
// StatusOperatorState defines model for status.#OperatorState.
type StatusOperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StatusOperatorStateState `json:"state"`
}
// StatusOperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type StatusOperatorStateState string
-43
View File
@@ -1,43 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package rolebinding
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/kinds"
)
// Resource is the kubernetes style representation of RoleBinding. (TODO be better)
type K8sResource = kinds.GrafanaResource[Spec, Status]
// NewResource creates a new instance of the resource with a given name (UID)
func NewK8sResource(name string, s *Spec) K8sResource {
return K8sResource{
TypeMeta: v1.TypeMeta{
Kind: "RoleBinding",
APIVersion: "v0-0-alpha",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Annotations: make(map[string]string),
Labels: make(map[string]string),
},
Spec: s,
}
}
// Resource is the wire representation of RoleBinding.
// It currently will soon be merged into the k8s flavor (TODO be better)
type Resource struct {
Metadata Metadata `json:"metadata"`
Spec Spec `json:"spec"`
Status Status `json:"status"`
}
@@ -1,42 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package rolebinding
import (
"time"
)
// Metadata defines model for Metadata.
type Metadata struct {
CreatedBy string `json:"createdBy"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
// extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata
ExtraFields map[string]any `json:"extraFields"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
UpdateTimestamp time.Time `json:"updateTimestamp"`
UpdatedBy string `json:"updatedBy"`
}
// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field.
// It is not exhaustive and only includes fields which may be relevant to a kind's implementation,
// As it is also intended to be generic enough to function with any API Server.
type KubeObjectMetadata struct {
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
}
@@ -1,144 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoResourceTypes
//
// Run 'make gen-cue' from repository root to regenerate.
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package rolebinding
import (
json "encoding/json"
errors "errors"
fmt "fmt"
)
type BuiltinRoleRef struct {
Kind string `json:"kind"`
Name BuiltinRoleRefName `json:"name"`
}
// NewBuiltinRoleRef creates a new BuiltinRoleRef object.
func NewBuiltinRoleRef() *BuiltinRoleRef {
return &BuiltinRoleRef{
Kind: "BuiltinRole",
}
}
type CustomRoleRef struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
// NewCustomRoleRef creates a new CustomRoleRef object.
func NewCustomRoleRef() *CustomRoleRef {
return &CustomRoleRef{
Kind: "Role",
}
}
type RoleBindingSubject struct {
Kind RoleBindingSubjectKind `json:"kind"`
// The team/user identifier name
Name string `json:"name"`
}
// NewRoleBindingSubject creates a new RoleBindingSubject object.
func NewRoleBindingSubject() *RoleBindingSubject {
return &RoleBindingSubject{}
}
type Spec struct {
// The role we are discussing
Role BuiltinRoleRefOrCustomRoleRef `json:"role"`
// The team or user that has the specified role
Subject RoleBindingSubject `json:"subject"`
}
// NewSpec creates a new Spec object.
func NewSpec() *Spec {
return &Spec{
Role: *NewBuiltinRoleRefOrCustomRoleRef(),
Subject: *NewRoleBindingSubject(),
}
}
type BuiltinRoleRefName string
const (
BuiltinRoleRefNameViewer BuiltinRoleRefName = "viewer"
BuiltinRoleRefNameEditor BuiltinRoleRefName = "editor"
BuiltinRoleRefNameAdmin BuiltinRoleRefName = "admin"
)
type RoleBindingSubjectKind string
const (
RoleBindingSubjectKindTeam RoleBindingSubjectKind = "Team"
RoleBindingSubjectKindUser RoleBindingSubjectKind = "User"
)
type BuiltinRoleRefOrCustomRoleRef struct {
BuiltinRoleRef *BuiltinRoleRef `json:"BuiltinRoleRef,omitempty"`
CustomRoleRef *CustomRoleRef `json:"CustomRoleRef,omitempty"`
}
// NewBuiltinRoleRefOrCustomRoleRef creates a new BuiltinRoleRefOrCustomRoleRef object.
func NewBuiltinRoleRefOrCustomRoleRef() *BuiltinRoleRefOrCustomRoleRef {
return &BuiltinRoleRefOrCustomRoleRef{}
}
// MarshalJSON implements a custom JSON marshalling logic to encode `BuiltinRoleRefOrCustomRoleRef` as JSON.
func (resource BuiltinRoleRefOrCustomRoleRef) MarshalJSON() ([]byte, error) {
if resource.BuiltinRoleRef != nil {
return json.Marshal(resource.BuiltinRoleRef)
}
if resource.CustomRoleRef != nil {
return json.Marshal(resource.CustomRoleRef)
}
return nil, fmt.Errorf("no value for disjunction of refs")
}
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `BuiltinRoleRefOrCustomRoleRef` from JSON.
func (resource *BuiltinRoleRefOrCustomRoleRef) UnmarshalJSON(raw []byte) error {
if raw == nil {
return nil
}
// FIXME: this is wasteful, we need to find a more efficient way to unmarshal this.
parsedAsMap := make(map[string]any)
if err := json.Unmarshal(raw, &parsedAsMap); err != nil {
return err
}
discriminator, found := parsedAsMap["kind"]
if !found {
return errors.New("discriminator field 'kind' not found in payload")
}
switch discriminator {
case "BuiltinRole":
var builtinRoleRef BuiltinRoleRef
if err := json.Unmarshal(raw, &builtinRoleRef); err != nil {
return err
}
resource.BuiltinRoleRef = &builtinRoleRef
return nil
case "Role":
var customRoleRef CustomRoleRef
if err := json.Unmarshal(raw, &customRoleRef); err != nil {
return err
}
resource.CustomRoleRef = &customRoleRef
return nil
}
return fmt.Errorf("could not unmarshal resource with `kind = %v`", discriminator)
}
@@ -1,74 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// K8ResourcesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package rolebinding
// Defines values for OperatorStateState.
const (
OperatorStateStateFailed OperatorStateState = "failed"
OperatorStateStateInProgress OperatorStateState = "in_progress"
OperatorStateStateSuccess OperatorStateState = "success"
)
// Defines values for StatusOperatorStateState.
const (
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
)
// OperatorState defines model for OperatorState.
type OperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State OperatorStateState `json:"state"`
}
// OperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type OperatorStateState string
// Status defines model for Status.
type Status struct {
// additionalFields is reserved for future use
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
}
// StatusOperatorState defines model for status.#OperatorState.
type StatusOperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StatusOperatorStateState `json:"state"`
}
// StatusOperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type StatusOperatorStateState string
@@ -10,7 +10,7 @@ import (
"github.com/grafana/grafana/pkg/registry"
apiregistry "github.com/grafana/grafana/pkg/registry/apis"
appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/auth"
@@ -66,7 +66,7 @@ func ProvideBackgroundServiceRegistry(
ssoSettings *ssosettingsimpl.Service,
pluginExternal *pluginexternal.Service,
pluginInstaller *plugininstaller.Service,
accessControl accesscontrol.Service,
zanzanaReconciler *dualwrite.ZanzanaReconciler,
appRegistry *appregistry.Service,
// Need to make sure these are initialized, is there a better place to put them?
_ dashboardsnapshots.Service,
@@ -111,7 +111,7 @@ func ProvideBackgroundServiceRegistry(
ssoSettings,
pluginExternal,
pluginInstaller,
accessControl,
zanzanaReconciler,
appRegistry,
)
}
-27
View File
@@ -30,15 +30,6 @@ func GetCoreKinds() ([]CoreKind, error) {
_, caller, _, _ := runtime.Caller(0)
root := filepath.Join(caller, "../../../..")
accesspolicyCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/accesspolicy/access_policy_kind.cue"))
if err != nil {
return nil, err
}
kinds = append(kinds, CoreKind{
Name: "accesspolicy",
CueFile: accesspolicyCue,
})
dashboardCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/dashboard/dashboard_kind.cue"))
if err != nil {
return nil, err
@@ -75,24 +66,6 @@ func GetCoreKinds() ([]CoreKind, error) {
CueFile: publicdashboardCue,
})
roleCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/role/role_kind.cue"))
if err != nil {
return nil, err
}
kinds = append(kinds, CoreKind{
Name: "role",
CueFile: roleCue,
})
rolebindingCue, err := loadCueFile(ctx, filepath.Join(root, "./kinds/rolebinding/role_binding_kind.cue"))
if err != nil {
return nil, err
}
kinds = append(kinds, CoreKind{
Name: "rolebinding",
CueFile: rolebindingCue,
})
return kinds, nil
}
+2
View File
@@ -40,6 +40,7 @@ import (
appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
@@ -368,6 +369,7 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)),
permreg.ProvidePermissionRegistry,
acimpl.ProvideAccessControl,
dualwrite.ProvideZanzanaReconciler,
navtreeimpl.ProvideService,
wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)),
wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)),
@@ -32,7 +32,6 @@ type AccessControl interface {
}
type Service interface {
registry.BackgroundService
registry.ProvidesUsageStats
// GetRoleByName returns a role by name
GetRoleByName(ctx context.Context, orgID int64, roleName string) (*RoleDTO, error)
+4 -16
View File
@@ -12,6 +12,7 @@ import (
"go.opentelemetry.io/otel/attribute"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
@@ -25,11 +26,9 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@@ -54,7 +53,7 @@ var OSSRolesPrefixes = []string{accesscontrol.ManagedRolePrefix, accesscontrol.E
func ProvideService(
cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
accessControl accesscontrol.AccessControl, userService user.Service, actionResolver accesscontrol.ActionResolver,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, zclient zanzana.Client, permRegistry permreg.PermissionRegistry,
features featuremgmt.FeatureToggles, tracer tracing.Tracer, permRegistry permreg.PermissionRegistry,
lock *serverlock.ServerLockService, folderService folder.Service,
) (*Service, error) {
service := ProvideOSSService(
@@ -64,7 +63,6 @@ func ProvideService(
cache,
features,
tracer,
zclient,
db,
permRegistry,
lock,
@@ -90,8 +88,8 @@ func ProvideService(
func ProvideOSSService(
cfg *setting.Cfg, store accesscontrol.Store, actionResolver accesscontrol.ActionResolver,
cache *localcache.CacheService, features featuremgmt.FeatureToggles, tracer tracing.Tracer,
zclient zanzana.Client, db db.DB, permRegistry permreg.PermissionRegistry,
lock *serverlock.ServerLockService, folderService folder.Service,
db db.DB, permRegistry permreg.PermissionRegistry, lock *serverlock.ServerLockService,
folderService folder.Service,
) *Service {
s := &Service{
actionResolver: actionResolver,
@@ -101,7 +99,6 @@ func ProvideOSSService(
log: log.New("accesscontrol.service"),
roles: accesscontrol.BuildBasicRoleDefinitions(),
store: store,
reconciler: dualwrite.NewZanzanaReconciler(cfg, zclient, db, lock, folderService),
permRegistry: permRegistry,
}
@@ -118,18 +115,9 @@ type Service struct {
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
store accesscontrol.Store
reconciler *dualwrite.ZanzanaReconciler
permRegistry permreg.PermissionRegistry
}
// Run implements accesscontrol.Service.
func (s *Service) Run(ctx context.Context) error {
if s.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return s.reconciler.Reconcile(ctx)
}
return nil
}
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
return map[string]any{
"stats.oss.accesscontrol.enabled.count": 1,
@@ -71,7 +71,6 @@ func TestUsageMetrics(t *testing.T) {
featuremgmt.WithFeatures(),
tracing.InitializeTracerForTest(),
nil,
nil,
permreg.ProvidePermissionRegistry(),
nil,
nil,
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
@@ -25,12 +26,12 @@ var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/migrator"
// We should rewrite the migration after we have "migrated" all possible actions
// into our schema.
type ZanzanaReconciler struct {
cfg *setting.Cfg
log log.Logger
store db.DB
client zanzana.Client
lock *serverlock.ServerLockService
cfg *setting.Cfg
log log.Logger
features featuremgmt.FeatureToggles
store db.DB
client zanzana.Client
lock *serverlock.ServerLockService
// reconcilers are migrations that tries to reconcile the state of grafana db to zanzana store.
// These are run periodically to try to maintain a consistent state.
reconcilers []resourceReconciler
@@ -38,13 +39,14 @@ type ZanzanaReconciler struct {
globalReconcilers []resourceReconciler
}
func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler {
func ProvideZanzanaReconciler(cfg *setting.Cfg, features featuremgmt.FeatureToggles, client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, folderService folder.Service) *ZanzanaReconciler {
zanzanaReconciler := &ZanzanaReconciler{
cfg: cfg,
log: log.New("zanzana.reconciler"),
client: client,
lock: lock,
store: store,
cfg: cfg,
log: log.New("zanzana.reconciler"),
features: features,
client: client,
lock: lock,
store: store,
reconcilers: []resourceReconciler{
newResourceReconciler(
"team memberships",
@@ -119,6 +121,14 @@ func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB,
return zanzanaReconciler
}
// Run implements registry.BackgroundService
func (r *ZanzanaReconciler) Run(ctx context.Context) error {
if r.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
return r.Reconcile(ctx)
}
return nil
}
// Reconcile schedules as job that will run and reconcile resources between
// legacy access control and zanzana.
func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
@@ -137,13 +147,6 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
}
}
// ReconcileSync runs reconciliation and returns. Useful for tests to perform
// reconciliation in a synchronous way.
func (r *ZanzanaReconciler) ReconcileSync(ctx context.Context) error {
r.reconcile(ctx)
return nil
}
func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
runGlobal := func(ctx context.Context) {
for _, reconciler := range r.globalReconcilers {
@@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
@@ -52,7 +51,7 @@ func ProvideFolderPermissions(
acSvc := acimpl.ProvideOSSService(
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sqlStore, permreg.ProvidePermissionRegistry(),
features, tracing.InitializeTracerForTest(), sqlStore, permreg.ProvidePermissionRegistry(),
nil, fService,
)
+3 -4
View File
@@ -18,6 +18,7 @@ import (
authzv1 "github.com/grafana/authlib/authz/proto/v1"
claims "github.com/grafana/authlib/types"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -29,8 +30,6 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
const zanzanaAudience = "zanzana"
// ProvideZanzana used to register ZanzanaClient.
// It will also start an embedded ZanzanaSever if mode is set to "embedded".
func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) {
@@ -179,7 +178,7 @@ func (z *Zanzana) start(ctx context.Context) error {
authenticator := authnlib.NewAccessTokenAuthenticator(
authnlib.NewAccessTokenVerifier(
authnlib.VerifierConfig{
AllowedAudiences: []string{zanzanaAudience},
AllowedAudiences: []string{authzServiceAudience},
},
authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{
SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL,
@@ -255,7 +254,7 @@ type tokenAuth struct {
func (t *tokenAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
token, err := t.tokenClient.Exchange(ctx, authnlib.TokenExchangeRequest{
Namespace: t.namespace,
Audiences: []string{zanzanaAudience},
Audiences: []string{authzServiceAudience},
})
if err != nil {
return nil, err
@@ -54,7 +54,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
enabled: true,
acSvc: acimpl.ProvideOSSService(
cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{},
localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil,
localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil,
permreg.ProvidePermissionRegistry(), nil, nil,
),
defaultOrgID: autoAssignOrgID,
-93
View File
@@ -1,5 +1,3 @@
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
@@ -63,8 +61,6 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
@@ -73,8 +69,6 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgXrwqZOwZ2DAc/aCi3Bu3xENpspW935vxu0=
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0=
github.com/Yiling-J/theine-go v0.6.0 h1:jv7V/tcD6ijL0T4kfbJDKP81TCZBkoriNTPSqwivWuY=
github.com/Yiling-J/theine-go v0.6.0/go.mod h1:mdch1vjgGWd7s3rWKvY+MF5InRLfRv/CWVI9RVNQ8wY=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -84,8 +78,6 @@ github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vS
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30 h1:hXVi7QKuCQ0E8Yujfu9b0f0RnzZ72efpWvPnZgnJPrE=
github.com/apache/arrow-go/v18 v18.0.1-0.20241212180703-82be143d7c30/go.mod h1:RNuWDIiGjq5nndL2PyQrndUy9nMLwheA3uWaAV7fe4U=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -224,8 +216,6 @@ github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTE
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa h1:kyoPzxViSXAyqfO0Mab7Qo1UogFIrxZKKyBU6kBOl+E=
github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -234,15 +224,11 @@ github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0
github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
@@ -370,8 +356,6 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
@@ -445,8 +429,6 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/sqlds/v4 v4.1.3 h1:+Hy5Yz+tSbD5N3yuLM0VKTsWlVaCzM1S1m1QEBZL7fE=
github.com/grafana/sqlds/v4 v4.1.3/go.mod h1:Lx8IR939lIrCBpCKthv7AXs7E7bmNWPgt0gene/idT8=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes=
@@ -484,8 +466,6 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@@ -496,16 +476,7 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY=
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
@@ -564,10 +535,6 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -576,8 +543,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
@@ -605,8 +570,6 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
@@ -648,10 +611,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU=
github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8=
@@ -661,8 +620,6 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -670,14 +627,6 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5 h1:z9jaRoo+NIN1AB0ogjtrjx1316TTuq6IbqpEg3UJycA=
github.com/openfga/api/proto v0.0.0-20250107154247-c22e6db5c4f5/go.mod h1:m74TNgnAAIJ03gfHcx+xaRWnr+IbQy3y/AVNwwCFrC0=
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c h1:1y84C0V4NRfPtRi4MqQ7+gnFtYgeBKPIeIAPLdVJ7j4=
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20241115164311-10e575c8e47c/go.mod h1:12RMe/HuRNyOzS33RQa53jwdcxE2znr8ycXMlVbgQN4=
github.com/openfga/openfga v1.8.4 h1:OqyRpuxMCxcS7irTFYFkhAIYzmAnczNwxUqjnuZOQyo=
github.com/openfga/openfga v1.8.4/go.mod h1:9Ax9VMMySV2JMsCT8MTePeYt4OrTnPAy1XUV1y9RyuU=
github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w=
github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
@@ -686,8 +635,6 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -702,8 +649,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72VU=
github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
github.com/prometheus/alertmanager v0.27.0 h1:V6nTa2J5V4s8TG4C4HtrBP/WNSebCCTYGGv4qecA/+I=
github.com/prometheus/alertmanager v0.27.0/go.mod h1:8Ia/R3urPmbzJ8OsdvmZvIprDwvwmYCmUbwBL+jlPOE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -737,8 +682,6 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -749,15 +692,9 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
@@ -775,20 +712,12 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PX
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -808,8 +737,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
@@ -894,12 +821,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng=
gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1181,20 +1102,6 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
@@ -663,7 +663,6 @@ const customHomeDashboardV2Spec = {
autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
quickRanges: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
hideTimepicker: false,
weekStart: '',
fiscalYearStartMonth: 0,
},
variables: [],
@@ -28,6 +28,7 @@ import {
} from '../../utils/utils';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
import { DashboardGridItem } from './DashboardGridItem';
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
@@ -45,7 +46,7 @@ export class DefaultGridLayoutManager
public readonly isDashboardLayoutManager = true;
public static readonly descriptor = {
public static readonly descriptor: LayoutRegistryItem = {
get name() {
return t('dashboard.default-layout.name', 'Default grid');
},
@@ -54,6 +55,7 @@ export class DefaultGridLayoutManager
},
id: 'default-grid',
createFromLayout: DefaultGridLayoutManager.createFromLayout,
kind: 'GridLayout',
};
public readonly descriptor = DefaultGridLayoutManager.descriptor;
@@ -7,6 +7,7 @@ import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelI
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
import { ResponsiveGridItem } from './ResponsiveGridItem';
import { getEditOptions } from './ResponsiveGridLayoutManagerEditor';
@@ -23,7 +24,7 @@ export class ResponsiveGridLayoutManager
public readonly isDashboardLayoutManager = true;
public static readonly descriptor = {
public static readonly descriptor: LayoutRegistryItem = {
get name() {
return t('dashboard.responsive-layout.name', 'Responsive grid');
},
@@ -32,6 +33,8 @@ export class ResponsiveGridLayoutManager
},
id: 'responsive-grid',
createFromLayout: ResponsiveGridLayoutManager.createFromLayout,
kind: 'ResponsiveGridLayout',
};
public readonly descriptor = ResponsiveGridLayoutManager.descriptor;
@@ -9,6 +9,7 @@ import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutMan
import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
import { RowItem } from './RowItem';
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
@@ -23,7 +24,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
public readonly isDashboardLayoutManager = true;
public static readonly descriptor = {
public static readonly descriptor: LayoutRegistryItem = {
get name() {
return t('dashboard.rows-layout.name', 'Rows');
},
@@ -32,6 +33,8 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
},
id: 'rows-layout',
createFromLayout: RowsLayoutManager.createFromLayout,
kind: 'RowsLayout',
};
public readonly descriptor = RowsLayoutManager.descriptor;
@@ -1,4 +1,5 @@
import { SceneObject, VizPanel } from '@grafana/scenes';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { LayoutRegistryItem } from './LayoutRegistryItem';
@@ -82,6 +83,15 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager;
}
export interface LayoutManagerSerializer {
serialize(layout: DashboardLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout'];
deserialize(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): DashboardLayoutManager;
}
export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager {
return 'isDashboardLayoutManager' in obj;
}
@@ -1,4 +1,5 @@
import { RegistryItem } from '@grafana/data';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { DashboardLayoutManager } from './DashboardLayoutManager';
@@ -17,4 +18,9 @@ export interface LayoutRegistryItem<S = {}> extends RegistryItem {
* @param saveModel
*/
createFromSaveModel?(saveModel: S): void;
/**
* Schema kind of layout
*/
kind?: DashboardV2Spec['layout']['kind'];
}
@@ -586,7 +586,6 @@ describe('DashboardSceneSerializer', () => {
autoRefreshIntervals: [],
quickRanges: [],
hideTimepicker: false,
weekStart: '',
fiscalYearStartMonth: 0,
timezone: '',
},
@@ -650,7 +649,6 @@ describe('DashboardSceneSerializer', () => {
quickRanges: [],
timezone: 'browser',
to: 'now',
weekStart: '',
});
});
@@ -0,0 +1,354 @@
import { config } from '@grafana/runtime';
import {
SceneGridItemLike,
SceneGridLayout,
SceneGridRow,
SceneObject,
VizPanel,
VizPanelMenu,
VizPanelState,
} from '@grafana/scenes';
import {
DashboardV2Spec,
GridLayoutItemKind,
GridLayoutKind,
GridLayoutRowKind,
RepeatOptions,
Element,
GridLayoutItemSpec,
PanelKind,
LibraryPanelKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { contextSrv } from 'app/core/core';
import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior';
import { PanelNotices } from '../../scene/PanelNotices';
import { AngularDeprecation } from '../../scene/angular/AngularDeprecation';
import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../../scene/layout-default/RowRepeaterBehavior';
import { RowActions } from '../../scene/layout-default/row-actions/RowActions';
import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext';
import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { isClonedKey } from '../../utils/clone';
import { calculateGridItemDimensions, getVizPanelKeyForPanelId, isLibraryPanel } from '../../utils/utils';
import { GRID_ROW_HEIGHT } from '../const';
import { buildVizPanel } from './utils';
export class DefaultGridLayoutManagerSerializer implements LayoutManagerSerializer {
serialize(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): DashboardV2Spec['layout'] {
return {
kind: 'GridLayout',
spec: {
items: getGridLayoutItems(layoutManager, isSnapshot),
},
};
}
deserialize(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): DashboardLayoutManager {
if (layout.kind !== 'GridLayout') {
throw new Error('Invalid layout kind');
}
return new DefaultGridLayoutManager({
grid: new SceneGridLayout({
isLazy: !(preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneGridLayoutForItems(layout, elements),
}),
});
}
}
function getGridLayoutItems(
body: DefaultGridLayoutManager,
isSnapshot?: boolean
): Array<GridLayoutItemKind | GridLayoutRowKind> {
let items: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
for (const child of body.state.grid.state.children) {
if (child instanceof DashboardGridItem) {
// TODO: handle panel repeater scenario
if (child.state.variableName) {
items = items.concat(repeaterToLayoutItems(child, isSnapshot));
} else {
items.push(gridItemToGridLayoutItemKind(child));
}
} else if (child instanceof SceneGridRow) {
if (isClonedKey(child.state.key!) && !isSnapshot) {
// Skip repeat rows
continue;
}
items.push(gridRowToLayoutRowKind(child, isSnapshot));
}
}
return items;
}
function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined {
if (row.state.$behaviors) {
for (const behavior of row.state.$behaviors) {
if (behavior instanceof RowRepeaterBehavior) {
return { value: behavior.state.variableName, mode: 'variable' };
}
}
}
return undefined;
}
function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind {
const children = row.state.children.map((child) => {
if (!(child instanceof DashboardGridItem)) {
throw new Error('Unsupported row child type');
}
const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT;
return gridItemToGridLayoutItemKind(child, y);
});
return {
kind: 'GridLayoutRow',
spec: {
title: row.state.title,
y: row.state.y ?? 0,
collapsed: Boolean(row.state.isCollapsed),
elements: children,
repeat: getRowRepeat(row),
},
};
}
function gridItemToGridLayoutItemKind(gridItem: DashboardGridItem, yOverride?: number): GridLayoutItemKind {
let elementGridItem: GridLayoutItemKind | undefined;
let x = 0,
y = 0,
width = 0,
height = 0;
let gridItem_ = gridItem;
if (!(gridItem_.state.body instanceof VizPanel)) {
throw new Error('DashboardGridItem body expected to be VizPanel');
}
// Get the grid position and size
height = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0;
x = gridItem_.state.x ?? 0;
y = gridItem_.state.y ?? 0;
width = gridItem_.state.width ?? 0;
const repeatVar = gridItem_.state.variableName;
// FIXME: which name should we use for the element reference, key or something else ?
const elementName = gridItem_.state.body.state.key ?? 'DefaultName';
elementGridItem = {
kind: 'GridLayoutItem',
spec: {
x,
y: yOverride ?? y,
width: width,
height: height,
element: {
kind: 'ElementReference',
name: elementName,
},
},
};
if (repeatVar) {
const repeat: RepeatOptions = {
mode: 'variable',
value: repeatVar,
};
if (gridItem_.state.maxPerRow) {
repeat.maxPerRow = gridItem_.getMaxPerRow();
}
if (gridItem_.state.repeatDirection) {
repeat.direction = gridItem_.getRepeatDirection();
}
elementGridItem.spec.repeat = repeat;
}
if (!elementGridItem) {
throw new Error('Unsupported grid item type');
}
return elementGridItem;
}
function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false): GridLayoutItemKind[] {
if (!isSnapshot) {
return [gridItemToGridLayoutItemKind(repeater)];
} else {
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
// TODO: implement
// const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
// return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
return [];
}
if (repeater.state.repeatedPanels) {
const { h, w, columnCount } = calculateGridItemDimensions(repeater);
const panels = repeater.state.repeatedPanels!.map((panel, index) => {
let x = 0,
y = 0;
if (repeater.state.repeatDirection === 'v') {
x = repeater.state.x!;
y = index * h;
} else {
x = (index % columnCount) * w;
y = repeater.state.y! + Math.floor(index / columnCount) * h;
}
const gridPos = { x, y, w, h };
const result: GridLayoutItemKind = {
kind: 'GridLayoutItem',
spec: {
x: gridPos.x,
y: gridPos.y,
width: gridPos.w,
height: gridPos.h,
repeat: {
mode: 'variable',
value: repeater.state.variableName!,
maxPerRow: repeater.getMaxPerRow(),
direction: repeater.state.repeatDirection,
},
element: {
kind: 'ElementReference',
name: panel.state.key!,
},
},
};
return result;
});
return panels;
}
return [];
}
}
function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record<string, Element>): SceneGridItemLike[] {
const gridElements = layout.spec.items;
return gridElements.map((element) => {
if (element.kind === 'GridLayoutItem') {
const panel = elements[element.spec.element.name];
if (!panel) {
throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`);
}
if (panel.kind === 'Panel') {
return buildGridItem(element.spec, panel);
} else if (panel.kind === 'LibraryPanel') {
const libraryPanel = buildLibraryPanel(panel);
return new DashboardGridItem({
key: `grid-item-${panel.spec.id}`,
x: element.spec.x,
y: element.spec.y,
width: element.spec.width,
height: element.spec.height,
itemHeight: element.spec.height,
body: libraryPanel,
});
} else {
throw new Error(`Unknown element kind: ${element.kind}`);
}
} else if (element.kind === 'GridLayoutRow') {
const children = element.spec.elements.map((gridElement) => {
const panel = elements[gridElement.spec.element.name];
if (panel.kind === 'Panel') {
return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y);
} else {
throw new Error(`Unknown element kind: ${gridElement.kind}`);
}
});
let behaviors: SceneObject[] | undefined;
if (element.spec.repeat) {
behaviors = [new RowRepeaterBehavior({ variableName: element.spec.repeat.value })];
}
return new SceneGridRow({
y: element.spec.y,
isCollapsed: element.spec.collapsed,
title: element.spec.title,
$behaviors: behaviors,
actions: new RowActions({}),
children,
});
} else {
// If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error.
//@ts-expect-error
throw new Error(`Unknown layout element kind: ${element.kind}`);
}
});
}
function buildGridItem(gridItem: GridLayoutItemSpec, panel: PanelKind, yOverride?: number): DashboardGridItem {
const vizPanel = buildVizPanel(panel);
return new DashboardGridItem({
key: `grid-item-${panel.spec.id}`,
x: gridItem.x,
y: yOverride ?? gridItem.y,
width: gridItem.repeat?.direction === 'h' ? 24 : gridItem.width,
height: gridItem.height,
itemHeight: gridItem.height,
body: vizPanel,
variableName: gridItem.repeat?.value,
repeatDirection: gridItem.repeat?.direction,
maxPerRow: gridItem.repeat?.maxPerRow,
});
}
function buildLibraryPanel(panel: LibraryPanelKind): VizPanel {
const titleItems: SceneObject[] = [];
if (config.featureToggles.angularDeprecationUI) {
titleItems.push(new AngularDeprecation());
}
titleItems.push(
new VizPanelLinks({
rawLinks: [],
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
})
);
titleItems.push(new PanelNotices());
const vizPanelState: VizPanelState = {
key: getVizPanelKeyForPanelId(panel.spec.id),
titleItems,
$behaviors: [
new LibraryPanelBehavior({
uid: panel.spec.libraryPanel.uid,
name: panel.spec.libraryPanel.name,
}),
],
extendPanelContext: setDashboardPanelContext,
pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID,
title: panel.spec.title,
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
};
if (!config.publicDashboardAccessToken) {
vizPanelState.menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],
});
}
return new VizPanel(vizPanelState);
}
@@ -0,0 +1,65 @@
import { SceneCSSGridLayout } from '@grafana/scenes';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { ResponsiveGridItem } from '../../scene/layout-responsive-grid/ResponsiveGridItem';
import { ResponsiveGridLayoutManager } from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { DashboardLayoutManager, LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { getGridItemKeyForPanelId } from '../../utils/utils';
import { buildVizPanel } from './utils';
export class ResponsiveGridLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: ResponsiveGridLayoutManager): DashboardV2Spec['layout'] {
return {
kind: 'ResponsiveGridLayout',
spec: {
col:
layoutManager.state.layout.state.templateColumns?.toString() ??
ResponsiveGridLayoutManager.defaultCSS.templateColumns,
row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows,
items: layoutManager.state.layout.state.children.map((child) => {
if (!(child instanceof ResponsiveGridItem)) {
throw new Error('Expected ResponsiveGridItem');
}
return {
kind: 'ResponsiveGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: child.state?.body?.state.key ?? 'DefaultName',
},
},
};
}),
},
};
}
deserialize(layout: DashboardV2Spec['layout'], elements: DashboardV2Spec['elements']): DashboardLayoutManager {
if (layout.kind !== 'ResponsiveGridLayout') {
throw new Error('Invalid layout kind');
}
const children = layout.spec.items.map((item) => {
const panel = elements[item.spec.element.name];
if (!panel) {
throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`);
}
if (panel.kind !== 'Panel') {
throw new Error(`Unsupported element kind: ${panel.kind}`);
}
return new ResponsiveGridItem({
key: getGridItemKeyForPanelId(panel.spec.id),
body: buildVizPanel(panel),
});
});
return new ResponsiveGridLayoutManager({
layout: new SceneCSSGridLayout({
templateColumns: layout.spec.col,
autoRows: layout.spec.row,
children,
}),
});
}
}
@@ -0,0 +1,51 @@
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { RowItem } from '../../scene/layout-rows/RowItem';
import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager';
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
import { getLayout } from './utils';
export class RowsLayoutSerializer implements LayoutManagerSerializer {
serialize(layoutManager: RowsLayoutManager): DashboardV2Spec['layout'] {
return {
kind: 'RowsLayout',
spec: {
rows: layoutManager.state.rows.map((row) => {
const layout = getLayout(row.state.layout);
if (layout.kind === 'RowsLayout') {
throw new Error('Nested RowsLayout is not supported');
}
return {
kind: 'RowsLayoutRow',
spec: {
title: row.state.title,
collapsed: row.state.isCollapsed ?? false,
layout: layout,
},
};
}),
},
};
}
deserialize(
layout: DashboardV2Spec['layout'],
elements: DashboardV2Spec['elements'],
preload: boolean
): RowsLayoutManager {
if (layout.kind !== 'RowsLayout') {
throw new Error('Invalid layout kind');
}
const rows = layout.spec.rows.map((row) => {
const layout = row.spec.layout;
return new RowItem({
title: row.spec.title,
isCollapsed: row.spec.collapsed,
layout: layoutSerializerRegistry.get(layout.kind).serializer.deserialize(layout, elements, preload),
});
});
return new RowsLayoutManager({ rows });
}
}
@@ -0,0 +1,20 @@
import { Registry, RegistryItem } from '@grafana/data';
import { LayoutManagerSerializer } from '../../scene/types/DashboardLayoutManager';
import { DefaultGridLayoutManagerSerializer } from './DefaultGridLayoutSerializer';
import { ResponsiveGridLayoutSerializer } from './ResponsiveGridLayoutSerializer';
import { RowsLayoutSerializer } from './RowsLayoutSerializer';
interface LayoutSerializerRegistryItem extends RegistryItem {
serializer: LayoutManagerSerializer;
}
export const layoutSerializerRegistry: Registry<LayoutSerializerRegistryItem> =
new Registry<LayoutSerializerRegistryItem>(() => {
return [
{ id: 'GridLayout', name: 'Grid Layout', serializer: new DefaultGridLayoutManagerSerializer() },
{ id: 'ResponsiveGridLayout', name: 'Responsive Grid Layout', serializer: new ResponsiveGridLayoutSerializer() },
{ id: 'RowsLayout', name: 'Rows Layout', serializer: new RowsLayoutSerializer() },
];
});
@@ -0,0 +1,154 @@
import { config } from '@grafana/runtime';
import {
SceneDataProvider,
SceneDataQuery,
SceneDataTransformer,
SceneObject,
SceneQueryRunner,
VizPanel,
VizPanelMenu,
VizPanelState,
} from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen';
import { DashboardV2Spec, PanelKind, PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior';
import { PanelNotices } from '../../scene/PanelNotices';
import { PanelTimeRange } from '../../scene/PanelTimeRange';
import { AngularDeprecation } from '../../scene/angular/AngularDeprecation';
import { setDashboardPanelContext } from '../../scene/setDashboardPanelContext';
import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager';
import { getVizPanelKeyForPanelId } from '../../utils/utils';
import { transformMappingsToV1 } from '../transformToV1TypesUtils';
import { layoutSerializerRegistry } from './layoutSerializerRegistry';
export function buildVizPanel(panel: PanelKind): VizPanel {
const titleItems: SceneObject[] = [];
if (config.featureToggles.angularDeprecationUI) {
titleItems.push(new AngularDeprecation());
}
titleItems.push(
new VizPanelLinks({
rawLinks: panel.spec.links,
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
})
);
titleItems.push(new PanelNotices());
const queryOptions = panel.spec.data.spec.queryOptions;
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
const vizPanelState: VizPanelState = {
key: getVizPanelKeyForPanelId(panel.spec.id),
title: panel.spec.title,
description: panel.spec.description,
pluginId: panel.spec.vizConfig.kind,
options: panel.spec.vizConfig.spec.options,
fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig),
pluginVersion: panel.spec.vizConfig.spec.pluginVersion,
displayMode: panel.spec.transparent ? 'transparent' : 'default',
hoverHeader: !panel.spec.title && !timeOverrideShown,
hoverHeaderOffset: 0,
$data: createPanelDataProvider(panel),
titleItems,
$behaviors: [],
extendPanelContext: setDashboardPanelContext,
// _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration
};
if (!config.publicDashboardAccessToken) {
vizPanelState.menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],
});
}
if (queryOptions.timeFrom || queryOptions.timeShift) {
vizPanelState.$timeRange = new PanelTimeRange({
timeFrom: queryOptions.timeFrom,
timeShift: queryOptions.timeShift,
hideTimeOverride: queryOptions.hideTimeOverride,
});
}
return new VizPanel(vizPanelState);
}
export function createPanelDataProvider(panelKind: PanelKind): SceneDataProvider | undefined {
const panel = panelKind.spec;
const targets = panel.data?.spec.queries ?? [];
// Skip setting query runner for panels without queries
if (!targets?.length) {
return undefined;
}
// Skip setting query runner for panel plugins with skipDataQuery
if (config.panels[panel.vizConfig.kind]?.skipDataQuery) {
return undefined;
}
let dataProvider: SceneDataProvider | undefined = undefined;
const datasource = getPanelDataSource(panelKind);
dataProvider = new SceneQueryRunner({
datasource,
queries: targets.map(panelQueryKindToSceneQuery),
maxDataPoints: panel.data.spec.queryOptions.maxDataPoints ?? undefined,
maxDataPointsFromWidth: true,
cacheTimeout: panel.data.spec.queryOptions.cacheTimeout,
queryCachingTTL: panel.data.spec.queryOptions.queryCachingTTL,
minInterval: panel.data.spec.queryOptions.interval ?? undefined,
dataLayerFilter: {
panelId: panel.id,
},
$behaviors: [new DashboardDatasourceBehaviour({})],
});
// Wrap inner data provider in a data transformer
return new SceneDataTransformer({
$data: dataProvider,
transformations: panel.data.spec.transformations.map((transformation) => transformation.spec),
});
}
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
if (!panel.spec.data?.spec.queries?.length) {
return undefined;
}
let datasource: DataSourceRef | undefined = undefined;
let isMixedDatasource = false;
panel.spec.data.spec.queries.forEach((query) => {
if (!datasource) {
datasource = query.spec.datasource;
} else if (datasource.uid !== query.spec.datasource?.uid || datasource.type !== query.spec.datasource?.type) {
isMixedDatasource = true;
}
});
return isMixedDatasource ? { type: 'mixed', uid: MIXED_DATASOURCE_NAME } : undefined;
}
function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery {
return {
refId: query.spec.refId,
datasource: query.spec.datasource,
hide: query.spec.hidden,
...query.spec.query.spec,
};
}
export function getLayout(sceneState: DashboardLayoutManager): DashboardV2Spec['layout'] {
const registryItem = layoutSerializerRegistry.get(sceneState.descriptor.kind ?? '');
if (!registryItem) {
throw new Error(`Layout serializer not found for kind: ${sceneState.descriptor.kind}`);
}
return registryItem.serializer.serialize(sceneState);
}
@@ -14,6 +14,7 @@ import {
AdHocFiltersVariable,
SceneDataTransformer,
SceneGridRow,
SceneGridItem,
} from '@grafana/scenes';
import {
AdhocVariableKind,
@@ -34,6 +35,9 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getQueryRunnerFor } from '../utils/utils';
@@ -495,5 +499,112 @@ describe('transformSaveModelSchemaV2ToScene', () => {
expect(scene.state.meta.canDelete).toBe(true);
});
});
describe('dynamic dashboard layouts', () => {
it('should build a dashboard scene with a responsive grid layout', () => {
const dashboard = cloneDeep(defaultDashboard);
dashboard.spec.layout = {
kind: 'ResponsiveGridLayout',
spec: {
col: 'colString',
row: 'rowString',
items: [
{
kind: 'ResponsiveGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: 'panel-1',
},
},
},
],
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as ResponsiveGridLayoutManager;
expect(layoutManager.descriptor.kind).toBe('ResponsiveGridLayout');
expect(layoutManager.state.layout.state.templateColumns).toBe('colString');
expect(layoutManager.state.layout.state.autoRows).toBe('rowString');
expect(layoutManager.state.layout.state.children.length).toBe(1);
const gridItem = layoutManager.state.layout.state.children[0] as ResponsiveGridItem;
expect(gridItem.state.body.state.key).toBe('panel-1');
});
it('should build a dashboard scene with rows layout', () => {
const dashboard = cloneDeep(defaultDashboard);
dashboard.spec.layout = {
kind: 'RowsLayout',
spec: {
rows: [
{
kind: 'RowsLayoutRow',
spec: {
title: 'row1',
collapsed: false,
layout: {
kind: 'ResponsiveGridLayout',
spec: {
col: 'colString',
row: 'rowString',
items: [
{
kind: 'ResponsiveGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: 'panel-1',
},
},
},
],
},
},
},
},
{
kind: 'RowsLayoutRow',
spec: {
title: 'row2',
collapsed: true,
layout: {
kind: 'GridLayout',
spec: {
items: [
{
kind: 'GridLayoutItem',
spec: {
y: 0,
x: 0,
height: 10,
width: 10,
element: {
kind: 'ElementReference',
name: 'panel-2',
},
},
},
],
},
},
},
},
],
},
};
const scene = transformSaveModelSchemaV2ToScene(dashboard);
const layoutManager = scene.state.body as RowsLayoutManager;
expect(layoutManager.descriptor.kind).toBe('RowsLayout');
expect(layoutManager.state.rows.length).toBe(2);
const row1Manager = layoutManager.state.rows[0].state.layout as ResponsiveGridLayoutManager;
expect(row1Manager.descriptor.kind).toBe('ResponsiveGridLayout');
const row1GridItem = row1Manager.state.layout.state.children[0] as ResponsiveGridItem;
expect(row1GridItem.state.body.state.key).toBe('panel-1');
const row2Manager = layoutManager.state.rows[1].state.layout as DefaultGridLayoutManager;
expect(row2Manager.descriptor.kind).toBe('GridLayout');
const row2GridItem = row2Manager.state.grid.state.children[0] as SceneGridItem;
expect(row2GridItem.state.body!.state.key).toBe('panel-2');
});
});
});
});
@@ -10,15 +10,10 @@ import {
GroupByVariable,
IntervalVariable,
QueryVariable,
SceneCSSGridLayout,
SceneDataLayerControls,
SceneDataProvider,
SceneDataQuery,
SceneDataTransformer,
SceneGridItemLike,
SceneGridLayout,
SceneGridRow,
SceneObject,
SceneQueryRunner,
SceneRefreshPicker,
SceneTimePicker,
@@ -27,9 +22,6 @@ import {
SceneVariableSet,
TextBoxVariable,
VariableValueSelectors,
VizPanel,
VizPanelMenu,
VizPanelState,
} from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema/dist/esm/index.gen';
import {
@@ -46,9 +38,6 @@ import {
defaultIntervalVariableKind,
defaultQueryVariableKind,
defaultTextVariableKind,
GridLayoutItemSpec,
GridLayoutKind,
Element,
GroupByVariableKind,
IntervalVariableKind,
LibraryPanelKind,
@@ -56,9 +45,7 @@ import {
PanelQueryKind,
QueryVariableKind,
TextVariableKind,
ResponsiveGridLayoutItemKind,
} from '@grafana/schema/src/schema/dashboard/v2alpha0';
import { contextSrv } from 'app/core/core';
import {
AnnoKeyCreatedBy,
AnnoKeyFolder,
@@ -80,32 +67,16 @@ import { registerDashboardMacro } from '../scene/DashboardMacro';
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardScopesFacade } from '../scene/DashboardScopesFacade';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { PanelNotices } from '../scene/PanelNotices';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { AngularDeprecation } from '../scene/angular/AngularDeprecation';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
import { RowActions } from '../scene/layout-default/row-actions/RowActions';
import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { RowItem } from '../scene/layout-rows/RowItem';
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { preserveDashboardSceneStateInLocalStorage } from '../utils/dashboardSessionState';
import { getGridItemKeyForPanelId, getIntervalsFromQueryString, getVizPanelKeyForPanelId } from '../utils/utils';
import { getIntervalsFromQueryString } from '../utils/utils';
import { GRID_ROW_HEIGHT } from './const';
import { SnapshotVariable } from './custom-variables/SnapshotVariable';
import { layoutSerializerRegistry } from './layoutSerializers/layoutSerializerRegistry';
import { registerPanelInteractionsReporter } from './transformSaveModelToScene';
import {
transformCursorSyncV2ToV1,
transformSortVariableToEnumV1,
transformMappingsToV1,
transformVariableHideToEnumV1,
transformVariableRefreshToEnumV1,
} from './transformToV1TypesUtils';
@@ -179,7 +150,11 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
meta.canSave = false;
}
const layoutManager: DashboardLayoutManager = createLayoutManager(dashboard);
const layoutManager: DashboardLayoutManager = layoutSerializerRegistry
.get(dashboard.layout.kind)
.serializer.deserialize(dashboard.layout, dashboard.elements, dashboard.preload);
//createLayoutManager(dashboard);
const dashboardScene = new DashboardScene({
description: dashboard.description,
@@ -243,251 +218,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
return dashboardScene;
}
function buildGridItem(gridItem: GridLayoutItemSpec, panel: PanelKind, yOverride?: number): DashboardGridItem {
const vizPanel = buildVizPanel(panel);
return new DashboardGridItem({
key: `grid-item-${panel.spec.id}`,
x: gridItem.x,
y: yOverride ?? gridItem.y,
width: gridItem.repeat?.direction === 'h' ? 24 : gridItem.width,
height: gridItem.height,
itemHeight: gridItem.height,
body: vizPanel,
variableName: gridItem.repeat?.value,
repeatDirection: gridItem.repeat?.direction,
maxPerRow: gridItem.repeat?.maxPerRow,
});
}
function createLayoutManager(dashboard: DashboardV2Spec): DashboardLayoutManager {
if (dashboard.layout.kind === 'GridLayout') {
return new DefaultGridLayoutManager({
grid: new SceneGridLayout({
isLazy: !(dashboard.preload || contextSrv.user.authenticatedBy === 'render'),
children: createSceneGridLayoutForItems(dashboard.layout, dashboard.elements),
}),
});
} else if (dashboard.layout.kind === 'RowsLayout') {
return new RowsLayoutManager({
rows: dashboard.layout.spec.rows.map((row) => {
let layout: DashboardLayoutManager | undefined = undefined;
if (row.spec.layout.kind === 'GridLayout') {
layout = new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: createSceneGridLayoutForItems(row.spec.layout, dashboard.elements),
}),
});
}
if (row.spec.layout.kind === 'ResponsiveGridLayout') {
layout = new ResponsiveGridLayoutManager({
layout: new SceneCSSGridLayout({
templateColumns: row.spec.layout.spec.col,
autoRows: row.spec.layout.spec.row,
children: createResponsiveGridItems(row.spec.layout.spec.items, dashboard.elements),
}),
});
}
if (!layout) {
throw new Error(`Unsupported layout kind: ${row.spec.layout.kind} in row`);
}
return new RowItem({
title: row.spec.title,
isCollapsed: row.spec.collapsed,
layout: layout,
});
}),
});
} else if (dashboard.layout.kind === 'ResponsiveGridLayout') {
return new ResponsiveGridLayoutManager({
layout: new SceneCSSGridLayout({
templateColumns: dashboard.layout.spec.col,
autoRows: dashboard.layout.spec.row,
children: createResponsiveGridItems(dashboard.layout.spec.items, dashboard.elements),
}),
});
}
// @ts-ignore - this complains because we should never reach this point. If the model does not match the schema we will though.
throw new Error(`Unsupported layout type: ${dashboard.layout.kind}`);
}
function createResponsiveGridItems(
items: ResponsiveGridLayoutItemKind[],
elements: Record<string, Element>
): ResponsiveGridItem[] {
return items.map((item) => {
const panel = elements[item.spec.element.name];
if (!panel) {
throw new Error(`Panel with uid ${item.spec.element.name} not found in the dashboard elements`);
}
if (panel.kind !== 'Panel') {
throw new Error(`Unsupported element kind: ${panel.kind}`);
}
return new ResponsiveGridItem({
key: getGridItemKeyForPanelId(panel.spec.id),
body: buildVizPanel(panel),
});
});
}
function createSceneGridLayoutForItems(layout: GridLayoutKind, elements: Record<string, Element>): SceneGridItemLike[] {
const gridElements = layout.spec.items;
return gridElements.map((element) => {
if (element.kind === 'GridLayoutItem') {
const panel = elements[element.spec.element.name];
if (!panel) {
throw new Error(`Panel with uid ${element.spec.element.name} not found in the dashboard elements`);
}
if (panel.kind === 'Panel') {
return buildGridItem(element.spec, panel);
} else if (panel.kind === 'LibraryPanel') {
const libraryPanel = buildLibraryPanel(panel);
return new DashboardGridItem({
key: `grid-item-${panel.spec.id}`,
x: element.spec.x,
y: element.spec.y,
width: element.spec.width,
height: element.spec.height,
itemHeight: element.spec.height,
body: libraryPanel,
});
} else {
throw new Error(`Unknown element kind: ${element.kind}`);
}
} else if (element.kind === 'GridLayoutRow') {
const children = element.spec.elements.map((gridElement) => {
const panel = elements[gridElement.spec.element.name];
if (panel.kind === 'Panel') {
return buildGridItem(gridElement.spec, panel, element.spec.y + GRID_ROW_HEIGHT + gridElement.spec.y);
} else {
throw new Error(`Unknown element kind: ${gridElement.kind}`);
}
});
let behaviors: SceneObject[] | undefined;
if (element.spec.repeat) {
behaviors = [new RowRepeaterBehavior({ variableName: element.spec.repeat.value })];
}
return new SceneGridRow({
y: element.spec.y,
isCollapsed: element.spec.collapsed,
title: element.spec.title,
$behaviors: behaviors,
actions: new RowActions({}),
children,
});
} else {
// If this has been validated by the schema we should never reach this point, which is why TS is telling us this is an error.
//@ts-expect-error
throw new Error(`Unknown layout element kind: ${element.kind}`);
}
});
}
function buildLibraryPanel(panel: LibraryPanelKind): VizPanel {
const titleItems: SceneObject[] = [];
if (config.featureToggles.angularDeprecationUI) {
titleItems.push(new AngularDeprecation());
}
titleItems.push(
new VizPanelLinks({
rawLinks: [],
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
})
);
titleItems.push(new PanelNotices());
const vizPanelState: VizPanelState = {
key: getVizPanelKeyForPanelId(panel.spec.id),
titleItems,
$behaviors: [
new LibraryPanelBehavior({
uid: panel.spec.libraryPanel.uid,
name: panel.spec.libraryPanel.name,
}),
],
extendPanelContext: setDashboardPanelContext,
pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID,
title: panel.spec.title,
options: {},
fieldConfig: {
defaults: {},
overrides: [],
},
};
if (!config.publicDashboardAccessToken) {
vizPanelState.menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],
});
}
return new VizPanel(vizPanelState);
}
function buildVizPanel(panel: PanelKind): VizPanel {
const titleItems: SceneObject[] = [];
if (config.featureToggles.angularDeprecationUI) {
titleItems.push(new AngularDeprecation());
}
titleItems.push(
new VizPanelLinks({
rawLinks: panel.spec.links,
menu: new VizPanelLinksMenu({ $behaviors: [panelLinksBehavior] }),
})
);
titleItems.push(new PanelNotices());
const queryOptions = panel.spec.data.spec.queryOptions;
const timeOverrideShown = (queryOptions.timeFrom || queryOptions.timeShift) && !queryOptions.hideTimeOverride;
const vizPanelState: VizPanelState = {
key: getVizPanelKeyForPanelId(panel.spec.id),
title: panel.spec.title,
description: panel.spec.description,
pluginId: panel.spec.vizConfig.kind,
options: panel.spec.vizConfig.spec.options,
fieldConfig: transformMappingsToV1(panel.spec.vizConfig.spec.fieldConfig),
pluginVersion: panel.spec.vizConfig.spec.pluginVersion,
displayMode: panel.spec.transparent ? 'transparent' : 'default',
hoverHeader: !panel.spec.title && !timeOverrideShown,
hoverHeaderOffset: 0,
$data: createPanelDataProvider(panel),
titleItems,
$behaviors: [],
extendPanelContext: setDashboardPanelContext,
// _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration
};
if (!config.publicDashboardAccessToken) {
vizPanelState.menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],
});
}
if (queryOptions.timeFrom || queryOptions.timeShift) {
vizPanelState.$timeRange = new PanelTimeRange({
timeFrom: queryOptions.timeFrom,
timeShift: queryOptions.timeShift,
hideTimeOverride: queryOptions.hideTimeOverride,
});
}
return new VizPanel(vizPanelState);
}
function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined {
if (!panel.spec.data?.spec.queries?.length) {
return undefined;
@@ -9,6 +9,7 @@ import {
GroupByVariable,
IntervalVariable,
QueryVariable,
SceneCSSGridLayout,
SceneGridLayout,
SceneGridRow,
SceneRefreshPicker,
@@ -24,6 +25,10 @@ import {
VariableSort as VariableSortV1,
} from '@grafana/schema/dist/esm/index.gen';
import {
ResponsiveGridLayoutSpec,
RowsLayoutSpec,
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0';
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardControls } from '../scene/DashboardControls';
@@ -33,6 +38,11 @@ import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { RowItem } from '../scene/layout-rows/RowItem';
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2';
@@ -365,6 +375,158 @@ describe('transformSceneToSaveModelSchemaV2', () => {
});
});
function getMinimalSceneState(body: DashboardLayoutManager): Partial<DashboardSceneState> {
return {
id: 1,
title: 'Test Dashboard',
description: 'Test Description',
preload: true,
tags: ['tag1', 'tag2'],
uid: 'test-uid',
version: 1,
controls: new DashboardControls({
refreshPicker: new SceneRefreshPicker({
refresh: '5s',
intervals: ['5s', '10s', '30s'],
autoEnabled: true,
autoMinInterval: '5s',
autoValue: '5s',
isOnCanvas: true,
primary: true,
withText: true,
minRefreshInterval: '5s',
}),
timePicker: new SceneTimePicker({
isOnCanvas: true,
hidePicker: true,
}),
}),
$timeRange: new SceneTimeRange({
timeZone: 'UTC',
from: 'now-1h',
to: 'now',
weekStart: 'monday',
fiscalYearStartMonth: 1,
UNSAFE_nowDelay: '1m',
refreshOnActivate: {
afterMs: 10,
percent: 0.1,
},
}),
body,
};
}
describe('dynamic layouts', () => {
it('should transform scene with rows layout with default grids in rows to save model schema v2', () => {
const scene = setupDashboardScene(
getMinimalSceneState(
new RowsLayoutManager({
rows: [
new RowItem({
layout: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [
new DashboardGridItem({
y: 0,
height: 10,
body: new VizPanel({}),
}),
],
}),
}),
}),
],
})
)
);
const result = transformSceneToSaveModelSchemaV2(scene);
expect(result.layout.kind).toBe('RowsLayout');
const rowsLayout = result.layout.spec as RowsLayoutSpec;
expect(rowsLayout.rows.length).toBe(1);
expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow');
expect(rowsLayout.rows[0].spec.layout.kind).toBe('GridLayout');
});
it('should transform scene with rows layout with multiple rows with different grids to save model schema v2', () => {
const scene = setupDashboardScene(
getMinimalSceneState(
new RowsLayoutManager({
rows: [
new RowItem({
layout: new ResponsiveGridLayoutManager({
layout: new SceneCSSGridLayout({
children: [
new ResponsiveGridItem({
body: new VizPanel({}),
}),
],
}),
}),
}),
new RowItem({
layout: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [
new DashboardGridItem({
y: 0,
height: 10,
body: new VizPanel({}),
}),
],
}),
}),
}),
],
})
)
);
const result = transformSceneToSaveModelSchemaV2(scene);
expect(result.layout.kind).toBe('RowsLayout');
const rowsLayout = result.layout.spec as RowsLayoutSpec;
expect(rowsLayout.rows.length).toBe(2);
expect(rowsLayout.rows[0].kind).toBe('RowsLayoutRow');
expect(rowsLayout.rows[0].spec.layout.kind).toBe('ResponsiveGridLayout');
expect(rowsLayout.rows[0].spec.layout.spec.items[0].kind).toBe('ResponsiveGridLayoutItem');
expect(rowsLayout.rows[1].spec.layout.kind).toBe('GridLayout');
expect(rowsLayout.rows[1].spec.layout.spec.items[0].kind).toBe('GridLayoutItem');
});
it('should transform scene with responsive grid layout to schema v2', () => {
const scene = setupDashboardScene(
getMinimalSceneState(
new ResponsiveGridLayoutManager({
layout: new SceneCSSGridLayout({
autoRows: 'rowString',
templateColumns: 'colString',
children: [
new ResponsiveGridItem({
body: new VizPanel({}),
}),
new ResponsiveGridItem({
body: new VizPanel({}),
}),
],
}),
})
)
);
const result = transformSceneToSaveModelSchemaV2(scene);
expect(result.layout.kind).toBe('ResponsiveGridLayout');
const respGridLayout = result.layout.spec as ResponsiveGridLayoutSpec;
expect(respGridLayout.col).toBe('colString');
expect(respGridLayout.row).toBe('rowString');
expect(respGridLayout.items.length).toBe(2);
expect(respGridLayout.items[0].kind).toBe('ResponsiveGridLayoutItem');
});
});
const annotationLayer1 = new DashboardAnnotationsDataLayer({
key: 'layer1',
query: {
@@ -7,7 +7,6 @@ import {
dataLayers,
SceneDataQuery,
SceneDataTransformer,
SceneGridRow,
SceneVariableSet,
VizPanel,
} from '@grafana/scenes';
@@ -24,7 +23,6 @@ import {
DataTransformerConfig,
PanelQuerySpec,
DataQueryKind,
GridLayoutItemKind,
QueryOptionsSpec,
QueryVariableKind,
TextVariableKind,
@@ -38,27 +36,13 @@ import {
DataLink,
LibraryPanelKind,
Element,
RepeatOptions,
GridLayoutRowKind,
DashboardCursorSync,
FieldConfig,
FieldColor,
GridLayoutKind,
RowsLayoutKind,
ResponsiveGridLayoutKind,
ResponsiveGridLayoutItemKind,
} from '../../../../../packages/grafana-schema/src/schema/dashboard/v2alpha0';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
import { ResponsiveGridItem } from '../scene/layout-responsive-grid/ResponsiveGridItem';
import { ResponsiveGridLayoutManager } from '../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { isClonedKey } from '../utils/clone';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import {
getLibraryPanelBehavior,
@@ -66,10 +50,9 @@ import {
getQueryRunnerFor,
getVizPanelKeyForPanelId,
isLibraryPanel,
calculateGridItemDimensions,
} from '../utils/utils';
import { GRID_ROW_HEIGHT } from './const';
import { getLayout } from './layoutSerializers/utils';
import { sceneVariablesSetToSchemaV2Variables } from './sceneVariablesSetToVariables';
import { colorIdEnumToColorIdV2, transformCursorSynctoEnum } from './transformToV2TypesUtils';
@@ -127,7 +110,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
// EOF annotations
// layout
layout: getLayout(sceneDash.body, isSnapshot),
layout: getLayout(sceneDash.body),
// EOF layout
};
@@ -144,75 +127,6 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps
}
}
function getLayout(
layoutManager: DashboardLayoutManager,
isSnapshot?: boolean
): GridLayoutKind | RowsLayoutKind | ResponsiveGridLayoutKind {
if (layoutManager instanceof DefaultGridLayoutManager) {
return getGridLayout(layoutManager, isSnapshot);
} else if (layoutManager instanceof RowsLayoutManager) {
return {
kind: 'RowsLayout',
spec: {
rows: layoutManager.state.rows.map((row) => {
if (row.state.layout instanceof RowsLayoutManager) {
throw new Error('Nesting row layouts is not supported');
}
let layout: GridLayoutKind | ResponsiveGridLayoutKind | undefined = undefined;
if (row.state.layout instanceof DefaultGridLayoutManager) {
layout = getGridLayout(row.state.layout, isSnapshot);
} else if (row.state.layout instanceof ResponsiveGridLayoutManager) {
layout = {
kind: 'ResponsiveGridLayout',
spec: {
items: getResponsiveGridLayoutItems(row.state.layout),
col:
row.state.layout.state.layout.state.templateColumns?.toString() ??
ResponsiveGridLayoutManager.defaultCSS.templateColumns,
row:
row.state.layout.state.layout.state.autoRows?.toString() ??
ResponsiveGridLayoutManager.defaultCSS.autoRows,
},
};
}
if (!layout) {
throw new Error('Unsupported layout type');
}
return {
kind: 'RowsLayoutRow',
spec: {
title: row.state.title,
collapsed: row.state.isCollapsed ?? false,
layout: layout,
},
};
}),
},
};
} else if (layoutManager instanceof ResponsiveGridLayoutManager) {
return {
kind: 'ResponsiveGridLayout',
spec: {
items: getResponsiveGridLayoutItems(layoutManager),
col:
layoutManager.state.layout.state.templateColumns?.toString() ??
ResponsiveGridLayoutManager.defaultCSS.templateColumns,
row: layoutManager.state.layout.state.autoRows?.toString() ?? ResponsiveGridLayoutManager.defaultCSS.autoRows,
},
};
}
throw new Error('Unsupported layout type');
}
function getGridLayout(layoutManager: DefaultGridLayoutManager, isSnapshot?: boolean): GridLayoutKind {
return {
kind: 'GridLayout',
spec: {
items: getGridLayoutItems(layoutManager, isSnapshot),
},
};
}
function getCursorSync(state: DashboardSceneState) {
const cursorSync = state.$behaviors?.find((b): b is behaviors.CursorSync => b instanceof behaviors.CursorSync)?.state
.sync;
@@ -231,146 +145,6 @@ function getLiveNow(state: DashboardSceneState) {
return Boolean(liveNow);
}
function getGridLayoutItems(
body: DefaultGridLayoutManager,
isSnapshot?: boolean
): Array<GridLayoutItemKind | GridLayoutRowKind> {
let elements: Array<GridLayoutItemKind | GridLayoutRowKind> = [];
for (const child of body.state.grid.state.children) {
if (child instanceof DashboardGridItem) {
// TODO: handle panel repeater scenario
if (child.state.variableName) {
elements = elements.concat(repeaterToLayoutItems(child, isSnapshot));
} else {
elements.push(gridItemToGridLayoutItemKind(child, isSnapshot));
}
} else if (child instanceof SceneGridRow) {
if (isClonedKey(child.state.key!) && !isSnapshot) {
// Skip repeat rows
continue;
}
elements.push(gridRowToLayoutRowKind(child, isSnapshot));
}
}
return elements;
}
function getResponsiveGridLayoutItems(body: ResponsiveGridLayoutManager): ResponsiveGridLayoutItemKind[] {
const items: ResponsiveGridLayoutItemKind[] = [];
for (const child of body.state.layout.state.children) {
if (child instanceof ResponsiveGridItem) {
items.push({
kind: 'ResponsiveGridLayoutItem',
spec: {
element: {
kind: 'ElementReference',
name: child.state?.body?.state.key ?? 'DefaultName',
},
},
});
}
}
return items;
}
export function gridItemToGridLayoutItemKind(
gridItem: DashboardGridItem,
isSnapshot = false,
yOverride?: number
): GridLayoutItemKind {
let elementGridItem: GridLayoutItemKind | undefined;
let x = 0,
y = 0,
width = 0,
height = 0;
let gridItem_ = gridItem;
if (!(gridItem_.state.body instanceof VizPanel)) {
throw new Error('DashboardGridItem body expected to be VizPanel');
}
// Get the grid position and size
height = (gridItem_.state.variableName ? gridItem_.state.itemHeight : gridItem_.state.height) ?? 0;
x = gridItem_.state.x ?? 0;
y = gridItem_.state.y ?? 0;
width = gridItem_.state.width ?? 0;
const repeatVar = gridItem_.state.variableName;
// FIXME: which name should we use for the element reference, key or something else ?
const elementName = gridItem_.state.body.state.key ?? 'DefaultName';
elementGridItem = {
kind: 'GridLayoutItem',
spec: {
x,
y: yOverride ?? y,
width: width,
height: height,
element: {
kind: 'ElementReference',
name: elementName,
},
},
};
if (repeatVar) {
const repeat: RepeatOptions = {
mode: 'variable',
value: repeatVar,
};
if (gridItem_.state.maxPerRow) {
repeat.maxPerRow = gridItem_.getMaxPerRow();
}
if (gridItem_.state.repeatDirection) {
repeat.direction = gridItem_.getRepeatDirection();
}
elementGridItem.spec.repeat = repeat;
}
if (!elementGridItem) {
throw new Error('Unsupported grid item type');
}
return elementGridItem;
}
function getRowRepeat(row: SceneGridRow): RepeatOptions | undefined {
if (row.state.$behaviors) {
for (const behavior of row.state.$behaviors) {
if (behavior instanceof RowRepeaterBehavior) {
return { value: behavior.state.variableName, mode: 'variable' };
}
}
}
return undefined;
}
function gridRowToLayoutRowKind(row: SceneGridRow, isSnapshot = false): GridLayoutRowKind {
const children = row.state.children.map((child) => {
if (!(child instanceof DashboardGridItem)) {
throw new Error('Unsupported row child type');
}
const y = (child.state.y ?? 0) - (row.state.y ?? 0) - GRID_ROW_HEIGHT;
return gridItemToGridLayoutItemKind(child, isSnapshot, y);
});
return {
kind: 'GridLayoutRow',
spec: {
title: row.state.title,
y: row.state.y ?? 0,
collapsed: Boolean(row.state.isCollapsed),
elements: children,
repeat: getRowRepeat(row),
},
};
}
function getElements(state: DashboardSceneState) {
const panels = state.body.getVizPanels() ?? [];
@@ -577,60 +351,6 @@ function createElements(panels: Element[]): Record<string, Element> {
}, {});
}
function repeaterToLayoutItems(repeater: DashboardGridItem, isSnapshot = false): GridLayoutItemKind[] {
if (!isSnapshot) {
return [gridItemToGridLayoutItemKind(repeater)];
} else {
if (repeater.state.body instanceof VizPanel && isLibraryPanel(repeater.state.body)) {
// TODO: implement
// const { x = 0, y = 0, width: w = 0, height: h = 0 } = repeater.state;
// return [vizPanelToPanel(repeater.state.body, { x, y, w, h }, isSnapshot)];
return [];
}
if (repeater.state.repeatedPanels) {
const { h, w, columnCount } = calculateGridItemDimensions(repeater);
const panels = repeater.state.repeatedPanels!.map((panel, index) => {
let x = 0,
y = 0;
if (repeater.state.repeatDirection === 'v') {
x = repeater.state.x!;
y = index * h;
} else {
x = (index % columnCount) * w;
y = repeater.state.y! + Math.floor(index / columnCount) * h;
}
const gridPos = { x, y, w, h };
const result: GridLayoutItemKind = {
kind: 'GridLayoutItem',
spec: {
x: gridPos.x,
y: gridPos.y,
width: gridPos.w,
height: gridPos.h,
repeat: {
mode: 'variable',
value: repeater.state.variableName!,
maxPerRow: repeater.getMaxPerRow(),
direction: repeater.state.repeatDirection,
},
element: {
kind: 'ElementReference',
name: panel.state.key!,
},
},
};
return result;
});
return panels;
}
return [];
}
}
function getVariables(oldDash: DashboardSceneState) {
const variablesSet = oldDash.$variables;
@@ -788,8 +508,12 @@ function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
if (!('hideTimepicker' in dash.timeSettings) || typeof dash.timeSettings.hideTimepicker !== 'boolean') {
throw new Error('HideTimepicker is not a boolean');
}
if (!('weekStart' in dash.timeSettings) || typeof dash.timeSettings.weekStart !== 'string') {
throw new Error('WeekStart is not a string');
if (
'weekStart' in dash.timeSettings &&
typeof dash.timeSettings.weekStart === 'string' &&
!['saturday', 'sunday', 'monday'].includes(dash.timeSettings.weekStart)
) {
throw new Error('WeekStart should be one of "saturday", "sunday" or "monday"');
}
if (!('fiscalYearStartMonth' in dash.timeSettings) || typeof dash.timeSettings.fiscalYearStartMonth !== 'number') {
throw new Error('FiscalYearStartMonth is not a number');
@@ -15,6 +15,7 @@ import {
Switch,
TagsInput,
TextArea,
WeekStart,
} from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
@@ -122,7 +123,7 @@ export class GeneralSettingsEditView
});
};
public onWeekStartChange = (value: string) => {
public onWeekStartChange = (value: WeekStart) => {
this.getTimeRange().setState({ weekStart: value });
};
@@ -26,7 +26,7 @@ describe('DashboardModelCompatibilityWrapper', () => {
expect(wrapper.links).toEqual([NEW_LINK]);
expect(wrapper.time.from).toBe('now-6h');
expect(wrapper.timezone).toBe('America/New_York');
expect(wrapper.weekStart).toBe('friday');
expect(wrapper.weekStart).toBe('saturday');
expect(wrapper.timepicker.refresh_intervals![0]).toEqual('5s');
expect(wrapper.timepicker.hidden).toEqual(true);
expect(wrapper.panels).toHaveLength(5);
@@ -141,7 +141,7 @@ function setup() {
},
},
$timeRange: new SceneTimeRange({
weekStart: 'friday',
weekStart: 'saturday',
timeZone: 'America/New_York',
}),
$data: new DashboardDataLayerSet({
@@ -41,6 +41,7 @@ import {
GridLayoutItemKind,
} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { DashboardLink, DataTransformerConfig } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen';
import { WeekStart } from '@grafana/ui';
import {
AnnoKeyCreatedBy,
AnnoKeyDashboardGnetId,
@@ -160,7 +161,8 @@ export function ensureV2Response(
fiscalYearStartMonth: dashboard.fiscalYearStartMonth || timeSettingsDefaults.fiscalYearStartMonth,
hideTimepicker: dashboard.timepicker?.hidden || timeSettingsDefaults.hideTimepicker,
quickRanges: dashboard.timepicker?.time_options || timeSettingsDefaults.quickRanges,
weekStart: dashboard.weekStart || timeSettingsDefaults.weekStart,
// casting WeekStart here to avoid editing old schema
weekStart: (dashboard.weekStart as WeekStart) || timeSettingsDefaults.weekStart,
nowDelay: dashboard.timepicker?.nowDelay || timeSettingsDefaults.nowDelay,
},
links: dashboard.links || [],
@@ -4,13 +4,13 @@ import * as React from 'react';
import { rangeUtil, TimeZone } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { CollapsableSection, Field, Input, Switch, TimeZonePicker, WeekStartPicker } from '@grafana/ui';
import { CollapsableSection, Field, Input, Switch, TimeZonePicker, WeekStart, WeekStartPicker } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { AutoRefreshIntervals } from './AutoRefreshIntervals';
interface Props {
onWeekStartChange: (weekStart: string) => void;
onWeekStartChange: (weekStart: WeekStart) => void;
onTimeZoneChange: (timeZone: TimeZone) => void;
onRefreshIntervalChange: (interval: string[]) => void;
onNowDelayChange: (nowDelay: string) => void;
@@ -62,7 +62,7 @@ export class TimePickerSettings extends PureComponent<Props, State> {
this.props.onTimeZoneChange(timeZone);
};
onWeekStartChange = (weekStart: string) => {
onWeekStartChange = (weekStart: WeekStart) => {
this.props.onWeekStartChange(weekStart);
};
+11 -11
View File
@@ -3831,11 +3831,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:5.41.2":
version: 5.41.2
resolution: "@grafana/scenes-react@npm:5.41.2"
"@grafana/scenes-react@npm:5.42.0":
version: 5.42.0
resolution: "@grafana/scenes-react@npm:5.42.0"
dependencies:
"@grafana/scenes": "npm:5.41.2"
"@grafana/scenes": "npm:5.42.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3846,13 +3846,13 @@ __metadata:
"@grafana/ui": ^11.0.0
react: ^18.0.0
react-dom: ^18.0.0
checksum: 10/43684bb4d169666b9edb44f5804eb533fcba531b6d161837306def3611bfa606ea2fb3ad2a9aa374ec7d6757d7560df55217593927c0874254d3a0cae8a215f0
checksum: 10/c94db6d57b02be5f960e44dc0c4be46e5e4708ede34f2f4e30934134543db88c8b553aceb89d9b3ae4dbf5d1eab1f48e534eb9a31d5c99b8908f715e45cb6dcf
languageName: node
linkType: hard
"@grafana/scenes@npm:5.41.2":
version: 5.41.2
resolution: "@grafana/scenes@npm:5.41.2"
"@grafana/scenes@npm:5.42.0":
version: 5.42.0
resolution: "@grafana/scenes@npm:5.42.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3869,7 +3869,7 @@ __metadata:
"@grafana/ui": ">=10.4"
react: ^18.0.0
react-dom: ^18.0.0
checksum: 10/68d6f722e7298123271785f0c38d47f9b1c000ad51bc1f4bfdd90b6ad83d17e69a18261263f4e9cbda6c7062a72514cd91a22e7189b3a0429f28b96f7f928dc1
checksum: 10/3232a499b839a45c8eed924441e4423d1b1d9f6b4ea6725437d357c1afea93d9ab300c46319b1ddab76b0da1197905b22c3abe8095d78c3320b1befb4731bd24
languageName: node
linkType: hard
@@ -18166,8 +18166,8 @@ __metadata:
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/saga-icons": "workspace:*"
"@grafana/scenes": "npm:5.41.2"
"@grafana/scenes-react": "npm:5.41.2"
"@grafana/scenes": "npm:5.42.0"
"@grafana/scenes-react": "npm:5.42.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/tsconfig": "npm:^2.0.0"