From 2645958c8cca71129ad1c97b6d0d68b5e39b2011 Mon Sep 17 00:00:00 2001 From: Leonor Oliveira <9090754+leonorfmartins@users.noreply.github.com> Date: Wed, 26 Jun 2024 09:19:40 +0100 Subject: [PATCH 01/16] Use global function to compare any entity to both stores (#89282) * WIP implement generic compare interface * Use global compare fn for all entities * Lint * Update pkg/apiserver/rest/dualwriter.go Co-authored-by: Dan Cech * Don't need to hash, just compare bytes * Fix tests --------- Co-authored-by: Dan Cech --- pkg/apiserver/rest/dualwriter.go | 26 +++++++++++++++++++-- pkg/apiserver/rest/dualwriter_mode1.go | 4 ---- pkg/apiserver/rest/dualwriter_mode1_test.go | 7 +++--- pkg/apiserver/rest/dualwriter_mode2.go | 4 ---- pkg/apiserver/rest/dualwriter_mode3.go | 4 ---- pkg/apiserver/rest/dualwriter_mode4.go | 4 ---- pkg/apiserver/rest/dualwriter_test.go | 24 +++++++++++++++++++ pkg/registry/apis/dashboard/storage.go | 6 ----- pkg/registry/apis/folders/storage.go | 6 ----- pkg/registry/apis/peakq/storage.go | 6 ----- pkg/registry/apis/playlist/storage.go | 15 ------------ pkg/registry/apis/scope/storage.go | 6 ----- pkg/registry/apis/service/storage.go | 6 ----- 13 files changed, 52 insertions(+), 66 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index f979c932d6b..09c1e1ed717 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -1,7 +1,9 @@ package rest import ( + "bytes" "context" + "encoding/json" "errors" "fmt" @@ -35,8 +37,6 @@ type Storage interface { rest.CreaterUpdater rest.GracefulDeleter rest.CollectionDeleter - // Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) - Compare(storageObj, legacyObj runtime.Object) bool } // LegacyStorage is a storage implementation that writes to the Grafana SQL database. @@ -207,3 +207,25 @@ func SetDualWritingMode( return NewDualWriter(currentMode, legacy, storage, reg), nil } + +var defaultConverter = runtime.UnstructuredConverter(runtime.DefaultUnstructuredConverter) + +// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) +func Compare(storageObj, legacyObj runtime.Object) bool { + return bytes.Equal(removeMeta(storageObj), removeMeta(legacyObj)) +} + +func removeMeta(obj runtime.Object) []byte { + cpy := obj.DeepCopyObject() + unstObj, err := defaultConverter.ToUnstructured(cpy) + if err != nil { + return nil + } + // we don't want to compare meta fields + delete(unstObj, "meta") + jsonObj, err := json.Marshal(cpy) + if err != nil { + return nil + } + return jsonObj +} diff --git a/pkg/apiserver/rest/dualwriter_mode1.go b/pkg/apiserver/rest/dualwriter_mode1.go index 4fb6b40d1ba..75aa3a10c43 100644 --- a/pkg/apiserver/rest/dualwriter_mode1.go +++ b/pkg/apiserver/rest/dualwriter_mode1.go @@ -243,7 +243,3 @@ func (d *DualWriterMode1) NewList() runtime.Object { func (d *DualWriterMode1) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { return d.Legacy.ConvertToTable(ctx, object, tableOptions) } - -func (d *DualWriterMode1) Compare(storageObj, legacyObj runtime.Object) bool { - return d.Storage.Compare(storageObj, legacyObj) -} diff --git a/pkg/apiserver/rest/dualwriter_mode1_test.go b/pkg/apiserver/rest/dualwriter_mode1_test.go index 998d683bedf..a9efdf22268 100644 --- a/pkg/apiserver/rest/dualwriter_mode1_test.go +++ b/pkg/apiserver/rest/dualwriter_mode1_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" @@ -15,10 +16,10 @@ import ( "k8s.io/apiserver/pkg/apis/example" ) -var exampleObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: example.PodSpec{}, Status: example.PodStatus{}} -var exampleObjNoRV = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: ""}, Spec: example.PodSpec{}, Status: example.PodStatus{}} +var exampleObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} +var exampleObjNoRV = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var exampleObjDifferentRV = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "3"}, Spec: example.PodSpec{}, Status: example.PodStatus{}} -var anotherObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "2"}, Spec: example.PodSpec{}, Status: example.PodStatus{}} +var anotherObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "2"}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}} var failingObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "object-fail", ResourceVersion: "2"}, Spec: example.PodSpec{}, Status: example.PodStatus{}} var exampleList = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{}, Items: []example.Pod{*exampleObj}} var anotherList = &example.PodList{Items: []example.Pod{*anotherObj}} diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index ff269800563..954dd1d7668 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -306,10 +306,6 @@ func (d *DualWriterMode2) ConvertToTable(ctx context.Context, object runtime.Obj return d.Storage.ConvertToTable(ctx, object, tableOptions) } -func (d *DualWriterMode2) Compare(storageObj, legacyObj runtime.Object) bool { - return d.Storage.Compare(storageObj, legacyObj) -} - func parseList(legacyList []runtime.Object) (metainternalversion.ListOptions, map[string]int, error) { options := metainternalversion.ListOptions{} originKeys := []string{} diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index f24d58627ae..12bf63865a1 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -155,7 +155,3 @@ func (d *DualWriterMode3) NewList() runtime.Object { func (d *DualWriterMode3) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { return d.Storage.ConvertToTable(ctx, object, tableOptions) } - -func (d *DualWriterMode3) Compare(storageObj, legacyObj runtime.Object) bool { - return d.Storage.Compare(storageObj, legacyObj) -} diff --git a/pkg/apiserver/rest/dualwriter_mode4.go b/pkg/apiserver/rest/dualwriter_mode4.go index 7985e69332e..34c8a09af56 100644 --- a/pkg/apiserver/rest/dualwriter_mode4.go +++ b/pkg/apiserver/rest/dualwriter_mode4.go @@ -83,7 +83,3 @@ func (d *DualWriterMode4) NewList() runtime.Object { func (d *DualWriterMode4) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { return d.Storage.ConvertToTable(ctx, object, tableOptions) } - -func (d *DualWriterMode4) Compare(storageObj, legacyObj runtime.Object) bool { - return d.Storage.Compare(storageObj, legacyObj) -} diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index 6c9a8da9619..486e11a5160 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/runtime" ) func TestSetDualWritingMode(t *testing.T) { @@ -58,3 +59,26 @@ func TestSetDualWritingMode(t *testing.T) { assert.Equal(t, val, fmt.Sprint(tt.expectedMode)) } } + +func TestCompare(t *testing.T) { + testCase := []struct { + name string + input runtime.Object + expected bool + }{ + { + name: "should return true when both objects are the same", + input: exampleObj, + expected: true, + }, + { + name: "should return false when objects are different", + input: anotherObj, + }, + } + for _, tt := range testCase { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, Compare(tt.input, exampleObj)) + }) + } +} diff --git a/pkg/registry/apis/dashboard/storage.go b/pkg/registry/apis/dashboard/storage.go index c426dca409a..064ed31aea1 100644 --- a/pkg/registry/apis/dashboard/storage.go +++ b/pkg/registry/apis/dashboard/storage.go @@ -66,9 +66,3 @@ func newStorage(scheme *runtime.Scheme) (*storage, error) { }) return &storage{Store: store}, nil } - -// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) -func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - //TODO: define the comparison logic between a dashboard returned by the storage and a dashboard returned by the legacy storage - return false -} diff --git a/pkg/registry/apis/folders/storage.go b/pkg/registry/apis/folders/storage.go index 1e36d8f8d71..3d20ee20f89 100644 --- a/pkg/registry/apis/folders/storage.go +++ b/pkg/registry/apis/folders/storage.go @@ -40,9 +40,3 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, le } return &storage{Store: store}, nil } - -// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) -func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - //TODO: define the comparison logic between a folder returned by the storage and a folder returned by the legacy storage - return false -} diff --git a/pkg/registry/apis/peakq/storage.go b/pkg/registry/apis/peakq/storage.go index 849134b1f42..32df4308595 100644 --- a/pkg/registry/apis/peakq/storage.go +++ b/pkg/registry/apis/peakq/storage.go @@ -62,9 +62,3 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (* } return &storage{Store: store}, nil } - -// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) -func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - //TODO: define the comparison logic between a query template returned by the storage and a query template returned by the legacy storage - return false -} diff --git a/pkg/registry/apis/playlist/storage.go b/pkg/registry/apis/playlist/storage.go index 230cbfef6ca..ead063391b7 100644 --- a/pkg/registry/apis/playlist/storage.go +++ b/pkg/registry/apis/playlist/storage.go @@ -1,7 +1,6 @@ package playlist import ( - "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" @@ -41,17 +40,3 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, le } return &storage{Store: store}, nil } - -// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) -func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - accStr, err := meta.Accessor(storageObj) - if err != nil { - return false - } - accLegacy, err := meta.Accessor(legacyObj) - if err != nil { - return false - } - - return accStr.GetName() == accLegacy.GetName() -} diff --git a/pkg/registry/apis/scope/storage.go b/pkg/registry/apis/scope/storage.go index 8b1216b3af2..453eea59712 100644 --- a/pkg/registry/apis/scope/storage.go +++ b/pkg/registry/apis/scope/storage.go @@ -207,9 +207,3 @@ func SelectableScopeNodeFields(obj *scope.ScopeNode) fields.Set { "spec.parentName": parentName, }) } - -// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) -func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - //TODO: define the comparison logic between a scope returned by the storage and a scope returned by the legacy storage - return false -} diff --git a/pkg/registry/apis/service/storage.go b/pkg/registry/apis/service/storage.go index 30a654d9001..86b1fbd3a2e 100644 --- a/pkg/registry/apis/service/storage.go +++ b/pkg/registry/apis/service/storage.go @@ -62,9 +62,3 @@ func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (* } return &storage{Store: store}, nil } - -// Compare asserts on the equality of objects returned from both stores (object storage and legacy storage) -func (s *storage) Compare(storageObj, legacyObj runtime.Object) bool { - //TODO: define the comparison logic between a generic object returned by the storage and a generic object returned by the legacy storage - return false -} From b1ce7e8a8370e07aecb4b10c0b1810db888c3d8a Mon Sep 17 00:00:00 2001 From: Nihal <38865967+wasim-nihal@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:51:37 +0530 Subject: [PATCH 02/16] Alerting: Reduce cyclomatic complexity of PrepareRuleGroupStatuses (#89649) * reduce cyclomatic complexity of PrepareRuleGroupStatuses. see https://github.com/grafana/grafana/issues/88670 Signed-off-by: Syed Nihal * reduce cyclomatic complexity for PrepareRuleGroupStatuses in pkg/services/ngalert/api/api_prometheus.go Signed-off-by: Syed Nihal * reduce cyclomatic complexity for PrepareRuleGroupStatuses in pkg/services/ngalert/api/api_prometheus.go Signed-off-by: Syed Nihal --------- Signed-off-by: Syed Nihal --- pkg/services/ngalert/api/api_prometheus.go | 96 +++++++++++----------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index d5c6343f780..7953772e5b1 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -235,9 +235,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } -// TODO: Refactor this function to reduce the cylomatic complexity -// -//nolint:gocyclo func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ @@ -330,28 +327,7 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager ruleNamesSet[rn] = struct{}{} } - // Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID, - // but in this API all rules belong to the same organization. Also filter by rule name if - // it was provided as a query param. - groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule) - for _, rule := range ruleList { - if len(ruleNamesSet) > 0 { - if _, exists := ruleNamesSet[rule.Title]; !exists { - continue - } - } - - groupKey := rule.GetGroupKey() - ruleGroup := groupedRules[groupKey] - ruleGroup = append(ruleGroup, rule) - groupedRules[groupKey] = ruleGroup - } - // Sort the rules in each rule group by index. We do this at the end instead of - // after each append to avoid having to sort each group multiple times. - for _, groupRules := range groupedRules { - ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(groupRules) - } - + groupedRules := getGroupedRules(ruleList, ruleNamesSet) rulesTotals := make(map[string]int64, len(groupedRules)) for groupKey, rules := range groupedRules { folder, ok := opts.Namespaces[groupKey.NamespaceUID] @@ -377,27 +353,7 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager } if len(withStates) > 0 { - // Filtering is weird but firing, pending, and normal filters also need to be - // applied to the rule. Others such as nodata and error should have no effect. - // This is to match the current behavior in the UI. - filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules)) - for _, rule := range ruleGroup.Rules { - var state *eval.State - switch rule.State { - case "normal", "inactive": - state = util.Pointer(eval.Normal) - case "alerting", "firing": - state = util.Pointer(eval.Alerting) - case "pending": - state = util.Pointer(eval.Pending) - } - if state != nil { - if _, ok := withStatesFast[*state]; ok { - filteredRules = append(filteredRules, rule) - } - } - } - ruleGroup.Rules = filteredRules + filterRules(ruleGroup, withStatesFast) } if limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > limitRulesPerGroup { @@ -418,6 +374,54 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager return ruleResponse } +func getGroupedRules(ruleList ngmodels.RulesGroup, ruleNamesSet map[string]struct{}) map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule { + // Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID, + // but in this API all rules belong to the same organization. Also filter by rule name if + // it was provided as a query param. + groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule) + for _, rule := range ruleList { + if len(ruleNamesSet) > 0 { + if _, exists := ruleNamesSet[rule.Title]; !exists { + continue + } + } + groupKey := rule.GetGroupKey() + ruleGroup := groupedRules[groupKey] + ruleGroup = append(ruleGroup, rule) + groupedRules[groupKey] = ruleGroup + } + // Sort the rules in each rule group by index. We do this at the end instead of + // after each append to avoid having to sort each group multiple times. + for _, groupRules := range groupedRules { + ngmodels.AlertRulesBy(ngmodels.AlertRulesByIndex).Sort(groupRules) + } + return groupedRules +} + +func filterRules(ruleGroup *apimodels.RuleGroup, withStatesFast map[eval.State]struct{}) { + // Filtering is weird but firing, pending, and normal filters also need to be + // applied to the rule. Others such as nodata and error should have no effect. + // This is to match the current behavior in the UI. + filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules)) + for _, rule := range ruleGroup.Rules { + var state *eval.State + switch rule.State { + case "normal", "inactive": + state = util.Pointer(eval.Normal) + case "alerting", "firing": + state = util.Pointer(eval.Alerting) + case "pending": + state = util.Pointer(eval.Pending) + } + if state != nil { + if _, ok := withStatesFast[*state]; ok { + filteredRules = append(filteredRules, rule) + } + } + } + ruleGroup.Rules = filteredRules +} + // This is the same as matchers.Matches but avoids the need to create a LabelSet func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { for _, m := range matchers { From 7b0362b2286f6c1613b948582d6e8d2cc6ccfb0f Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 26 Jun 2024 09:37:17 +0100 Subject: [PATCH 03/16] Drawer: Improve performance when resizing (#89691) apply dynamic styles as styles instead of emotion classes --- .../src/components/Drawer/Drawer.tsx | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index f3b17f2511f..9d5defacf03 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -54,6 +54,12 @@ export interface Props { onClose: () => void; } +const drawerSizes = { + sm: { width: '25vw', minWidth: 384 }, + md: { width: '50vw', minWidth: 568 }, + lg: { width: '75vw', minWidth: 744 }, +}; + export function Drawer({ children, onClose, @@ -68,7 +74,7 @@ export function Drawer({ const [drawerWidth, onMouseDown, onTouchStart] = useResizebleDrawer(); const styles = useStyles2(getStyles); - const sizeStyles = useStyles2(getSizeStyles, size, drawerWidth ?? width); + const wrapperStyles = useStyles2(getWrapperStyles, size); const dragStyles = useStyles2(getDragStyles); const overlayRef = React.useRef(null); @@ -85,8 +91,9 @@ export function Drawer({ // Adds body class while open so the toolbar nav can hide some actions while drawer is open useBodyClassWhileOpen(); - const rootClass = cx(styles.drawer, sizeStyles); const content =
{children}
; + const overrideWidth = drawerWidth ?? width ?? drawerSizes[size].width; + const minWidth = drawerSizes[size].minWidth; return ( { }; }; -const drawerSizes = { - sm: { width: '25vw', minWidth: 384 }, - md: { width: '50vw', minWidth: 568 }, - lg: { width: '75vw', minWidth: 744 }, -}; - -function getSizeStyles(theme: GrafanaTheme2, size: 'sm' | 'md' | 'lg', overrideWidth: number | string | undefined) { - let width = overrideWidth ?? drawerSizes[size].width; - let minWidth = drawerSizes[size].minWidth; - +function getWrapperStyles(theme: GrafanaTheme2, size: 'sm' | 'md' | 'lg') { return css({ - '.rc-drawer-content-wrapper': { - label: `drawer-content-wrapper-${size}`, - width: width, - minWidth: minWidth, - overflow: 'unset', + label: `drawer-content-wrapper-${size}`, + overflow: 'unset !important', - [theme.breakpoints.down('md')]: { - width: `calc(100% - ${theme.spacing(2)}) !important`, - minWidth: 0, - }, + [theme.breakpoints.down('md')]: { + width: `calc(100% - ${theme.spacing(2)}) !important`, + minWidth: 0, }, }); } From be2aa9a3a7ac08817adc7a94df1b04a97203cb95 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:10:00 +0100 Subject: [PATCH 04/16] Scenes: Implement 't a' shortcut (#89619) --- package.json | 2 +- .../dashboard-scene/scene/keyboardShortcuts.ts | 10 ++++++++++ yarn.lock | 10 +++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d012e5d4771..452a65bccfd 100644 --- a/package.json +++ b/package.json @@ -260,7 +260,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "^5.0.2", + "@grafana/scenes": "^5.3.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index f0edd586360..b5282772143 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -102,6 +102,7 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { handleZoomOut(scene); }, }); + keybindings.addBinding({ key: 'ctrl+z', onTrigger: () => { @@ -109,6 +110,15 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { }, }); + // Relative -> Absolute time range + keybindings.addBinding({ + key: 't a', + onTrigger: () => { + const timePicker = dashboardSceneGraph.getTimePicker(scene); + timePicker?.toAbsolute(); + }, + }); + keybindings.addBinding({ key: 't left', onTrigger: () => { diff --git a/yarn.lock b/yarn.lock index e42cada06fb..393f7b62e03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3580,9 +3580,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^5.0.2": - version: 5.1.2 - resolution: "@grafana/scenes@npm:5.1.2" +"@grafana/scenes@npm:^5.3.0": + version: 5.3.0 + resolution: "@grafana/scenes@npm:5.3.0" dependencies: "@grafana/e2e-selectors": "npm:^11.0.0" "@leeoniya/ufuzzy": "npm:^1.0.14" @@ -3597,7 +3597,7 @@ __metadata: "@grafana/ui": ^10.4.1 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/814fe81537d267640cf0e4d91c1fc5805290fd6f46bbf37633edfbc8fbeae8a064a2b9540d482adb061235bd20148ddf246db6e41a8141380e98d49ca31638f3 + checksum: 10/625b7e009af1b79de8903eb2fbe9e2da3558e229d51e532bbb385bc72c316f9a5df9e1e81caff49e3b245dd1e00a019331f4ce11cdd980fe112917992c80c3c2 languageName: node linkType: hard @@ -17145,7 +17145,7 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:^5.0.2" + "@grafana/scenes": "npm:^5.3.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" From 54cfae8fef337dde7308b59e80781e623c7f893c Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 26 Jun 2024 10:37:33 +0100 Subject: [PATCH 05/16] Chore: Simplify keybinds e2e test (#89760) simplify keybinds e2e test --- e2e/scenes/various-suite/keybinds.spec.ts | 24 +++-------------------- e2e/various-suite/keybinds.spec.ts | 24 +++-------------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/e2e/scenes/various-suite/keybinds.spec.ts b/e2e/scenes/various-suite/keybinds.spec.ts index 88e256b5576..624731dbec4 100644 --- a/e2e/scenes/various-suite/keybinds.spec.ts +++ b/e2e/scenes/various-suite/keybinds.spec.ts @@ -41,7 +41,7 @@ describe.skip('Keyboard shortcuts', () => { e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); - it('multiple time range shortcuts should work', () => { + it('time range shortcuts should work', () => { cy.get('body').type('ge'); e2e.pages.Explore.General.container().should('be.visible'); @@ -61,28 +61,10 @@ describe.skip('Keyboard shortcuts', () => { expectedRange = `Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00`; // 1 min back e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - cy.log('Trying two shift-lefts'); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:03:00 to 2024-06-05 10:04:00`; // 1 min back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:02:00 to 2024-06-05 10:03:00`; // 2 mins back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - - cy.log('Trying two shift-lefts and a shift-right'); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:01:00 to 2024-06-05 10:02:00`; // 1 min back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:00:00 to 2024-06-05 10:01:00`; // 2 mins back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); + cy.log('Trying one shift-right'); cy.get('body').type('t{rightarrow}'); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:01:00 to 2024-06-05 10:02:00`; // 1 min forward (1 min back total) + expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`; // 1 min forward e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); }); diff --git a/e2e/various-suite/keybinds.spec.ts b/e2e/various-suite/keybinds.spec.ts index 37f3c8da9c0..3f51fe04037 100644 --- a/e2e/various-suite/keybinds.spec.ts +++ b/e2e/various-suite/keybinds.spec.ts @@ -42,7 +42,7 @@ describe('Keyboard shortcuts', () => { e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); - it('multiple time range shortcuts should work', () => { + it('time range shortcuts should work', () => { cy.get('body').type('ge'); e2e.pages.Explore.General.container().should('be.visible'); @@ -62,28 +62,10 @@ describe('Keyboard shortcuts', () => { expectedRange = `Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00`; // 1 min back e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - cy.log('Trying two shift-lefts'); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:03:00 to 2024-06-05 10:04:00`; // 1 min back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:02:00 to 2024-06-05 10:03:00`; // 2 mins back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - - cy.log('Trying two shift-lefts and a shift-right'); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:01:00 to 2024-06-05 10:02:00`; // 1 min back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); - cy.get('body').type('t{leftarrow}'); - e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:00:00 to 2024-06-05 10:01:00`; // 2 mins back - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); + cy.log('Trying one shift-right'); cy.get('body').type('t{rightarrow}'); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:01:00 to 2024-06-05 10:02:00`; // 1 min forward (1 min back total) + expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`; // 1 min forward e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); }); From 1040dc1baf8e4fac21d6d48c3bc920696fe50616 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 26 Jun 2024 13:18:50 +0300 Subject: [PATCH 06/16] Scopes: Implement clear search for nodes (#89678) --- .../scene/Scopes/ScopesDashboardsScene.tsx | 21 +++++-------------- .../scene/Scopes/ScopesScene.test.tsx | 17 --------------- .../scene/Scopes/ScopesTreeLevel.tsx | 20 +++++++++++------- .../scene/Scopes/testUtils.tsx | 4 +--- public/locales/en-US/grafana.json | 1 - public/locales/pseudo-LOCALE/grafana.json | 1 - 6 files changed, 19 insertions(+), 45 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx index 3fa727e583b..665561937ce 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { GrafanaTheme2, Scope, urlUtil } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { CustomScrollbar, Icon, IconButton, Input, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { CustomScrollbar, FilterInput, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { t } from 'app/core/internationalization'; @@ -70,23 +70,12 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps
- } - placeholder={t('scopes.suggestedDashboards.search', 'Search')} + model.changeSearchQuery('')} - /> - ) : undefined - } - onChange={(evt) => model.changeSearchQuery(evt.currentTarget.value)} + data-testid="scopes-dashboards-search" + onChange={(value) => model.changeSearchQuery(value)} />
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx index 839c76d5933..ae67ba1f558 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx @@ -45,7 +45,6 @@ import { queryDashboardsContainer, queryDashboardsExpand, renderDashboard, - getDashboardsClear, } from './testUtils'; jest.mock('@grafana/runtime', () => ({ @@ -286,22 +285,6 @@ describe('ScopesScene', () => { expect(queryDashboard('2')).not.toBeInTheDocument(); }); - it('Clears the filter', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getFiltersInput()); - await userEvents.click(getApplicationsExpand()); - await userEvents.click(getApplicationsSlothPictureFactorySelect()); - await userEvents.click(getFiltersApply()); - expect(getDashboard('1')).toBeInTheDocument(); - expect(getDashboard('2')).toBeInTheDocument(); - await userEvents.type(getDashboardsSearch(), '1'); - expect(queryDashboard('2')).not.toBeInTheDocument(); - await userEvents.click(getDashboardsClear()); - expect(getDashboardsSearch().value).toBe(''); - expect(getDashboard('1')).toBeInTheDocument(); - expect(getDashboard('2')).toBeInTheDocument(); - }); - it('Deduplicates the dashboards list', async () => { await userEvents.click(getDashboardsExpand()); await userEvents.click(getFiltersInput()); diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx index 399472cfa00..4c9963f1f53 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx @@ -1,10 +1,10 @@ import { css } from '@emotion/css'; import { debounce } from 'lodash'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; -import { Checkbox, Icon, IconButton, Input, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { Checkbox, FilterInput, IconButton, RadioButtonDot, useStyles2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { NodesMap, TreeScope } from './types'; @@ -37,18 +37,24 @@ export function ScopesTreeLevel({ const scopeNames = scopes.map(({ scopeName }) => scopeName); const anyChildExpanded = childNodesArr.some(({ isExpanded }) => isExpanded); + const [queryValue, setQueryValue] = useState(node.query); + useEffect(() => { + setQueryValue(node.query); + }, [node.query]); const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]); return ( <> {!anyChildExpanded && ( - } - className={styles.searchInput} + onQueryUpdate(nodePath, true, evt.currentTarget.value)} + onChange={(value) => { + setQueryValue(value); + onQueryUpdate(nodePath, true, value); + }} /> )} diff --git a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx index a66ed6a9e37..dc1fbe0d9fc 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx @@ -310,7 +310,6 @@ const selectors = { expand: 'scopes-dashboards-expand', container: 'scopes-dashboards-container', search: 'scopes-dashboards-search', - clear: 'scopes-dashboards-clear', loading: 'scopes-dashboards-loading', dashboard: (uid: string) => `scopes-dashboards-${uid}`, }, @@ -326,13 +325,12 @@ export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search); -export const getDashboardsClear = () => screen.getByTestId(selectors.dashboards.clear); export const queryAllDashboard = (uid: string) => screen.queryAllByTestId(selectors.dashboards.dashboard(uid)); export const queryDashboard = (uid: string) => screen.queryByTestId(selectors.dashboards.dashboard(uid)); export const getDashboard = (uid: string) => screen.getByTestId(selectors.dashboards.dashboard(uid)); export const getApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications')); -export const getApplicationsSearch = () => screen.getByTestId(selectors.tree.search('applications')); +export const getApplicationsSearch = () => screen.getByTestId(selectors.tree.search('applications')); export const queryApplicationsSlothPictureFactoryTitle = () => screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory')); export const getApplicationsSlothPictureFactoryTitle = () => diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 42e225bb28c..7d5a1812623 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1656,7 +1656,6 @@ "title": "Select scopes" }, "suggestedDashboards": { - "clear": "Clear search", "loading": "Loading dashboards", "search": "Search", "toggle": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 695dbb06a45..cee6b19f34e 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1656,7 +1656,6 @@ "title": "Ŝęľęčŧ şčőpęş" }, "suggestedDashboards": { - "clear": "Cľęäř şęäřčĥ", "loading": "Ŀőäđįʼnģ đäşĥþőäřđş", "search": "Ŝęäřčĥ", "toggle": { From 529d4e116995a67e0bec9d5c13da3bca2c1acb85 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 26 Jun 2024 13:35:04 +0300 Subject: [PATCH 07/16] Dashboards: Use dashboard_api k8s wrapper for save options also (#89598) --- .betterer.results | 3 + public/app/features/apiserver/client.ts | 9 ++- public/app/features/apiserver/types.ts | 6 +- .../api/browseDashboardsAPI.ts | 20 +++--- .../saving/useSaveDashboard.ts | 1 + .../features/dashboard/api/dashboard_api.ts | 71 ++++++++++++++++--- .../components/SaveDashboard/types.ts | 4 ++ .../SaveDashboard/useDashboardSave.tsx | 3 +- .../dashboard/services/DashboardSrv.ts | 28 ++------ public/app/types/dashboard.ts | 6 ++ 10 files changed, 101 insertions(+), 50 deletions(-) diff --git a/.betterer.results b/.betterer.results index 6ad6560471f..35c44194c85 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3100,6 +3100,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard/api/dashboard_api.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard/components/AddLibraryPanelWidget/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./AddLibraryPanelWidget\`)", "0"] ], diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index ee784d4df4d..6a23c3dce50 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -11,6 +11,9 @@ import { ResourceList, ResourceClient, ObjectMeta, + AnnoKeyOriginPath, + AnnoKeyOriginHash, + AnnoKeyOriginName, } from './types'; export interface GroupVersionResource { @@ -103,7 +106,7 @@ function setOriginAsUI(meta: Partial) { if (!meta.annotations) { meta.annotations = {}; } - meta.annotations.AnnoKeyOriginName = 'UI'; - meta.annotations.AnnoKeyOriginPath = window.location.pathname; - meta.annotations.AnnoKeyOriginHash = config.buildInfo.versionString; + meta.annotations[AnnoKeyOriginName] = 'UI'; + meta.annotations[AnnoKeyOriginPath] = window.location.pathname; + meta.annotations[AnnoKeyOriginHash] = config.buildInfo.versionString; } diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 97fe0317f30..9065f67f668 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -37,9 +37,9 @@ export const AnnoKeyMessage = 'grafana.app/message'; export const AnnoKeySlug = 'grafana.app/slug'; // Identify where values came from -const AnnoKeyOriginName = 'grafana.app/originName'; -const AnnoKeyOriginPath = 'grafana.app/originPath'; -const AnnoKeyOriginHash = 'grafana.app/originHash'; +export const AnnoKeyOriginName = 'grafana.app/originName'; +export const AnnoKeyOriginPath = 'grafana.app/originPath'; +export const AnnoKeyOriginHash = 'grafana.app/originHash'; const AnnoKeyOriginTimestamp = 'grafana.app/originTimestamp'; type GrafanaAnnotations = { diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 48c6b6b916a..47567dcf153 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -331,17 +331,15 @@ export const browseDashboardsAPI = createApi({ // save an existing dashboard saveDashboard: builder.mutation({ - query: ({ dashboard, folderUid, message, overwrite, showErrorAlert }) => ({ - url: `/dashboards/db`, - method: 'POST', - showErrorAlert, - data: { - dashboard, - folderUid, - message: message ?? '', - overwrite: Boolean(overwrite), - }, - }), + queryFn: async (cmd) => { + try { + const rsp = await getDashboardAPI().saveDashboard(cmd); + return { data: rsp }; + } catch (error) { + return { error }; + } + }, + onQueryStarted: ({ folderUid }, { queryFulfilled, dispatch }) => { dashboardWatcher.ignoreNextSave(); queryFulfilled.then(async () => { diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index ebd71b6ca63..04ab768aaad 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -28,6 +28,7 @@ export function useSaveDashboard(isCopy = false) { message: options.message, overwrite: options.overwrite, showErrorAlert: false, + k8s: undefined, // TODO? pass the original metadata }); if ('error' in result) { diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 40bb77523ec..477495fda96 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -1,6 +1,12 @@ import { config, getBackendSrv } from '@grafana/runtime'; import { ScopedResourceClient } from 'app/features/apiserver/client'; -import { Resource, ResourceClient } from 'app/features/apiserver/types'; +import { + AnnoKeyFolder, + AnnoKeyMessage, + Resource, + ResourceClient, + ResourceForCreate, +} from 'app/features/apiserver/types'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; @@ -53,7 +59,7 @@ interface DashboardWithAccessInfo extends Resource; - constructor(private legacy: DashboardAPI) { + constructor() { this.client = new ScopedResourceClient({ group: 'dashboard.grafana.app', version: 'v0alpha1', @@ -62,23 +68,69 @@ class K8sDashboardAPI implements DashboardAPI { } saveDashboard(options: SaveDashboardCommand): Promise { - return this.legacy.saveDashboard(options); + const dashboard = options.dashboard as DashboardDataDTO; // type for the uid property + const obj: ResourceForCreate = { + metadata: { + ...options?.k8s, + }, + spec: { + ...dashboard, + }, + }; + + if (options.message) { + obj.metadata.annotations = { + ...obj.metadata.annotations, + [AnnoKeyMessage]: options.message, + }; + } else if (obj.metadata.annotations) { + delete obj.metadata.annotations[AnnoKeyMessage]; + } + + if (options.folderUid) { + obj.metadata.annotations = { + ...obj.metadata.annotations, + [AnnoKeyFolder]: options.folderUid, + }; + } + + if (dashboard.uid) { + obj.metadata.name = dashboard.uid; + return this.client.update(obj).then((v) => this.asSaveDashboardResponseDTO(v)); + } + return this.client.create(obj).then((v) => this.asSaveDashboardResponseDTO(v)); + } + + asSaveDashboardResponseDTO(v: Resource): SaveDashboardResponseDTO { + return { + uid: v.metadata.name, + version: v.spec.version ?? 0, + id: v.spec.id ?? 0, + status: 'success', + slug: '', + url: '', + }; } deleteDashboard(uid: string, showSuccessAlert: boolean): Promise { - return this.legacy.deleteDashboard(uid, showSuccessAlert); + return this.client.delete(uid).then((v) => ({ + id: 0, + message: v.message, + title: 'deleted', + })); } async getDashboardDTO(uid: string): Promise { - const dto = await this.client.subresource(uid, 'dto'); + const dash = await this.client.subresource(uid, 'dto'); return { meta: { - ...dto.access, + ...dash.access, isNew: false, isFolder: false, - uid: dto.metadata.name, + uid: dash.metadata.name, + k8s: dash.metadata, }, - dashboard: dto.spec, + dashboard: dash.spec, }; } } @@ -87,8 +139,7 @@ let instance: DashboardAPI | undefined = undefined; export function getDashboardAPI() { if (!instance) { - const legacy = new LegacyDashboardAPI(); - instance = config.featureToggles.kubernetesDashboards ? new K8sDashboardAPI(legacy) : legacy; + instance = config.featureToggles.kubernetesDashboards ? new K8sDashboardAPI() : new LegacyDashboardAPI(); } return instance; } diff --git a/public/app/features/dashboard/components/SaveDashboard/types.ts b/public/app/features/dashboard/components/SaveDashboard/types.ts index a8af17cb806..f5b9418a5ee 100644 --- a/public/app/features/dashboard/components/SaveDashboard/types.ts +++ b/public/app/features/dashboard/components/SaveDashboard/types.ts @@ -1,4 +1,5 @@ import { Dashboard } from '@grafana/schema'; +import { ObjectMeta } from 'app/features/apiserver/types'; import { CloneOptions, DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { Diffs } from 'app/features/dashboard-scene/settings/version-history/utils'; @@ -22,6 +23,9 @@ export interface SaveDashboardCommand { folderUid?: string; overwrite?: boolean; showErrorAlert?: boolean; + + // When loading dashboards from k8s, we need to have access to the metadata wrapper + k8s?: Partial; } export interface SaveDashboardFormProps { diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 8bf16da07e9..08de75d36c0 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -26,6 +26,7 @@ const saveDashboard = async ( folderUid: options.folderUid ?? dashboard.meta.folderUid ?? saveModel.meta?.folderUid, message: options.message, overwrite: options.overwrite, + k8s: dashboard.meta.k8s, }); if ('error' in query) { @@ -70,7 +71,7 @@ export const useDashboardSave = (isCopy = false) => { const currentPath = locationService.getLocation().pathname; const newUrl = locationUtil.stripBaseFromUrl(result.url); - if (newUrl !== currentPath) { + if (newUrl !== currentPath && result.url) { setTimeout(() => locationService.replace(newUrl)); } if (dashboard.meta.isStarred) { diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 8482ea555ff..e89ad53668a 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -1,5 +1,3 @@ -import { lastValueFrom } from 'rxjs'; - import { AppEvents } from '@grafana/data'; import { BackendSrvRequest } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; @@ -28,15 +26,6 @@ export interface SaveDashboardOptions { refresh?: string; } -interface SaveDashboardResponse { - id: number; - slug: string; - status: string; - uid: string; - url: string; - version: number; -} - export class DashboardSrv { dashboard?: DashboardModel; @@ -75,17 +64,12 @@ export class DashboardSrv { data: SaveDashboardOptions, requestOptions?: Pick ) { - return lastValueFrom( - getBackendSrv().fetch({ - url: '/api/dashboards/db/', - method: 'POST', - data: { - ...data, - dashboard: data.dashboard.getSaveModelClone(), - }, - ...requestOptions, - }) - ); + return getDashboardAPI().saveDashboard({ + message: data.message, + folderUid: data.folderUid, + dashboard: data.dashboard.getSaveModelClone(), + showErrorAlert: requestOptions?.showErrorAlert, + }); } starDashboard(dashboardUid: string, isStarred: boolean) { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 51c0dcca4db..395255e2784 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,5 +1,6 @@ import { DataQuery } from '@grafana/data'; import { Dashboard, DataSourceRef } from '@grafana/schema'; +import { ObjectMeta } from 'app/features/apiserver/types'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; export interface DashboardDTO { @@ -67,6 +68,11 @@ export interface DashboardMeta { dashboardNotFound?: boolean; isEmbedded?: boolean; isNew?: boolean; + + // When loaded from kubernetes, we stick the raw metadata here + // yes weird, but this means all the editor structures can exist unchanged + // until we use the resource as the main container + k8s?: Partial; } export interface AnnotationActions { From 9770ebcd6ca4a09bac33952e5abd8eba814fba7c Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 26 Jun 2024 13:46:44 +0300 Subject: [PATCH 08/16] Scopes: Open the selector to a selected scope (#89686) --- .../scene/Scopes/ScopesFiltersScene.tsx | 43 ++++++++++++++++++- .../scene/Scopes/ScopesScene.test.tsx | 11 +++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx index f0d9ddfb503..7af747fc9d8 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx @@ -166,7 +166,19 @@ export class ScopesFiltersScene extends SceneObjectBase public open() { if (!this.scopesParent.state.isViewing) { - this.setState({ isOpened: true }); + let nodes = { ...this.state.nodes }; + + // First close all nodes + nodes = this.closeNodes(nodes); + + // Extract the path of a scope + let path = [...(this.state.scopes[0]?.path ?? ['', ''])]; + path.splice(path.length - 1, 1); + + // Expand the nodes to the selected scope + nodes = this.expandNodes(nodes, path); + + this.setState({ isOpened: true, nodes }); } } @@ -207,6 +219,35 @@ export class ScopesFiltersScene extends SceneObjectBase this.setState({ isOpened: false }); } + private closeNodes(nodes: NodesMap): NodesMap { + return Object.entries(nodes).reduce((acc, [id, node]) => { + acc[id] = { + ...node, + isExpanded: false, + nodes: this.closeNodes(node.nodes), + }; + + return acc; + }, {}); + } + + private expandNodes(nodes: NodesMap, path: string[]): NodesMap { + nodes = { ...nodes }; + let currentNodes = nodes; + + for (let i = 0; i < path.length; i++) { + const nodeId = path[i]; + + currentNodes[nodeId] = { + ...currentNodes[nodeId], + isExpanded: true, + }; + currentNodes = currentNodes[nodeId].nodes; + } + + return nodes; + } + private getTreeScopes(): TreeScope[] { return this.state.scopes.map(({ scope, path }) => ({ scopeName: scope.metadata.name, diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx index ae67ba1f558..bbda7e05cac 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx @@ -181,6 +181,17 @@ describe('ScopesScene', () => { expect(getApplicationsSlothVoteTrackerSelect()).toBeInTheDocument(); expect(queryApplicationsClustersTitle()).not.toBeInTheDocument(); }); + + it('Opens to a selected scope', async () => { + await userEvents.click(getFiltersInput()); + await userEvents.click(getApplicationsExpand()); + await userEvents.click(getApplicationsSlothPictureFactorySelect()); + await userEvents.click(getApplicationsExpand()); + await userEvents.click(getClustersExpand()); + await userEvents.click(getFiltersApply()); + await userEvents.click(getFiltersInput()); + expect(queryApplicationsSlothPictureFactoryTitle()).toBeInTheDocument(); + }); }); describe('Filters', () => { From 95a8fc1dd72b192322315d7f407429c17efa4fbd Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 26 Jun 2024 14:10:38 +0300 Subject: [PATCH 09/16] Scopes: Implement message for when there are no dashboards found (#89675) --- .../scene/Scopes/ScopesDashboardsScene.tsx | 51 +++++++++++++++++-- .../scene/Scopes/ScopesScene.test.tsx | 35 +++++++++++++ .../scene/Scopes/testUtils.tsx | 32 ++++++++++++ public/locales/en-US/grafana.json | 4 ++ public/locales/pseudo-LOCALE/grafana.json | 4 ++ 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx index 665561937ce..d9bc1cc88d5 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx @@ -3,9 +3,9 @@ import { Link } from 'react-router-dom'; import { GrafanaTheme2, Scope, urlUtil } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; -import { CustomScrollbar, FilterInput, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { Button, CustomScrollbar, FilterInput, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { t } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { fetchSuggestedDashboards } from './api'; import { SuggestedDashboard } from './types'; @@ -14,6 +14,7 @@ export interface ScopesDashboardsSceneState extends SceneObjectState { dashboards: SuggestedDashboard[]; filteredDashboards: SuggestedDashboard[]; isLoading: boolean; + scopesSelected: boolean; searchQuery: string; } @@ -25,13 +26,14 @@ export class ScopesDashboardsScene extends SceneObjectBase 0, }); } @@ -62,11 +65,29 @@ export class ScopesDashboardsScene extends SceneObjectBase) { - const { filteredDashboards, isLoading, searchQuery } = model.useState(); + const { dashboards, filteredDashboards, isLoading, searchQuery, scopesSelected } = model.useState(); const styles = useStyles2(getStyles); const [queryParams] = useQueryParams(); + if (!isLoading) { + if (!scopesSelected) { + return ( +

+ No scopes selected +

+ ); + } else if (dashboards.length === 0) { + return ( +

+ + No dashboards found for the selected scopes + +

+ ); + } + } + return ( <>
@@ -85,7 +106,7 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps - ) : ( + ) : filteredDashboards.length > 0 ? ( {filteredDashboards.map(({ dashboard, dashboardTitle }) => ( ))} + ) : ( +

+ No results found for your query + + +

)} ); @@ -105,6 +138,14 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps { return { + noResultsContainer: css({ + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + justifyContent: 'center', + textAlign: 'center', + }), searchInputContainer: css({ flex: '0 1 auto', }), diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx index bbda7e05cac..84cea5de184 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx @@ -45,6 +45,12 @@ import { queryDashboardsContainer, queryDashboardsExpand, renderDashboard, + getNotFoundForScope, + queryDashboardsSearch, + getNotFoundForFilter, + getClustersSlothClusterEastRadio, + getNotFoundForFilterClear, + getNotFoundNoScopes, } from './testUtils'; jest.mock('@grafana/runtime', () => ({ @@ -309,6 +315,35 @@ describe('ScopesScene', () => { expect(queryAllDashboard('7')).toHaveLength(1); expect(queryAllDashboard('8')).toHaveLength(1); }); + + it('Does show a proper message when no scopes are selected', async () => { + await userEvents.click(getDashboardsExpand()); + expect(getNotFoundNoScopes()).toBeInTheDocument(); + expect(queryDashboardsSearch()).not.toBeInTheDocument(); + }); + + it('Does not show the input when there are no dashboards found for scope', async () => { + await userEvents.click(getDashboardsExpand()); + await userEvents.click(getFiltersInput()); + await userEvents.click(getClustersExpand()); + await userEvents.click(getClustersSlothClusterEastRadio()); + await userEvents.click(getFiltersApply()); + expect(getNotFoundForScope()).toBeInTheDocument(); + expect(queryDashboardsSearch()).not.toBeInTheDocument(); + }); + + it('Does show the input and a message when there are no dashboards found for filter', async () => { + await userEvents.click(getDashboardsExpand()); + await userEvents.click(getFiltersInput()); + await userEvents.click(getApplicationsExpand()); + await userEvents.click(getApplicationsSlothPictureFactorySelect()); + await userEvents.click(getFiltersApply()); + await userEvents.type(getDashboardsSearch(), 'unknown'); + expect(queryDashboardsSearch()).toBeInTheDocument(); + expect(getNotFoundForFilter()).toBeInTheDocument(); + await userEvents.click(getNotFoundForFilterClear()); + expect(getDashboardsSearch().value).toBe(''); + }); }); describe('View mode', () => { diff --git a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx index dc1fbe0d9fc..cdc9053a163 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx @@ -49,6 +49,16 @@ export const mocksScopes: Scope[] = [ filters: [{ key: 'cluster', value: 'slothClusterSouth', operator: 'equals' }], }, }, + { + metadata: { name: 'slothClusterEast' }, + spec: { + title: 'slothClusterEast', + type: 'cluster', + description: 'slothClusterEast', + category: 'clusters', + filters: [{ key: 'cluster', value: 'slothClusterEast', operator: 'equals' }], + }, + }, { metadata: { name: 'slothPictureFactory' }, spec: { @@ -213,6 +223,17 @@ export const mocksNodes: Array = [ linkId: 'slothClusterSouth', }, }, + { + parent: 'clusters', + metadata: { name: 'clusters-slothClusterEast' }, + spec: { + nodeType: 'leaf', + title: 'slothClusterEast', + description: 'slothClusterEast', + linkType: 'scope', + linkId: 'slothClusterEast', + }, + }, { parent: 'clusters', metadata: { name: 'clusters.applications' }, @@ -312,6 +333,10 @@ const selectors = { search: 'scopes-dashboards-search', loading: 'scopes-dashboards-loading', dashboard: (uid: string) => `scopes-dashboards-${uid}`, + notFoundNoScopes: 'scopes-dashboards-notFoundNoScopes', + notFoundForScope: 'scopes-dashboards-notFoundForScope', + notFoundForFilter: 'scopes-dashboards-notFoundForFilter', + notFoundForFilterClear: 'scopes-dashboards-notFoundForFilter-clear', }, }; @@ -324,10 +349,15 @@ export const queryDashboardsExpand = () => screen.queryByTestId(selectors.dashbo export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand); export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); +export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search); export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search); export const queryAllDashboard = (uid: string) => screen.queryAllByTestId(selectors.dashboards.dashboard(uid)); export const queryDashboard = (uid: string) => screen.queryByTestId(selectors.dashboards.dashboard(uid)); export const getDashboard = (uid: string) => screen.getByTestId(selectors.dashboards.dashboard(uid)); +export const getNotFoundNoScopes = () => screen.getByTestId(selectors.dashboards.notFoundNoScopes); +export const getNotFoundForScope = () => screen.getByTestId(selectors.dashboards.notFoundForScope); +export const getNotFoundForFilter = () => screen.getByTestId(selectors.dashboards.notFoundForFilter); +export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dashboards.notFoundForFilterClear); export const getApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications')); export const getApplicationsSearch = () => screen.getByTestId(selectors.tree.search('applications')); @@ -357,6 +387,8 @@ export const getClustersSlothClusterNorthRadio = () => screen.getByTestId(selectors.tree.radio('clusters-slothClusterNorth')); export const getClustersSlothClusterSouthRadio = () => screen.getByTestId(selectors.tree.radio('clusters-slothClusterSouth')); +export const getClustersSlothClusterEastRadio = () => + screen.getByTestId(selectors.tree.radio('clusters-slothClusterEast')); export function buildTestScene(overrides: Partial = {}) { return new DashboardScene({ diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7d5a1812623..afbd474048c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1657,6 +1657,10 @@ }, "suggestedDashboards": { "loading": "Loading dashboards", + "noResultsForFilter": "No results found for your query", + "noResultsForFilterClear": "Clear search", + "noResultsForScopes": "No dashboards found for the selected scopes", + "noResultsNoScopes": "No scopes selected", "search": "Search", "toggle": { "collapse": "Collapse scope filters", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index cee6b19f34e..c3eb128f08d 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1657,6 +1657,10 @@ }, "suggestedDashboards": { "loading": "Ŀőäđįʼnģ đäşĥþőäřđş", + "noResultsForFilter": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy", + "noResultsForFilterClear": "Cľęäř şęäřčĥ", + "noResultsForScopes": "Ńő đäşĥþőäřđş ƒőūʼnđ ƒőř ŧĥę şęľęčŧęđ şčőpęş", + "noResultsNoScopes": "Ńő şčőpęş şęľęčŧęđ", "search": "Ŝęäřčĥ", "toggle": { "collapse": "Cőľľäpşę şčőpę ƒįľŧęřş", From d3dda4ca897762f95a4c88c86923014e6b41fd4f Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 26 Jun 2024 12:14:11 +0100 Subject: [PATCH 10/16] Storybook: Use new JSX runtime (#89768) use new jsx runtime in storybook --- packages/grafana-ui/.storybook/main.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts index 3fb1a5d1749..dfb53620b97 100644 --- a/packages/grafana-ui/.storybook/main.ts +++ b/packages/grafana-ui/.storybook/main.ts @@ -96,10 +96,19 @@ const mainConfig: StorybookConfig = { tsconfigPath: path.resolve(__dirname, 'tsconfig.json'), shouldExtractLiteralValuesFromEnum: true, shouldRemoveUndefinedFromOptional: true, - propFilter: (prop: any) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), + propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), savePropValueAsString: true, }, }, + swc: () => ({ + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }), webpackFinal: async (config) => { // expose jquery as a global so jquery plugins don't break at runtime. config.module?.rules?.push({ From 5fdbafdc456ddcbbbbd470edbabc429a7ce01b56 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 26 Jun 2024 12:30:02 +0100 Subject: [PATCH 11/16] E2C: New onprem empty state (#89628) * E2C: New onprem empty state * fix test --- .../EmptyState/CallToAction/CallToAction.tsx | 2 +- .../onprem/EmptyState/EmptyState.tsx | 47 ++++++++----------- .../onprem/EmptyState/InfoPaneLeft.tsx | 8 ++-- .../onprem/EmptyState/InfoPaneRight.tsx | 6 +-- 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx index 56bd133fbb8..d9c6153780a 100644 --- a/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx +++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/CallToAction/CallToAction.tsx @@ -13,7 +13,7 @@ export const CallToAction = () => { return ( <> - + Let us manage your Grafana stack diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx index 37f56297a91..ee5a399b954 100644 --- a/public/app/features/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx +++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/EmptyState.tsx @@ -1,38 +1,29 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Grid, Stack, useStyles2 } from '@grafana/ui'; +import { Box, Grid, Stack } from '@grafana/ui'; import { CallToAction } from './CallToAction/CallToAction'; import { InfoPaneLeft } from './InfoPaneLeft'; import { InfoPaneRight } from './InfoPaneRight'; export const EmptyState = () => { - const styles = useStyles2(getStyles); - return ( -
- - + + + + - - - - - -
+ + + + + +
+
); }; - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - maxWidth: theme.breakpoints.values.xl, - }), -}); diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneLeft.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneLeft.tsx index 693332bd4f6..37ca5356f8c 100644 --- a/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneLeft.tsx +++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneLeft.tsx @@ -1,11 +1,11 @@ -import { Box } from '@grafana/ui'; +import { Stack } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { InfoItem } from '../../shared/InfoItem'; export const InfoPaneLeft = () => { return ( - + { installation. + { SLOs, incident management, machine learning, and powerful observability integrations. + { unauthorized access, use, or disclosure. - + ); }; diff --git a/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneRight.tsx b/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneRight.tsx index f0967a6604d..01173bab9e8 100644 --- a/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneRight.tsx +++ b/public/app/features/migrate-to-cloud/onprem/EmptyState/InfoPaneRight.tsx @@ -1,11 +1,11 @@ -import { Box } from '@grafana/ui'; +import { Stack } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { InfoItem } from '../../shared/InfoItem'; export const InfoPaneRight = () => { return ( - + { dashboards. - + ); }; From 68a5bf3c18e4f53ddbebe32b5e27db558527b666 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 26 Jun 2024 14:41:38 +0300 Subject: [PATCH 12/16] Scopes: Allow node title to expand the container (#89682) --- .../scene/Scopes/ScopesTreeLevel.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx index 4c9963f1f53..8d57b74d825 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; -import { Checkbox, FilterInput, IconButton, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { Checkbox, FilterInput, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; import { NodesMap, TreeScope } from './types'; @@ -105,20 +105,24 @@ export function ScopesTreeLevel({ ) ) : null} - {childNode.isExpandable && ( - { onNodeUpdate(childNodePath, !childNode.isExpanded, childNode.query); }} - /> - )} + > + - {childNode.title} + {childNode.title} + + ) : ( + {childNode.title} + )}
@@ -165,6 +169,15 @@ const getStyles = (theme: GrafanaTheme2) => { gap: 0, }), }), + itemExpand: css({ + alignItems: 'center', + background: 'none', + border: 0, + display: 'flex', + gap: theme.spacing(1), + margin: 0, + padding: 0, + }), itemChildren: css({ paddingLeft: theme.spacing(4), }), From fb15d681a1f4e6ac694f53f22347d14cad941343 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Wed, 26 Jun 2024 08:44:05 -0300 Subject: [PATCH 13/16] PublicDashboards: Update data sources supported list (#89704) PublicDashboards: Update datasources compatibility list --- .../SupportedPubdashDatasources.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources.ts b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources.ts index aac1aaaf0f5..d33b843451e 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources.ts +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources.ts @@ -12,17 +12,21 @@ export const supportedDatasources = new Set([ 'dlopes7-appdynamics-datasource', 'dvelop-odata-datasource', 'elasticsearch', + 'factry-historian-datasource', + 'fiskaly-surrealdb-datasource', 'frser-sqlite-datasource', 'grafadruid-druid-datasource', 'grafana-athena-datasource', 'grafana-azure-data-explorer-datasource', 'grafana-azure-monitor-datasource', + 'grafana-azurecosmosdb-datasource', 'grafana-bigquery-datasource', + 'grafana-catchpoint-datasource', 'grafana-clickhouse-datasource', 'grafana-databricks-datasource', 'grafana-datadog-datasource', + 'grafana-dynamodb-datasource', 'grafana-dynatrace-datasource', - 'grafana-es-open-distro-datasource', 'grafana-falconlogscale-datasource', 'grafana-github-datasource', 'grafana-gitlab-datasource', @@ -34,10 +38,10 @@ export const supportedDatasources = new Set([ 'grafana-mongodb-datasource', 'grafana-newrelic-datasource', 'grafana-odbc-datasource', - 'grafana-opcua-datasource', 'grafana-opensearch-datasource', 'grafana-oracle-datasource', 'grafana-orbit-datasource', + 'grafana-pagerduty-datasource', 'grafana-redshift-datasource', 'grafana-salesforce-datasource', 'grafana-saphana-datasource', @@ -47,12 +51,16 @@ export const supportedDatasources = new Set([ 'grafana-splunk-datasource', 'grafana-splunk-monitoring-datasource', 'grafana-sumologic-datasource', + 'grafana-surrealdb-datasource', 'grafana-testdata-datasource', 'grafana-timestream-datasource', 'grafana-wavefront-datasource', 'grafana-x-ray-datasource', 'graphite', 'hadesarchitect-cassandra-datasource', + 'highlightinc-highlight-datasource', + 'ibm-aql-datasource', + 'ibm-kql-datasource', 'influxdata-flightsql-datasource', 'influxdb', 'innius-grpc-datasource', @@ -64,16 +72,18 @@ export const supportedDatasources = new Set([ 'mysql', 'nagasudhirpulla-api-datasource', 'needleinajaystack-haystack-datasource', + 'oci-metrics-datasource', 'opentsdb', + 'parseable-parseable-datasource', 'postgres', 'prometheus', + 'questdb-questdb-datasource', 'quickwit-quickwit-datasource', 'redis-datasource', - 'sentinelone-dataset-datasource', - 'sneller-sneller-datasource', - 'spiceai-spicexyz-datasource', + 'retrodaredevil-wildgraphql-datasource', 'stackdriver', 'tdengine-datasource', + 'timeplus-proton-datasource', 'trino-datasource', 'vertamedia-clickhouse-datasource', 'vertica-grafana-datasource', From 3db4e5a0c6947dc8b0d193a897a597cf6573d162 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 26 Jun 2024 15:00:24 +0300 Subject: [PATCH 14/16] K8s: remove dashboard summary abstraction (#89761) --- pkg/apis/dashboard/v0alpha1/register.go | 6 -- pkg/apis/dashboard/v0alpha1/types.go | 23 ----- .../v0alpha1/zz_generated.deepcopy.go | 81 ------------------ .../apis/dashboard/access/sql_dashboards.go | 52 ------------ pkg/registry/apis/dashboard/access/types.go | 3 - pkg/registry/apis/dashboard/register.go | 11 --- pkg/registry/apis/dashboard/storage.go | 8 -- .../apis/dashboard/summary_storage.go | 83 ------------------- pkg/tests/apis/dashboard/dashboards_test.go | 14 ---- 9 files changed, 281 deletions(-) delete mode 100644 pkg/registry/apis/dashboard/summary_storage.go diff --git a/pkg/apis/dashboard/v0alpha1/register.go b/pkg/apis/dashboard/v0alpha1/register.go index 1bb1bf22351..2b8a97f1eb5 100644 --- a/pkg/apis/dashboard/v0alpha1/register.go +++ b/pkg/apis/dashboard/v0alpha1/register.go @@ -19,12 +19,6 @@ var DashboardResourceInfo = common.NewResourceInfo(GROUP, VERSION, func() runtime.Object { return &DashboardList{} }, ) -var DashboardSummaryResourceInfo = common.NewResourceInfo(GROUP, VERSION, - "summary", "summary", "DashboardSummary", - func() runtime.Object { return &DashboardSummary{} }, - func() runtime.Object { return &DashboardSummaryList{} }, -) - var ( // SchemeGroupVersion is group version used to register these objects SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} diff --git a/pkg/apis/dashboard/v0alpha1/types.go b/pkg/apis/dashboard/v0alpha1/types.go index 3b531127031..b2fd3897317 100644 --- a/pkg/apis/dashboard/v0alpha1/types.go +++ b/pkg/apis/dashboard/v0alpha1/types.go @@ -27,29 +27,6 @@ type DashboardList struct { Items []Dashboard `json:"items,omitempty"` } -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardSummary struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - // The dashboard body - Spec DashboardSummarySpec `json:"spec,omitempty"` -} - -type DashboardSummarySpec struct { - Title string `json:"title"` - Tags []string `json:"tags,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardSummaryList struct { - metav1.TypeMeta `json:",inline"` - // +optional - metav1.ListMeta `json:"metadata,omitempty"` - - Items []DashboardSummary `json:"items,omitempty"` -} - // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type DashboardVersionList struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go index acac319e53d..46e4700ab07 100644 --- a/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/dashboard/v0alpha1/zz_generated.deepcopy.go @@ -126,87 +126,6 @@ func (in *DashboardList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardSummary) DeepCopyInto(out *DashboardSummary) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSummary. -func (in *DashboardSummary) DeepCopy() *DashboardSummary { - if in == nil { - return nil - } - out := new(DashboardSummary) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardSummary) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardSummaryList) DeepCopyInto(out *DashboardSummaryList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DashboardSummary, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSummaryList. -func (in *DashboardSummaryList) DeepCopy() *DashboardSummaryList { - if in == nil { - return nil - } - out := new(DashboardSummaryList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardSummaryList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardSummarySpec) DeepCopyInto(out *DashboardSummarySpec) { - *out = *in - if in.Tags != nil { - in, out := &in.Tags, &out.Tags - *out = make([]string, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSummarySpec. -func (in *DashboardSummarySpec) DeepCopy() *DashboardSummarySpec { - if in == nil { - return nil - } - out := new(DashboardSummarySpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DashboardVersionInfo) DeepCopyInto(out *DashboardVersionInfo) { *out = *in diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go index f043e84b9fb..86bbbfe1239 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go @@ -189,58 +189,6 @@ func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid return nil, fmt.Errorf("not found") } -// GetDashboards implements DashboardAccess. -func (a *dashboardSqlAccess) GetDashboardSummaries(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardSummaryList, error) { - rows, limit, err := a.getRows(ctx, query, true) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - - totalSize := 0 - list := &dashboardsV0.DashboardSummaryList{} - for { - row, err := rows.Next() - if err != nil || row == nil { - return list, err - } - - totalSize += row.Bytes - if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) { - if query.Requirements.Folder != nil { - row.token.folder = *query.Requirements.Folder - } - list.Continue = row.token.String() // will skip this one but start here next time - return list, err - } - list.Items = append(list.Items, toSummary(row)) - } -} - -func (a *dashboardSqlAccess) GetDashboardSummary(ctx context.Context, orgId int64, uid string) (*dashboardsV0.DashboardSummary, error) { - r, err := a.GetDashboardSummaries(ctx, &DashboardQuery{ - OrgID: orgId, - UID: uid, - }) - if err != nil { - return nil, err - } - if len(r.Items) > 0 { - return &r.Items[0], nil - } - return nil, fmt.Errorf("not found") -} - -func toSummary(row *dashboardRow) dashboardsV0.DashboardSummary { - return dashboardsV0.DashboardSummary{ - ObjectMeta: row.Dash.ObjectMeta, - Spec: dashboardsV0.DashboardSummarySpec{ - Title: row.Title, - Tags: row.Tags, - }, - } -} - func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...any) (*rowsWrapper, error) { user, err := appcontext.User(ctx) if err != nil { diff --git a/pkg/registry/apis/dashboard/access/types.go b/pkg/registry/apis/dashboard/access/types.go index cf5df48e19f..50065c8f2a2 100644 --- a/pkg/registry/apis/dashboard/access/types.go +++ b/pkg/registry/apis/dashboard/access/types.go @@ -30,9 +30,6 @@ type DashboardAccess interface { GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error) GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error) - GetDashboardSummary(ctx context.Context, orgId int64, uid string) (*dashboardsV0.DashboardSummary, error) - GetDashboardSummaries(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardSummaryList, error) - SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 0fbf7dcc5c5..3aba4a4bfce 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -87,8 +87,6 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { &v0alpha1.DashboardList{}, &v0alpha1.DashboardWithAccessInfo{}, &v0alpha1.DashboardVersionList{}, - &v0alpha1.DashboardSummary{}, - &v0alpha1.DashboardSummaryList{}, &v0alpha1.VersionsQueryOptions{}, ) } @@ -152,14 +150,6 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo( storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store, reg) } - // Summary - resourceInfo2 := v0alpha1.DashboardSummaryResourceInfo - storage[resourceInfo2.StoragePath()] = &summaryStorage{ - resource: resourceInfo2, - access: b.access, - tableConverter: store.TableConvertor, - } - apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage return &apiGroupInfo, nil } @@ -178,7 +168,6 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op // Hide the ability to list or watch across all tenants delete(oas.Paths.Paths, root+v0alpha1.DashboardResourceInfo.GroupResource().Resource) delete(oas.Paths.Paths, root+"watch/"+v0alpha1.DashboardResourceInfo.GroupResource().Resource) - delete(oas.Paths.Paths, root+v0alpha1.DashboardSummaryResourceInfo.GroupResource().Resource) // The root API discovery list sub := oas.Paths.Paths[root] diff --git a/pkg/registry/apis/dashboard/storage.go b/pkg/registry/apis/dashboard/storage.go index 064ed31aea1..ace79bc02e3 100644 --- a/pkg/registry/apis/dashboard/storage.go +++ b/pkg/registry/apis/dashboard/storage.go @@ -54,14 +54,6 @@ func newStorage(scheme *runtime.Scheme) (*storage, error) { }, nil } } - summary, ok := obj.(*v0alpha1.DashboardSummary) - if ok { - return []interface{}{ - dash.Name, - summary.Spec.Title, - dash.CreationTimestamp.UTC().Format(time.RFC3339), - }, nil - } return nil, fmt.Errorf("expected dashboard or summary") }) return &storage{Store: store}, nil diff --git a/pkg/registry/apis/dashboard/summary_storage.go b/pkg/registry/apis/dashboard/summary_storage.go deleted file mode 100644 index 85fe3bff0ff..00000000000 --- a/pkg/registry/apis/dashboard/summary_storage.go +++ /dev/null @@ -1,83 +0,0 @@ -package dashboard - -import ( - "context" - - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - "github.com/grafana/grafana/pkg/registry/apis/dashboard/access" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" -) - -var ( - _ rest.Storage = (*summaryStorage)(nil) - _ rest.Scoper = (*summaryStorage)(nil) - _ rest.SingularNameProvider = (*summaryStorage)(nil) - _ rest.Getter = (*summaryStorage)(nil) - _ rest.Lister = (*summaryStorage)(nil) -) - -type summaryStorage struct { - resource common.ResourceInfo - access access.DashboardAccess - tableConverter rest.TableConvertor -} - -func (s *summaryStorage) New() runtime.Object { - return s.resource.NewFunc() -} - -func (s *summaryStorage) Destroy() {} - -func (s *summaryStorage) NamespaceScoped() bool { - return true -} - -func (s *summaryStorage) GetSingularName() string { - return s.resource.GetSingularName() -} - -func (s *summaryStorage) NewList() runtime.Object { - return s.resource.NewListFunc() -} - -func (s *summaryStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.tableConverter.ConvertToTable(ctx, object, tableOptions) -} - -func (s *summaryStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - orgId, err := request.OrgIDForList(ctx) - if err != nil { - return nil, err - } - - // translate grafana.app/* label selectors into field requirements - requirements, newSelector, err := entity.ReadLabelSelectors(options.LabelSelector) - if err != nil { - return nil, err - } - - query := &access.DashboardQuery{ - OrgID: orgId, - Limit: int(options.Limit), - MaxBytes: 2 * 1024 * 1024, // 2MB, - ContinueToken: options.Continue, - Requirements: requirements, - Labels: newSelector, - } - return s.access.GetDashboardSummaries(ctx, query) -} - -func (s *summaryStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return s.access.GetDashboardSummary(ctx, info.OrgID, name) -} diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index cad2432d7b1..8ff9569d91e 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -95,20 +95,6 @@ func TestIntegrationDashboardsApp(t *testing.T) { "patch", "update" ] - }, - { - "resource": "summary", - "responseKind": { - "group": "", - "kind": "DashboardSummary", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "summary", - "verbs": [ - "get", - "list" - ] } ], "version": "v0alpha1" From fe1309dd96dbf887163ea0f31ba8449932056ba6 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 26 Jun 2024 14:02:02 +0200 Subject: [PATCH 15/16] Alerting: Send external URL to the remote Alertmanager (#89701) * Alerting: Send external URL to the remote Alertmanager * test that the URL is sent to the remote Alertmanager * AppURL -> ExternalURL --- pkg/services/ngalert/ngalert.go | 3 +++ pkg/services/ngalert/remote/alertmanager.go | 5 ++++- pkg/services/ngalert/remote/alertmanager_test.go | 4 ++++ .../ngalert/remote/client/alertmanager_configuration.go | 2 ++ pkg/services/ngalert/remote/client/mimir.go | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 901a083b53c..e5fbb77b581 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -203,6 +203,7 @@ func (ng *AlertNG) init() error { URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL, PromoteConfig: true, SyncInterval: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.SyncInterval, + ExternalURL: ng.Cfg.AppURL, } remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m, ng.tracer) if err != nil { @@ -237,6 +238,7 @@ func (ng *AlertNG) init() error { PromoteConfig: true, TenantID: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.TenantID, URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL, + ExternalURL: ng.Cfg.AppURL, } remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m, ng.tracer) if err != nil { @@ -273,6 +275,7 @@ func (ng *AlertNG) init() error { TenantID: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.TenantID, URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL, SyncInterval: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.SyncInterval, + ExternalURL: ng.Cfg.AppURL, } remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, autogenFn, m, ng.tracer) if err != nil { diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 2014220eda9..819df0792ec 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -77,10 +77,12 @@ type AlertmanagerConfig struct { BasicAuthPassword string DefaultConfig string + + // ExternalURL is used in notifications sent by the remote Alertmanager. + ExternalURL string // PromoteConfig is a flag that determines whether the configuration should be used in the remote Alertmanager. // The same flag is used for promoting state. PromoteConfig bool - // SyncInterval determines how often we should attempt to synchronize configuration. SyncInterval time.Duration } @@ -117,6 +119,7 @@ func NewAlertmanager(cfg AlertmanagerConfig, store stateStore, decryptFn Decrypt TenantID: cfg.TenantID, URL: u, PromoteConfig: cfg.PromoteConfig, + ExternalURL: cfg.ExternalURL, } mc, err := remoteClient.New(mcCfg, metrics, tracer) if err != nil { diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 9617a0f820d..3f61d163245 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -168,6 +168,7 @@ func TestApplyConfig(t *testing.T) { DefaultConfig: defaultGrafanaConfig, PromoteConfig: true, SyncInterval: 1 * time.Hour, + ExternalURL: "https://test.grafana.com", } ctx := context.Background() @@ -198,6 +199,9 @@ func TestApplyConfig(t *testing.T) { require.JSONEq(t, testGrafanaConfigWithSecret, string(amCfg)) require.True(t, configSent.Promoted) + // Grafana's URL should be sent alongside the configuration. + require.Equal(t, cfg.ExternalURL, configSent.ExternalURL) + // If we already got a 200 status code response and the sync interval hasn't elapsed, // we shouldn't send the state/configuration again. expStateSync := lastStateSync diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index ebb61ce9196..2a94b851a38 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -21,6 +21,7 @@ type UserGrafanaConfig struct { CreatedAt int64 `json:"created"` Default bool `json:"default"` Promoted bool `json:"promoted"` + ExternalURL string `json:"external_url"` } func (mc *Mimir) ShouldPromoteConfig() bool { @@ -53,6 +54,7 @@ func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimo CreatedAt: createdAt, Default: isDefault, Promoted: mc.promoteConfig, + ExternalURL: mc.externalURL, }) if err != nil { return err diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index 1fda8206c4d..4b58bb22b60 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -40,6 +40,7 @@ type Mimir struct { logger log.Logger metrics *metrics.RemoteAlertmanager promoteConfig bool + externalURL string } type Config struct { @@ -49,6 +50,7 @@ type Config struct { Logger log.Logger PromoteConfig bool + ExternalURL string } // successResponse represents a successful response from the Mimir API. @@ -91,6 +93,7 @@ func New(cfg *Config, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer logger: cfg.Logger, metrics: metrics, promoteConfig: cfg.PromoteConfig, + externalURL: cfg.ExternalURL, }, nil } From ba4f2713c532735adcfab38b84c362c63ffba354 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Wed, 26 Jun 2024 09:16:13 -0300 Subject: [PATCH 16/16] Sharing: Change code ownership (#89732) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e8ccfc1f9b..5f89bdaeb41 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -591,6 +591,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/plugins/datasource/alertmanager/ @grafana/alerting-squad # Grafana Sharing Squad +/public/app/features/dashboard-scene/sharing/ @grafana/sharing-squad /public/app/features/dashboard/components/ShareModal/ @grafana/sharing-squad /public/app/features/manage-dashboards/components/PublicDashboardListTable/ @grafana/sharing-squad /public/app/features/dashboard/containers/PublicDashboardPage.tsx @grafana/sharing-squad