Dashboard Schema V2: Fix public dashboards and snapshots (#110060)
* wip; public dashboards and snapshots work * Chore: Fix example of major release (#110007) baldm0mma/ fix example of major release * CI: Push docker images to dockerhub on merges to main (#110056) * support extracting queries in schema V2 * fix lint and test * fix test * clean up * clean up * apply feedback about early returns * fix url issue when clicking open original dashboard in v1 * refactor to early returns * fix api version comparison --------- Co-authored-by: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com>
This commit is contained in:
co-authored by
Jev Forsberg
Kevin Minehart
parent
7c0a44579c
commit
11898abccb
@@ -159,6 +159,12 @@ func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context,
|
||||
|
||||
// buildMetricRequest merges public dashboard parameters with dashboard and returns a metrics request to be sent to query backend
|
||||
func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelID int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
||||
isV2 := dashboard.Data.Get("elements").Interface() != nil
|
||||
|
||||
if isV2 {
|
||||
return pd.buildMetricRequestV2(dashboard, publicDashboard, panelID, reqDTO)
|
||||
}
|
||||
|
||||
// group queries by panel
|
||||
queriesByPanel := groupQueriesByPanelId(dashboard.Data)
|
||||
queries, ok := queriesByPanel[panelID]
|
||||
@@ -183,6 +189,31 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.D
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pd *PublicDashboardServiceImpl) buildMetricRequestV2(dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelID int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) {
|
||||
// group queries by panel for V2
|
||||
queriesByPanel := groupQueriesByPanelIdV2(dashboard.Data)
|
||||
queries, ok := queriesByPanel[panelID]
|
||||
if !ok {
|
||||
return dtos.MetricRequest{}, models.ErrPanelNotFound.Errorf("buildMetricRequestV2: public dashboard panel not found")
|
||||
}
|
||||
|
||||
ts := buildTimeSettingsV2(dashboard, reqDTO, publicDashboard, panelID)
|
||||
|
||||
// determine safe resolution to query data at
|
||||
safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts)
|
||||
for i := range queries {
|
||||
queries[i].Set("intervalMs", safeInterval)
|
||||
queries[i].Set("maxDataPoints", safeResolution)
|
||||
queries[i].Set("queryCachingTTL", reqDTO.QueryCachingTTL)
|
||||
}
|
||||
|
||||
return dtos.MetricRequest{
|
||||
From: ts.From,
|
||||
To: ts.To,
|
||||
Queries: queries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json {
|
||||
result := make(map[int64][]*simplejson.Json)
|
||||
|
||||
@@ -191,6 +222,89 @@ func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.J
|
||||
return result
|
||||
}
|
||||
|
||||
func groupQueriesByPanelIdV2(dashboard *simplejson.Json) map[int64][]*simplejson.Json {
|
||||
result := make(map[int64][]*simplejson.Json)
|
||||
|
||||
elementsMap := dashboard.Get("elements").MustMap()
|
||||
for _, element := range elementsMap {
|
||||
element := simplejson.NewFromAny(element)
|
||||
|
||||
var panelQueries []*simplejson.Json
|
||||
hasExpression := panelHasAnExpressionSchemaV2(element)
|
||||
|
||||
// For schema v2, queries are nested in element.spec.data.spec.queries
|
||||
spec := element.Get("spec")
|
||||
if spec.Interface() == nil {
|
||||
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
||||
continue
|
||||
}
|
||||
|
||||
data := spec.Get("data")
|
||||
if data.Interface() == nil {
|
||||
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
||||
continue
|
||||
}
|
||||
|
||||
dataSpec := data.Get("spec")
|
||||
if dataSpec.Interface() == nil {
|
||||
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
||||
continue
|
||||
}
|
||||
|
||||
queries := dataSpec.Get("queries")
|
||||
if queries.Interface() == nil {
|
||||
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
||||
continue
|
||||
}
|
||||
|
||||
for _, queryObj := range queries.MustArray() {
|
||||
query := simplejson.NewFromAny(queryObj)
|
||||
|
||||
// Check if query is hidden (PanelQuery.spec.hidden)
|
||||
panelQuerySpec := query.Get("spec")
|
||||
if panelQuerySpec.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasExpression && panelQuerySpec.Get("hidden").MustBool() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract the actual query from PanelQuery.spec.query
|
||||
dataQueryKind := panelQuerySpec.Get("query")
|
||||
if dataQueryKind.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dataQuerySpec := dataQueryKind.Get("spec")
|
||||
if dataQuerySpec.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dataQuerySpec.Del("exemplar")
|
||||
|
||||
group := dataQueryKind.Get("group").MustString()
|
||||
|
||||
// if query target has no datasource, set it to have the datasource on the panel
|
||||
if _, ok := dataQuerySpec.CheckGet("datasource"); !ok {
|
||||
uid := getDataSourceUidFromJsonSchemaV2(dataQueryKind)
|
||||
datasource := map[string]any{"type": group, "uid": uid}
|
||||
dataQuerySpec.Set("datasource", datasource)
|
||||
}
|
||||
|
||||
// We don't support exemplars for public dashboards currently
|
||||
dataQuerySpec.Del("exemplar")
|
||||
|
||||
// The query object contains the DataQuery with the actual expression
|
||||
panelQueries = append(panelQueries, dataQuerySpec)
|
||||
}
|
||||
|
||||
result[element.Get("spec").Get("id").MustInt64()] = panelQueries
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func extractQueriesFromPanels(panels []any, result map[int64][]*simplejson.Json) {
|
||||
for _, panelObj := range panels {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
@@ -242,6 +356,54 @@ func panelHasAnExpression(panel *simplejson.Json) bool {
|
||||
return hasExpression
|
||||
}
|
||||
|
||||
func panelHasAnExpressionSchemaV2(panel *simplejson.Json) bool {
|
||||
var hasExpression bool
|
||||
|
||||
// For schema v2, check the nested structure: spec.data.spec.queries[].spec.query
|
||||
spec := panel.Get("spec")
|
||||
if spec.Interface() == nil {
|
||||
return hasExpression
|
||||
}
|
||||
|
||||
data := spec.Get("data")
|
||||
if data.Interface() == nil {
|
||||
return hasExpression
|
||||
}
|
||||
|
||||
dataSpec := data.Get("spec")
|
||||
if dataSpec.Interface() == nil {
|
||||
return hasExpression
|
||||
}
|
||||
|
||||
queries := dataSpec.Get("queries")
|
||||
if queries.Interface() == nil {
|
||||
return hasExpression
|
||||
}
|
||||
|
||||
for _, queryObj := range queries.MustArray() {
|
||||
query := simplejson.NewFromAny(queryObj)
|
||||
|
||||
// Navigate to the actual query object
|
||||
querySpec := query.Get("spec")
|
||||
if querySpec.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
queryData := querySpec.Get("query")
|
||||
if queryData.Interface() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this query is an expression
|
||||
if expr.NodeTypeFromDatasourceUID(getDataSourceUidFromJsonSchemaV2(queryData)) == expr.TypeCMDNode {
|
||||
hasExpression = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return hasExpression
|
||||
}
|
||||
|
||||
func getDataSourceUidFromJson(query *simplejson.Json) string {
|
||||
uid := query.Get("datasource").Get("uid").MustString()
|
||||
|
||||
@@ -253,6 +415,18 @@ func getDataSourceUidFromJson(query *simplejson.Json) string {
|
||||
return uid
|
||||
}
|
||||
|
||||
func getDataSourceUidFromJsonSchemaV2(query *simplejson.Json) string {
|
||||
// For schema v2, datasource info is in query.datasource
|
||||
uid := query.Get("datasource").Get("name").MustString()
|
||||
|
||||
// before 8.3 special types could be sent as datasource (expr)
|
||||
if uid == "" {
|
||||
uid = query.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
func sanitizeMetadataFromQueryData(res *backend.QueryDataResponse) {
|
||||
for k := range res.Responses {
|
||||
frames := res.Responses[k].Frames
|
||||
@@ -310,6 +484,28 @@ func buildTimeSettings(d *dashboards.Dashboard, reqDTO models.PublicDashboardQue
|
||||
}
|
||||
}
|
||||
|
||||
// buildTimeSettingsV2 builds time settings for V2 dashboards
|
||||
func buildTimeSettingsV2(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, pd *models.PublicDashboard, panelID int64) models.TimeSettings {
|
||||
from, to, timezone := getTimeRangeValuesOrDefaultV2(d, reqDTO, pd.TimeSelectionEnabled, panelID)
|
||||
|
||||
timeRange := NewTimeRange(from, to)
|
||||
|
||||
timeFrom, _ := timeRange.ParseFrom(
|
||||
gtime.WithLocation(timezone),
|
||||
)
|
||||
timeTo, _ := timeRange.ParseTo(
|
||||
gtime.WithLocation(timezone),
|
||||
)
|
||||
timeToAsEpoch := timeTo.UnixMilli()
|
||||
timeFromAsEpoch := timeFrom.UnixMilli()
|
||||
|
||||
// Were using epoch ms because this is used to build a MetricRequest, which is used by query caching, which want the time range in epoch milliseconds.
|
||||
return models.TimeSettings{
|
||||
From: strconv.FormatInt(timeFromAsEpoch, 10),
|
||||
To: strconv.FormatInt(timeToAsEpoch, 10),
|
||||
}
|
||||
}
|
||||
|
||||
// returns from, to and timezone from the request if the timeSelection is enabled or the dashboard default values
|
||||
func getTimeRangeValuesOrDefault(reqDTO models.PublicDashboardQueryDTO, d *dashboards.Dashboard, timeSelectionEnabled bool, panelID int64) (string, string, *time.Location) {
|
||||
from := d.Data.GetPath("time", "from").MustString()
|
||||
@@ -344,6 +540,43 @@ func getTimeRangeValuesOrDefault(reqDTO models.PublicDashboardQueryDTO, d *dashb
|
||||
return from, to, timezone
|
||||
}
|
||||
|
||||
// getTimeRangeValuesOrDefaultV2 returns from, to and timezone from the request if the timeSelection is enabled or the dashboard default values for V2
|
||||
func getTimeRangeValuesOrDefaultV2(d *dashboards.Dashboard, reqDTO models.PublicDashboardQueryDTO, timeSelectionEnabled bool, panelID int64) (string, string, *time.Location) {
|
||||
// In V2, time settings are in dashboard.timeSettings
|
||||
timeSettings := d.Data.Get("timeSettings")
|
||||
from := timeSettings.Get("from").MustString()
|
||||
to := timeSettings.Get("to").MustString()
|
||||
dashboardTimezone := timeSettings.Get("timezone").MustString()
|
||||
|
||||
// Check for panel-specific time override in V2 structure
|
||||
panelRelativeTime := getPanelRelativeTimeRangeV2(d.Data, panelID)
|
||||
if panelRelativeTime != "" {
|
||||
from = panelRelativeTime
|
||||
}
|
||||
|
||||
// we use the values from the request if the time selection is enabled and the values are valid
|
||||
if timeSelectionEnabled {
|
||||
if reqDTO.TimeRange.From != "" && reqDTO.TimeRange.To != "" {
|
||||
from = reqDTO.TimeRange.From
|
||||
to = reqDTO.TimeRange.To
|
||||
}
|
||||
|
||||
if reqDTO.TimeRange.Timezone != "" {
|
||||
if userTimezone, err := time.LoadLocation(reqDTO.TimeRange.Timezone); err == nil {
|
||||
return from, to, userTimezone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the dashboardTimezone is blank or there is an error default is UTC
|
||||
timezone, err := time.LoadLocation(dashboardTimezone)
|
||||
if err != nil {
|
||||
return from, to, time.UTC
|
||||
}
|
||||
|
||||
return from, to, timezone
|
||||
}
|
||||
|
||||
func getPanelRelativeTimeRange(dashboard *simplejson.Json, panelID int64) string {
|
||||
for _, panelObj := range dashboard.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
@@ -355,3 +588,51 @@ func getPanelRelativeTimeRange(dashboard *simplejson.Json, panelID int64) string
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func getPanelRelativeTimeRangeV2(dashboard *simplejson.Json, panelID int64) string {
|
||||
// In V2, check elements for panel-specific time settings
|
||||
elements := dashboard.Get("elements")
|
||||
if elements.Interface() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
elementsMap := elements.MustMap()
|
||||
for _, element := range elementsMap {
|
||||
element := simplejson.NewFromAny(element)
|
||||
|
||||
// Check if this is the panel we're looking for
|
||||
if element.Get("spec").Get("id").MustInt64() != panelID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for time override in data.spec.queryOptions.timeFrom
|
||||
spec := element.Get("spec")
|
||||
if spec.Interface() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
data := spec.Get("data")
|
||||
if data.Interface() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
dataSpec := data.Get("spec")
|
||||
if dataSpec.Interface() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
queryOptions := dataSpec.Get("queryOptions")
|
||||
if queryOptions.Interface() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
timeFrom := queryOptions.Get("timeFrom")
|
||||
if timeFrom.Interface() != nil {
|
||||
return timeFrom.MustString()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
AnnoKeyManagerKind,
|
||||
AnnoKeySourcePath,
|
||||
} from 'app/features/apiserver/types';
|
||||
import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers';
|
||||
import { ensureV2Response, transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers';
|
||||
import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
import { isDashboardV2Resource, isDashboardV2Spec, isV2StoredVersion } from 'app/features/dashboard/api/utils';
|
||||
import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
@@ -408,6 +408,10 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
const rsp = await dashboardLoaderSrv.loadSnapshot(slug);
|
||||
|
||||
if (rsp?.dashboard) {
|
||||
if (isDashboardV2Spec(rsp.dashboard)) {
|
||||
throw new DashboardVersionError('v2beta1', 'Using legacy snapshot API to get a V2 dashboard');
|
||||
}
|
||||
|
||||
const scene = transformSaveModelToScene(rsp);
|
||||
return scene;
|
||||
}
|
||||
@@ -451,7 +455,14 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
case DashboardRoutes.Provisioning:
|
||||
return this.loadProvisioningDashboard(slug || '', uid);
|
||||
case DashboardRoutes.Public: {
|
||||
return await dashboardLoaderSrv.loadDashboard('public', '', uid);
|
||||
const result = await dashboardLoaderSrv.loadDashboard('public', '', uid);
|
||||
// public dashboards use legacy API but can return V2 dashboards
|
||||
// in this case we need to throw a dashboard version error so that the call can be delegated
|
||||
// to V2 state manager which will run fetchDashboard
|
||||
if (isDashboardV2Spec(result.dashboard)) {
|
||||
throw new DashboardVersionError('v2beta1', 'Using legacy public dashboard API to get a V2 dashboard');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
// If reloadDashboardsOnParamsChange is on, we need to process query params for dashboard load
|
||||
@@ -583,9 +594,10 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
|
||||
|
||||
public async loadSnapshotScene(slug: string): Promise<DashboardScene> {
|
||||
const rsp = await this.dashboardLoader.loadSnapshot(slug);
|
||||
const v2Response = ensureV2Response(rsp);
|
||||
|
||||
if (rsp?.spec) {
|
||||
const scene = transformSaveModelSchemaV2ToScene(rsp);
|
||||
if (v2Response.spec) {
|
||||
const scene = transformSaveModelSchemaV2ToScene(v2Response);
|
||||
return scene;
|
||||
}
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
|
||||
let annotationLayers: SceneDataLayerProvider[] = [];
|
||||
let alertStatesLayer: AlertStatesDataLayer | undefined;
|
||||
const uid = oldModel.uid;
|
||||
const serializerVersion = config.featureToggles.dashboardNewLayouts ? 'v2' : 'v1';
|
||||
const serializerVersion = config.featureToggles.dashboardNewLayouts && !oldModel.meta.isSnapshot ? 'v2' : 'v1';
|
||||
|
||||
if (oldModel.meta.isSnapshot) {
|
||||
variables = createVariablesForSnapshot(oldModel);
|
||||
|
||||
+100
-2
@@ -1,6 +1,6 @@
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { AnnotationQuery } from '@grafana/data';
|
||||
import { AnnotationQuery, isEmptyObject, TimeRange } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
behaviors,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { DataSourceRef, VariableRefresh } from '@grafana/schema';
|
||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||
|
||||
import {
|
||||
@@ -497,6 +497,104 @@ export function getDefaultDataSourceRef(): DataSourceRef {
|
||||
return { type: ds.meta.id, uid: ds.name }; // in the datasource list from bootData "id" is the type
|
||||
}
|
||||
|
||||
export function trimDashboardForSnapshot(title: string, time: TimeRange, dash: DashboardV2Spec, panel?: VizPanel) {
|
||||
let spec: DashboardV2Spec = {
|
||||
...dash,
|
||||
title,
|
||||
timeSettings: {
|
||||
...dash.timeSettings,
|
||||
from: time.from.toISOString(),
|
||||
to: time.to.toISOString(),
|
||||
},
|
||||
links: [],
|
||||
};
|
||||
|
||||
// When VizPanel is present, we are snapshoting a single panel. The rest of the panels is removed from the dashboard,
|
||||
// and the panel is resized to 24x20 grid and placed at the top of the dashboard.
|
||||
if (panel) {
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
|
||||
// Find the panel in elements
|
||||
const panelElementKey = Object.keys(dash.elements || {}).find((key) => {
|
||||
const element = dash.elements![key];
|
||||
return element.spec.id === panelId;
|
||||
});
|
||||
|
||||
if (panelElementKey) {
|
||||
// Keep only this panel in elements
|
||||
spec.elements = {
|
||||
[panelElementKey]: dash.elements![panelElementKey],
|
||||
};
|
||||
|
||||
spec.layout = {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
element: {
|
||||
kind: 'ElementReference',
|
||||
name: panelElementKey,
|
||||
},
|
||||
width: 24,
|
||||
height: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Remove links from all panels
|
||||
spec.elements = Object.fromEntries(
|
||||
Object.entries(spec.elements).map(([key, element]) => {
|
||||
if ('links' in element) {
|
||||
element.links = [];
|
||||
}
|
||||
return [key, element];
|
||||
})
|
||||
);
|
||||
|
||||
if (spec.annotations) {
|
||||
const annotations = spec.annotations.filter((annotation) => annotation.spec.enable) || [];
|
||||
const trimedAnnotations = annotations.map((annotation): AnnotationQueryKind => {
|
||||
return {
|
||||
kind: 'AnnotationQuery',
|
||||
spec: {
|
||||
name: annotation.spec.name,
|
||||
enable: annotation.spec.enable,
|
||||
iconColor: annotation.spec.iconColor,
|
||||
builtIn: annotation.spec.builtIn,
|
||||
hide: annotation.spec.hide,
|
||||
query: annotation.spec.query,
|
||||
},
|
||||
};
|
||||
});
|
||||
spec.annotations = trimedAnnotations;
|
||||
}
|
||||
|
||||
if (spec.variables) {
|
||||
spec.variables.forEach((variable) => {
|
||||
if ('query' in variable) {
|
||||
variable.query = '';
|
||||
}
|
||||
if ('options' in variable && 'current' in variable) {
|
||||
variable.options = variable.current && !isEmptyObject(variable.current) ? [variable.current] : [];
|
||||
}
|
||||
|
||||
if ('refresh' in variable) {
|
||||
variable.refresh = VariableRefresh.never;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
// Function to know if the dashboard transformed is a valid DashboardV2Spec
|
||||
export function validateDashboardSchemaV2(dash: unknown): dash is DashboardV2Spec {
|
||||
if (typeof dash !== 'object' || dash === null || Array.isArray(dash)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup, Stack } from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
@@ -12,8 +13,13 @@ import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/co
|
||||
import { getDashboardSnapshotSrv, SnapshotSharingOptions } from 'app/features/dashboard/services/SnapshotSrv';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { Spec as DashboardV2Spec } from '../../../../../packages/grafana-schema/src/schema/dashboard/v2';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { transformSceneToSaveModel, trimDashboardForSnapshot } from '../serialization/transformSceneToSaveModel';
|
||||
import {
|
||||
transformSceneToSaveModelSchemaV2,
|
||||
trimDashboardForSnapshot as trimDashboardForSnapshotV2,
|
||||
} from '../serialization/transformSceneToSaveModelSchemaV2';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
|
||||
import { SceneShareTabState, ShareView } from './types';
|
||||
@@ -55,6 +61,12 @@ export interface ShareSnapshotTabState extends SceneShareTabState {
|
||||
snapshotSharingOptions?: SnapshotSharingOptions;
|
||||
}
|
||||
|
||||
// this is a hacky way to pass the uid with the dashboard to the backend so the dashboard can be found
|
||||
// and snapshot can be created
|
||||
interface DashboardV2SpecWithUid extends DashboardV2Spec {
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> implements ShareView {
|
||||
public tabId = shareDashboardType.snapshot;
|
||||
static Component = ShareSnapshotTabRenderer;
|
||||
@@ -102,7 +114,26 @@ export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> imp
|
||||
private prepareSnapshot() {
|
||||
const timeRange = sceneGraph.getTimeRange(this);
|
||||
const { dashboardRef, panelRef } = this.state;
|
||||
const saveModel = transformSceneToSaveModel(dashboardRef.resolve(), true);
|
||||
|
||||
let saveModel: Dashboard | DashboardV2SpecWithUid;
|
||||
|
||||
const apiVersion = dashboardRef.resolve().serializer.apiVersion;
|
||||
|
||||
const isV2Dashboard =
|
||||
apiVersion === 'dashboard.grafana.app/v2beta1' || apiVersion === 'dashboard.grafana.app/v2alpha1';
|
||||
|
||||
if (isV2Dashboard) {
|
||||
saveModel = transformSceneToSaveModelSchemaV2(dashboardRef.resolve(), true);
|
||||
saveModel.uid = dashboardRef.resolve().serializer.getK8SMetadata()?.name;
|
||||
return trimDashboardForSnapshotV2(
|
||||
this.state.snapshotName.trim() || '',
|
||||
timeRange.state.value,
|
||||
saveModel,
|
||||
panelRef?.resolve()
|
||||
);
|
||||
}
|
||||
|
||||
saveModel = transformSceneToSaveModel(dashboardRef.resolve(), true);
|
||||
|
||||
return trimDashboardForSnapshot(
|
||||
this.state.snapshotName.trim() || '',
|
||||
|
||||
@@ -78,7 +78,7 @@ import {
|
||||
import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardWithAccessInfo } from './types';
|
||||
import { isDashboardResource, isDashboardV0Spec, isDashboardV2Resource } from './utils';
|
||||
import { isDashboardResource, isDashboardV0Spec, isDashboardV2Resource, isDashboardV2Spec } from './utils';
|
||||
|
||||
export function ensureV2Response(
|
||||
dto: DashboardDTO | DashboardWithAccessInfo<DashboardDataDTO> | DashboardWithAccessInfo<DashboardV2Spec>
|
||||
@@ -94,14 +94,6 @@ export function ensureV2Response(
|
||||
dashboard = dto.dashboard;
|
||||
}
|
||||
|
||||
const timeSettingsDefaults = defaultTimeSettingsSpec();
|
||||
const dashboardDefaults = defaultDashboardV2Spec();
|
||||
const [elements, layout] = getElementsFromPanels(dashboard.panels || []);
|
||||
// @ts-expect-error - dashboard.templating.list is VariableModel[] and we need TypedVariableModel[] here
|
||||
// that would allow accessing unique properties for each variable type that the API returns
|
||||
const variables = getVariables(dashboard.templating?.list || []);
|
||||
const annotations = getAnnotations(dashboard.annotations?.list || []);
|
||||
|
||||
let accessMeta: DashboardWithAccessInfo<DashboardV2Spec>['access'];
|
||||
let annotationsMeta: DashboardWithAccessInfo<DashboardV2Spec>['metadata']['annotations'];
|
||||
let labelsMeta: DashboardWithAccessInfo<DashboardV2Spec>['metadata']['labels'];
|
||||
@@ -154,6 +146,36 @@ export function ensureV2Response(
|
||||
annotationsMeta[AnnoKeyDashboardSnapshotOriginalUrl] = dashboard.snapshot?.originalUrl;
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
creationTimestamp: creationTimestamp || '', // TODO verify this empty string is valid
|
||||
name: dashboard.uid,
|
||||
resourceVersion: dashboard.version?.toString() || '0',
|
||||
annotations: annotationsMeta,
|
||||
labels: labelsMeta,
|
||||
};
|
||||
|
||||
if (!isDashboardResource(dto)) {
|
||||
if (isDashboardV2Spec(dto.dashboard)) {
|
||||
// sometimes we can have a v2 spec returned through legacy api like public dashboard
|
||||
// in that case we need to return dashboard as it is, since the conversion is not needed
|
||||
return {
|
||||
apiVersion: 'v2beta1',
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
metadata,
|
||||
spec: dto.dashboard,
|
||||
access: accessMeta,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const timeSettingsDefaults = defaultTimeSettingsSpec();
|
||||
const dashboardDefaults = defaultDashboardV2Spec();
|
||||
const [elements, layout] = getElementsFromPanels(dashboard.panels || []);
|
||||
// @ts-expect-error - dashboard.templating.list is VariableModel[] and we need TypedVariableModel[] here
|
||||
// that would allow accessing unique properties for each variable type that the API returns
|
||||
const variables = getVariables(dashboard.templating?.list || []);
|
||||
const annotations = getAnnotations(dashboard.annotations?.list || []);
|
||||
|
||||
const spec: DashboardV2Spec = {
|
||||
title: dashboard.title,
|
||||
description: dashboard.description,
|
||||
@@ -185,13 +207,7 @@ export function ensureV2Response(
|
||||
return {
|
||||
apiVersion: 'v2beta1',
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
metadata: {
|
||||
creationTimestamp: creationTimestamp || '', // TODO verify this empty string is valid
|
||||
name: dashboard.uid,
|
||||
resourceVersion: dashboard.version?.toString() || '0',
|
||||
annotations: annotationsMeta,
|
||||
labels: labelsMeta,
|
||||
},
|
||||
metadata,
|
||||
spec,
|
||||
access: accessMeta,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user