mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
Merge remote-tracking branch 'origin/main' into resource-store
This commit is contained in:
commit
86a7064334
@ -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"]
|
||||
],
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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:*",
|
||||
|
@ -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({
|
||||
|
@ -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 = <div className={styles.content}>{children}</div>;
|
||||
const overrideWidth = drawerWidth ?? width ?? drawerSizes[size].width;
|
||||
const minWidth = drawerSizes[size].minWidth;
|
||||
|
||||
return (
|
||||
<RcDrawer
|
||||
@ -95,7 +102,16 @@ export function Drawer({
|
||||
placement="right"
|
||||
getContainer={'.main-view'}
|
||||
className={styles.drawerContent}
|
||||
rootClassName={rootClass}
|
||||
rootClassName={styles.drawer}
|
||||
classNames={{
|
||||
wrapper: wrapperStyles,
|
||||
}}
|
||||
styles={{
|
||||
wrapper: {
|
||||
width: overrideWidth,
|
||||
minWidth,
|
||||
},
|
||||
}}
|
||||
width={''}
|
||||
motion={{
|
||||
motionAppear: true,
|
||||
@ -356,27 +372,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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}}
|
||||
|
@ -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{}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -89,8 +89,6 @@ func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||
&v0alpha1.DashboardList{},
|
||||
&v0alpha1.DashboardWithAccessInfo{},
|
||||
&v0alpha1.DashboardVersionList{},
|
||||
&v0alpha1.DashboardSummary{},
|
||||
&v0alpha1.DashboardSummaryList{},
|
||||
&v0alpha1.VersionsQueryOptions{},
|
||||
)
|
||||
}
|
||||
@ -159,14 +157,6 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo(
|
||||
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
|
||||
}
|
||||
@ -185,7 +175,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]
|
||||
|
@ -54,21 +54,7 @@ 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -11,6 +11,9 @@ import {
|
||||
ResourceList,
|
||||
ResourceClient,
|
||||
ObjectMeta,
|
||||
AnnoKeyOriginPath,
|
||||
AnnoKeyOriginHash,
|
||||
AnnoKeyOriginName,
|
||||
} from './types';
|
||||
|
||||
export interface GroupVersionResource {
|
||||
@ -103,7 +106,7 @@ function setOriginAsUI(meta: Partial<ObjectMeta>) {
|
||||
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;
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -331,17 +331,15 @@ export const browseDashboardsAPI = createApi({
|
||||
|
||||
// save an existing dashboard
|
||||
saveDashboard: builder.mutation<SaveDashboardResponseDTO, SaveDashboardCommand>({
|
||||
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 () => {
|
||||
|
@ -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) {
|
||||
|
@ -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, Icon, IconButton, Input, 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<ScopesDashboardsScene
|
||||
dashboards: [],
|
||||
filteredDashboards: [],
|
||||
isLoading: false,
|
||||
scopesSelected: false,
|
||||
searchQuery: '',
|
||||
});
|
||||
}
|
||||
|
||||
public async fetchDashboards(scopes: Scope[]) {
|
||||
if (scopes.length === 0) {
|
||||
return this.setState({ dashboards: [], filteredDashboards: [], isLoading: false });
|
||||
return this.setState({ dashboards: [], filteredDashboards: [], isLoading: false, scopesSelected: false });
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
@ -42,6 +44,7 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
||||
dashboards,
|
||||
filteredDashboards: this.filterDashboards(dashboards, this.state.searchQuery),
|
||||
isLoading: false,
|
||||
scopesSelected: scopes.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
@ -62,31 +65,38 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
||||
}
|
||||
|
||||
export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<ScopesDashboardsScene>) {
|
||||
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 (
|
||||
<p className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundNoScopes">
|
||||
<Trans i18nKey="scopes.suggestedDashboards.noResultsNoScopes">No scopes selected</Trans>
|
||||
</p>
|
||||
);
|
||||
} else if (dashboards.length === 0) {
|
||||
return (
|
||||
<p className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundForScope">
|
||||
<Trans i18nKey="scopes.suggestedDashboards.noResultsForScopes">
|
||||
No dashboards found for the selected scopes
|
||||
</Trans>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.searchInputContainer}>
|
||||
<Input
|
||||
prefix={<Icon name="search" />}
|
||||
placeholder={t('scopes.suggestedDashboards.search', 'Search')}
|
||||
<FilterInput
|
||||
disabled={isLoading}
|
||||
data-testid="scopes-dashboards-search"
|
||||
placeholder={t('scopes.suggestedDashboards.search', 'Search')}
|
||||
value={searchQuery}
|
||||
suffix={
|
||||
searchQuery && !isLoading ? (
|
||||
<IconButton
|
||||
aria-label={t('scopes.suggestedDashboards.clear', 'Clear search')}
|
||||
name="times"
|
||||
data-testid="scopes-dashboards-clear"
|
||||
onClick={() => model.changeSearchQuery('')}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
onChange={(evt) => model.changeSearchQuery(evt.currentTarget.value)}
|
||||
data-testid="scopes-dashboards-search"
|
||||
onChange={(value) => model.changeSearchQuery(value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -96,7 +106,7 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
|
||||
text={t('scopes.suggestedDashboards.loading', 'Loading dashboards')}
|
||||
data-testid="scopes-dashboards-loading"
|
||||
/>
|
||||
) : (
|
||||
) : filteredDashboards.length > 0 ? (
|
||||
<CustomScrollbar>
|
||||
{filteredDashboards.map(({ dashboard, dashboardTitle }) => (
|
||||
<Link
|
||||
@ -109,6 +119,18 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
|
||||
</Link>
|
||||
))}
|
||||
</CustomScrollbar>
|
||||
) : (
|
||||
<p className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundForFilter">
|
||||
<Trans i18nKey="scopes.suggestedDashboards.noResultsForFilter">No results found for your query</Trans>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => model.changeSearchQuery('')}
|
||||
data-testid="scopes-dashboards-notFoundForFilter-clear"
|
||||
>
|
||||
<Trans i18nKey="scopes.suggestedDashboards.noResultsForFilterClear">Clear search</Trans>
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -116,6 +138,14 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
noResultsContainer: css({
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
searchInputContainer: css({
|
||||
flex: '0 1 auto',
|
||||
}),
|
||||
|
@ -166,7 +166,19 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
|
||||
|
||||
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<ScopesFiltersSceneState>
|
||||
this.setState({ isOpened: false });
|
||||
}
|
||||
|
||||
private closeNodes(nodes: NodesMap): NodesMap {
|
||||
return Object.entries(nodes).reduce<NodesMap>((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,
|
||||
|
@ -45,7 +45,12 @@ import {
|
||||
queryDashboardsContainer,
|
||||
queryDashboardsExpand,
|
||||
renderDashboard,
|
||||
getDashboardsClear,
|
||||
getNotFoundForScope,
|
||||
queryDashboardsSearch,
|
||||
getNotFoundForFilter,
|
||||
getClustersSlothClusterEastRadio,
|
||||
getNotFoundForFilterClear,
|
||||
getNotFoundNoScopes,
|
||||
} from './testUtils';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
@ -182,6 +187,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', () => {
|
||||
@ -286,22 +302,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());
|
||||
@ -315,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', () => {
|
||||
|
@ -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, Icon, 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 && (
|
||||
<Input
|
||||
prefix={<Icon name="filter" />}
|
||||
className={styles.searchInput}
|
||||
<FilterInput
|
||||
placeholder={t('scopes.tree.search', 'Search')}
|
||||
defaultValue={node.query}
|
||||
value={queryValue}
|
||||
className={styles.searchInput}
|
||||
data-testid={`scopes-tree-${nodeId}-search`}
|
||||
onInput={(evt) => onQueryUpdate(nodePath, true, evt.currentTarget.value)}
|
||||
onChange={(value) => {
|
||||
setQueryValue(value);
|
||||
onQueryUpdate(nodePath, true, value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -99,20 +105,24 @@ export function ScopesTreeLevel({
|
||||
)
|
||||
) : null}
|
||||
|
||||
{childNode.isExpandable && (
|
||||
<IconButton
|
||||
name={!childNode.isExpanded ? 'angle-right' : 'angle-down'}
|
||||
{childNode.isExpandable ? (
|
||||
<button
|
||||
className={styles.itemExpand}
|
||||
data-testid={`scopes-tree-${childNode.name}-expand`}
|
||||
aria-label={
|
||||
childNode.isExpanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand')
|
||||
}
|
||||
data-testid={`scopes-tree-${childNode.name}-expand`}
|
||||
onClick={() => {
|
||||
onNodeUpdate(childNodePath, !childNode.isExpanded, childNode.query);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Icon name={!childNode.isExpanded ? 'angle-right' : 'angle-down'} />
|
||||
|
||||
<span data-testid={`scopes-tree-${childNode.name}-title`}>{childNode.title}</span>
|
||||
{childNode.title}
|
||||
</button>
|
||||
) : (
|
||||
<span data-testid={`scopes-tree-${childNode.name}-title`}>{childNode.title}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.itemChildren}>
|
||||
@ -159,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),
|
||||
}),
|
||||
|
@ -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<ScopeNode & { parent: string }> = [
|
||||
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' },
|
||||
@ -310,9 +331,12 @@ 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}`,
|
||||
notFoundNoScopes: 'scopes-dashboards-notFoundNoScopes',
|
||||
notFoundForScope: 'scopes-dashboards-notFoundForScope',
|
||||
notFoundForFilter: 'scopes-dashboards-notFoundForFilter',
|
||||
notFoundForFilterClear: 'scopes-dashboards-notFoundForFilter-clear',
|
||||
},
|
||||
};
|
||||
|
||||
@ -325,14 +349,18 @@ 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<HTMLInputElement>(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 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'));
|
||||
export const getApplicationsSearch = () => screen.getByTestId<HTMLInputElement>(selectors.tree.search('applications'));
|
||||
export const queryApplicationsSlothPictureFactoryTitle = () =>
|
||||
screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory'));
|
||||
export const getApplicationsSlothPictureFactoryTitle = () =>
|
||||
@ -359,6 +387,8 @@ export const getClustersSlothClusterNorthRadio = () =>
|
||||
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterNorth'));
|
||||
export const getClustersSlothClusterSouthRadio = () =>
|
||||
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterSouth'));
|
||||
export const getClustersSlothClusterEastRadio = () =>
|
||||
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('clusters-slothClusterEast'));
|
||||
|
||||
export function buildTestScene(overrides: Partial<DashboardScene> = {}) {
|
||||
return new DashboardScene({
|
||||
|
@ -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: () => {
|
||||
|
@ -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<DashboardDataDTO, 'DashboardW
|
||||
class K8sDashboardAPI implements DashboardAPI {
|
||||
private client: ResourceClient<DashboardDataDTO>;
|
||||
|
||||
constructor(private legacy: DashboardAPI) {
|
||||
constructor() {
|
||||
this.client = new ScopedResourceClient<DashboardDataDTO>({
|
||||
group: 'dashboard.grafana.app',
|
||||
version: 'v0alpha1',
|
||||
@ -62,23 +68,69 @@ class K8sDashboardAPI implements DashboardAPI {
|
||||
}
|
||||
|
||||
saveDashboard(options: SaveDashboardCommand): Promise<SaveDashboardResponseDTO> {
|
||||
return this.legacy.saveDashboard(options);
|
||||
const dashboard = options.dashboard as DashboardDataDTO; // type for the uid property
|
||||
const obj: ResourceForCreate<DashboardDataDTO> = {
|
||||
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<DashboardDataDTO>): 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<DeleteDashboardResponse> {
|
||||
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<DashboardDTO> {
|
||||
const dto = await this.client.subresource<DashboardWithAccessInfo>(uid, 'dto');
|
||||
const dash = await this.client.subresource<DashboardWithAccessInfo>(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;
|
||||
}
|
||||
|
@ -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<ObjectMeta>;
|
||||
}
|
||||
|
||||
export interface SaveDashboardFormProps {
|
||||
|
@ -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) {
|
||||
|
@ -12,17 +12,21 @@ export const supportedDatasources = new Set<string>([
|
||||
'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<string>([
|
||||
'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<string>([
|
||||
'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<string>([
|
||||
'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',
|
||||
|
@ -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<BackendSrvRequest, 'showErrorAlert' | 'showSuccessAlert'>
|
||||
) {
|
||||
return lastValueFrom(
|
||||
getBackendSrv().fetch<SaveDashboardResponse>({
|
||||
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) {
|
||||
|
@ -13,7 +13,7 @@ export const CallToAction = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" padding={5} gap={2} direction="column" alignItems="center" backgroundColor="secondary">
|
||||
<Box display="flex" gap={2} direction="column" alignItems="center" backgroundColor="secondary">
|
||||
<Text variant="h3" textAlignment="center">
|
||||
<Trans i18nKey="migrate-to-cloud.cta.header">Let us manage your Grafana stack</Trans>
|
||||
</Text>
|
||||
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<Stack direction="column">
|
||||
<CallToAction />
|
||||
<Box backgroundColor="secondary" display="flex" alignItems="center" direction="column">
|
||||
<Box maxWidth={180} paddingY={6} paddingX={2}>
|
||||
<Stack gap={5} direction="column">
|
||||
<CallToAction />
|
||||
|
||||
<Grid
|
||||
alignItems="flex-start"
|
||||
gap={1}
|
||||
columns={{
|
||||
xs: 1,
|
||||
lg: 2,
|
||||
}}
|
||||
>
|
||||
<InfoPaneLeft />
|
||||
<InfoPaneRight />
|
||||
</Grid>
|
||||
</Stack>
|
||||
</div>
|
||||
<Grid
|
||||
alignItems="flex-start"
|
||||
gap={4}
|
||||
columns={{
|
||||
xs: 1,
|
||||
lg: 2,
|
||||
}}
|
||||
>
|
||||
<InfoPaneLeft />
|
||||
<InfoPaneRight />
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
maxWidth: theme.breakpoints.values.xl,
|
||||
}),
|
||||
});
|
||||
|
@ -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 (
|
||||
<Box alignItems="flex-start" display="flex" padding={2} gap={2} direction="column" backgroundColor="secondary">
|
||||
<Stack gap={4} direction="column">
|
||||
<InfoItem
|
||||
title={t('migrate-to-cloud.what-is-cloud.title', 'What is Grafana Cloud?')}
|
||||
linkTitle={t('migrate-to-cloud.what-is-cloud.link-title', 'Learn about cloud features')}
|
||||
@ -17,6 +17,7 @@ export const InfoPaneLeft = () => {
|
||||
installation.
|
||||
</Trans>
|
||||
</InfoItem>
|
||||
|
||||
<InfoItem
|
||||
title={t('migrate-to-cloud.why-host.title', 'Why host with Grafana?')}
|
||||
linkTitle={t('migrate-to-cloud.why-host.link-title', 'More questions? Talk to an expert')}
|
||||
@ -27,6 +28,7 @@ export const InfoPaneLeft = () => {
|
||||
SLOs, incident management, machine learning, and powerful observability integrations.
|
||||
</Trans>
|
||||
</InfoItem>
|
||||
|
||||
<InfoItem
|
||||
title={t('migrate-to-cloud.is-it-secure.title', 'Is it secure?')}
|
||||
linkTitle={t('migrate-to-cloud.is-it-secure.link-title', 'Grafana Labs Trust Center')}
|
||||
@ -38,6 +40,6 @@ export const InfoPaneLeft = () => {
|
||||
unauthorized access, use, or disclosure.
|
||||
</Trans>
|
||||
</InfoItem>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<Box alignItems="flex-start" display="flex" direction="column" gap={2} padding={2} backgroundColor="secondary">
|
||||
<Stack gap={4} direction="column">
|
||||
<InfoItem
|
||||
title={t('migrate-to-cloud.pdc.title', 'Not all my data sources are on the public internet')}
|
||||
linkTitle={t('migrate-to-cloud.pdc.link-title', 'Learn about PDC')}
|
||||
@ -36,6 +36,6 @@ export const InfoPaneRight = () => {
|
||||
dashboards.
|
||||
</Trans>
|
||||
</InfoItem>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -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<ObjectMeta>;
|
||||
}
|
||||
|
||||
export interface AnnotationActions {
|
||||
|
@ -1656,8 +1656,11 @@
|
||||
"title": "Select scopes"
|
||||
},
|
||||
"suggestedDashboards": {
|
||||
"clear": "Clear search",
|
||||
"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",
|
||||
|
@ -1656,8 +1656,11 @@
|
||||
"title": "Ŝęľęčŧ şčőpęş"
|
||||
},
|
||||
"suggestedDashboards": {
|
||||
"clear": "Cľęäř şęäřčĥ",
|
||||
"loading": "Ŀőäđįʼnģ đäşĥþőäřđş",
|
||||
"noResultsForFilter": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy",
|
||||
"noResultsForFilterClear": "Cľęäř şęäřčĥ",
|
||||
"noResultsForScopes": "Ńő đäşĥþőäřđş ƒőūʼnđ ƒőř ŧĥę şęľęčŧęđ şčőpęş",
|
||||
"noResultsNoScopes": "Ńő şčőpęş şęľęčŧęđ",
|
||||
"search": "Ŝęäřčĥ",
|
||||
"toggle": {
|
||||
"collapse": "Cőľľäpşę şčőpę ƒįľŧęřş",
|
||||
|
10
yarn.lock
10
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"
|
||||
|
Loading…
Reference in New Issue
Block a user