diff --git a/.betterer.results b/.betterer.results
index 917b0d0f1f2..518606f1515 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1643,6 +1643,27 @@ 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-scene/v2schema/ImportDashboardFormV2.tsx:5381": [
+ [0, 0, 0, "Do not use any type assertions.", "0"],
+ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
+ [0, 0, 0, "Unexpected any. Specify a different type.", "3"],
+ [0, 0, 0, "Unexpected any. Specify a different type.", "4"],
+ [0, 0, 0, "Unexpected any. Specify a different type.", "5"]
+ ],
+ "public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx:5381": [
+ [0, 0, 0, "Do not use any type assertions.", "0"],
+ [0, 0, 0, "Do not use any type assertions.", "1"],
+ [0, 0, 0, "Do not use any type assertions.", "2"],
+ [0, 0, 0, "Do not use any type assertions.", "3"],
+ [0, 0, 0, "Do not use any type assertions.", "4"],
+ [0, 0, 0, "Do not use any type assertions.", "5"],
+ [0, 0, 0, "Do not use any type assertions.", "6"],
+ [0, 0, 0, "Do not use any type assertions.", "7"],
+ [0, 0, 0, "Do not use any type assertions.", "8"],
+ [0, 0, 0, "Do not use any type assertions.", "9"],
+ [0, 0, 0, "Unexpected any. Specify a different type.", "10"]
+ ],
"public/app/features/dashboard-scene/v2schema/test-helpers.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue
index 4e15cffeb3d..73faa5df734 100644
--- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue
+++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue
@@ -64,7 +64,6 @@ LibraryPanelSpec: {
id: number
// Title for the library panel in the dashboard
title: string
-
libraryPanel: LibraryPanelRef
}
diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx
new file mode 100644
index 00000000000..4df6d8faf52
--- /dev/null
+++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx
@@ -0,0 +1,164 @@
+import { useEffect, useState } from 'react';
+import { Controller, FieldErrors, UseFormReturn } from 'react-hook-form';
+
+import { selectors } from '@grafana/e2e-selectors';
+import { ExpressionDatasourceRef } from '@grafana/runtime/internal';
+import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
+import { Button, Field, FormFieldErrors, FormsOnSubmit, Stack, Input } from '@grafana/ui';
+import { FolderPicker } from 'app/core/components/Select/FolderPicker';
+import { t, Trans } from 'app/core/internationalization';
+import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
+import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
+import { DashboardInputs, DataSourceInput } from 'app/features/manage-dashboards/state/reducers';
+import { validateTitle } from 'app/features/manage-dashboards/utils/validation';
+interface Props
+ extends Pick<
+ UseFormReturn & { [key: `datasource-${string}`]: string }>,
+ 'register' | 'control' | 'getValues' | 'watch'
+ > {
+ inputs: DashboardInputs;
+ uidReset: boolean;
+ errors: FieldErrors & { [key: `datasource-${string}`]: string }>;
+ onCancel: () => void;
+ onUidReset: () => void;
+ onSubmit: FormsOnSubmit & { [key: `datasource-${string}`]: string }>;
+}
+
+export const ImportDashboardFormV2 = ({
+ register,
+ errors,
+ control,
+ inputs,
+ getValues,
+ uidReset,
+ onUidReset,
+ onCancel,
+ onSubmit,
+ watch,
+}: Props) => {
+ const [isSubmitted, setSubmitted] = useState(false);
+ const [selectedDataSources, setSelectedDataSources] = useState>({});
+ /*
+ This useEffect is needed for overwriting a dashboard. It
+ submits the form even if there's validation errors on title or uid.
+ */
+ useEffect(() => {
+ if (isSubmitted && (errors.dashboard?.title || errors.k8s?.name)) {
+ const formValues = getValues();
+ onSubmit({
+ ...formValues,
+ dashboard: {
+ ...formValues.dashboard,
+ title: formValues.dashboard.title,
+ },
+ });
+ }
+ }, [errors, getValues, isSubmitted, onSubmit]);
+
+ return (
+ <>
+ Options
+
+ await validateTitle(v, getValues().folderUid ?? ''),
+ })}
+ type="text"
+ data-testid={selectors.components.ImportDashboardForm.name}
+ />
+
+
+
+ render={({ field: { ref, value, onChange, ...field } }) => (
+ {
+ onChange(uid, title);
+ }}
+ value={value}
+ />
+ )}
+ name="folderUid"
+ control={control}
+ />
+
+ {inputs.dataSources &&
+ inputs.dataSources.map((input: DataSourceInput) => {
+ if (input.pluginId === ExpressionDatasourceRef.type) {
+ return null;
+ }
+
+ const dataSourceOption = `datasource-${input.pluginId}` as const;
+
+ return (
+
+
+ name={dataSourceOption}
+ render={({ field: { ref, ...field } }) => (
+ {
+ field.onChange(ds);
+ // Update our selected datasources map
+ setSelectedDataSources((prev) => ({
+ ...prev,
+ [input.pluginId]: {
+ uid: ds.uid,
+ type: ds.type,
+ },
+ }));
+ }}
+ />
+ )}
+ control={control}
+ rules={{ required: true }}
+ />
+
+ );
+ })}
+
+
+
+
+
+ >
+ );
+};
+
+function getButtonVariant(
+ errors: FormFieldErrors & { [key: `datasource-${string}`]: string }>
+) {
+ return errors && (errors.dashboard?.title || errors.k8s?.name) ? 'destructive' : 'primary';
+}
+
+function getButtonText(
+ errors: FormFieldErrors & { [key: `datasource-${string}`]: string }>
+) {
+ return errors && (errors.dashboard?.title || errors.k8s?.name) ? 'Import (Overwrite)' : 'Import';
+}
diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx
new file mode 100644
index 00000000000..ef0262adec5
--- /dev/null
+++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx
@@ -0,0 +1,181 @@
+import { useState } from 'react';
+
+import { locationUtil } from '@grafana/data';
+import { locationService, reportInteraction } from '@grafana/runtime';
+import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
+import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
+import { Form } from 'app/core/components/Form/Form';
+import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
+import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
+import { clearLoadedDashboard } from 'app/features/manage-dashboards/state/actions';
+import { useDispatch, useSelector, StoreState } from 'app/types';
+
+import { ImportDashboardFormV2 } from './ImportDashboardFormV2';
+
+const IMPORT_FINISHED_EVENT_NAME = 'dashboard_import_imported';
+
+export function ImportDashboardOverviewV2() {
+ const [uidReset, setUidReset] = useState(false);
+ const dispatch = useDispatch();
+
+ // Get state from Redux store
+ const searchObj = locationService.getSearchObject();
+ const dashboard = useSelector((state: StoreState) => state.importDashboard.dashboard as DashboardV2Spec);
+ const inputs = useSelector((state: StoreState) => state.importDashboard.inputs);
+ const folder = searchObj.folderUid ? { uid: String(searchObj.folderUid) } : { uid: '' };
+
+ function onUidReset() {
+ setUidReset(true);
+ }
+
+ function onCancel() {
+ dispatch(clearLoadedDashboard());
+ }
+
+ async function onSubmit(form: SaveDashboardCommand) {
+ reportInteraction(IMPORT_FINISHED_EVENT_NAME);
+
+ const dashboardWithDataSources: DashboardV2Spec = {
+ ...dashboard,
+ title: form.dashboard.title,
+ annotations: dashboard.annotations?.map((annotation: AnnotationQueryKind) => {
+ if (annotation.spec.datasource?.type) {
+ const dsType = annotation.spec.datasource.type;
+ if (form[`datasource-${dsType}` as keyof typeof form]) {
+ const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string };
+ return {
+ ...annotation,
+ spec: {
+ ...annotation.spec,
+ datasource: {
+ uid: ds.uid,
+ type: ds.type,
+ },
+ },
+ };
+ }
+ }
+ return annotation;
+ }),
+ variables: dashboard.variables?.map((variable) => {
+ if (variable.kind === 'QueryVariable') {
+ if (variable.spec.datasource?.type) {
+ const dsType = variable.spec.datasource.type;
+ if (form[`datasource-${dsType}` as keyof typeof form]) {
+ const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string };
+ return {
+ ...variable,
+ spec: {
+ ...variable.spec,
+ datasource: {
+ ...variable.spec.datasource,
+ uid: ds.uid,
+ type: ds.type,
+ },
+ options: [],
+ current: {
+ text: '',
+ value: '',
+ },
+ refresh: 'onDashboardLoad',
+ },
+ };
+ }
+ }
+ } else if (variable.kind === 'DatasourceVariable') {
+ return {
+ ...variable,
+ spec: {
+ ...variable.spec,
+ current: {
+ text: '',
+ value: '',
+ },
+ },
+ };
+ }
+ return variable;
+ }),
+ elements: Object.fromEntries(
+ Object.entries(dashboard.elements).map(([key, element]) => {
+ if (element.kind === 'Panel') {
+ const panel = { ...element.spec };
+ if (panel.data?.kind === 'QueryGroup') {
+ const newQueries = panel.data.spec.queries.map((query: any) => {
+ if (query.kind === 'PanelQuery') {
+ const queryType = query.spec.query?.kind;
+ // Match datasource by query kind
+ if (queryType && form[`datasource-${queryType}` as keyof typeof form]) {
+ const ds = form[`datasource-${queryType}` as keyof typeof form] as { uid: string; type: string };
+ return {
+ ...query,
+ spec: {
+ ...query.spec,
+ datasource: {
+ uid: ds.uid,
+ type: ds.type,
+ },
+ },
+ };
+ }
+ }
+ return query;
+ });
+ panel.data = {
+ ...panel.data,
+ spec: {
+ ...panel.data.spec,
+ queries: newQueries,
+ },
+ };
+ }
+ return [
+ key,
+ {
+ kind: element.kind,
+ spec: panel,
+ },
+ ];
+ }
+ return [key, element];
+ })
+ ),
+ };
+
+ const result = await getDashboardAPI('v2').saveDashboard({
+ ...form,
+ dashboard: dashboardWithDataSources,
+ });
+
+ if (result.url) {
+ const dashboardUrl = locationUtil.stripBaseFromUrl(result.url);
+ locationService.push(dashboardUrl);
+ }
+ }
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx
index bfebb0cec81..43297d1ec37 100644
--- a/public/app/features/manage-dashboards/DashboardImportPage.tsx
+++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx
@@ -27,12 +27,14 @@ import { Form } from 'app/core/components/Form/Form';
import { Page } from 'app/core/components/Page/Page';
import { t, Trans } from 'app/core/internationalization';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
+import { dispatch } from 'app/store/store';
import { StoreState } from 'app/types';
import { cleanUpAction } from '../../core/actions/cleanUp';
+import { ImportDashboardOverviewV2 } from '../dashboard-scene/v2schema/ImportDashboardOverviewV2';
import { ImportDashboardOverview } from './components/ImportDashboardOverview';
-import { fetchGcomDashboard, importDashboardJson } from './state/actions';
+import { fetchGcomDashboard, importDashboardJson, importDashboardV2Json } from './state/actions';
import { initialImportDashboardState } from './state/reducers';
import { validateDashboardJson, validateGcomDashboard } from './utils/validation';
@@ -53,6 +55,7 @@ const JSON_PLACEHOLDER = `{
const mapStateToProps = (state: StoreState) => ({
loadingState: state.importDashboard.state,
+ dashboard: state.importDashboard.dashboard,
});
const mapDispatchToProps = {
@@ -88,7 +91,13 @@ class UnthemedDashboardImport extends PureComponent {
});
try {
- this.props.importDashboardJson(JSON.parse(String(result)));
+ const json = JSON.parse(String(result));
+
+ if (json.elements) {
+ dispatch(importDashboardV2Json(json));
+ return;
+ }
+ this.props.importDashboardJson(json);
} catch (error) {
if (error instanceof Error) {
appEvents.emit(AppEvents.alertError, ['Import failed', 'JSON -> JS Serialization failed: ' + error.message]);
@@ -102,7 +111,14 @@ class UnthemedDashboardImport extends PureComponent {
import_source: 'json_pasted',
});
- this.props.importDashboardJson(JSON.parse(formData.dashboardJson));
+ const dashboard = JSON.parse(formData.dashboardJson);
+
+ if (dashboard.elements) {
+ dispatch(importDashboardV2Json(dashboard));
+ return;
+ }
+
+ this.props.importDashboardJson(dashboard);
};
getGcomDashboard = (formData: { gcomDashboard: string }) => {
@@ -229,6 +245,19 @@ class UnthemedDashboardImport extends PureComponent {
subTitle: 'Import dashboard from file or Grafana.com',
};
+ getDashboardOverview() {
+ const { loadingState, dashboard } = this.props;
+
+ if (loadingState === LoadingState.Done) {
+ if (dashboard.elements) {
+ return ;
+ }
+ return ;
+ }
+
+ return null;
+ }
+
render() {
const { loadingState } = this.props;
@@ -243,7 +272,7 @@ class UnthemedDashboardImport extends PureComponent {
)}
{[LoadingState.Error, LoadingState.NotStarted].includes(loadingState) && this.renderImportForm()}
- {loadingState === LoadingState.Done && }
+ {this.getDashboardOverview()}
);
diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts
index 48a8462a7f9..a5ddf74bbc6 100644
--- a/public/app/features/manage-dashboards/state/actions.ts
+++ b/public/app/features/manage-dashboards/state/actions.ts
@@ -1,8 +1,10 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { getBackendSrv, getDataSourceSrv, isFetchError } from '@grafana/runtime';
+import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types';
import {
@@ -18,6 +20,7 @@ import { DashboardJson } from '../types';
import {
clearDashboard,
+ DataSourceInput,
fetchDashboard,
fetchFailed,
ImportDashboardDTO,
@@ -56,6 +59,13 @@ export function importDashboardJson(dashboard: any): ThunkResult {
};
}
+export function importDashboardV2Json(dashboard: DashboardV2Spec): ThunkResult {
+ return async (dispatch) => {
+ dispatch(setJsonDashboard(dashboard));
+ dispatch(processV2Elements(dashboard));
+ };
+}
+
const getNewLibraryPanelsByInput = (input: Input, state: ImportDashboardState): LibraryPanel[] | undefined => {
return input?.usage?.libraryPanels?.filter((usageLibPanel) =>
state.inputs.libraryPanels.some(
@@ -142,6 +152,53 @@ function processElements(dashboardJson?: { __elements?: Record {
+ return async function (dispatch) {
+ const elements = dashboard.elements;
+ // get elements from dashboard
+ // each element can only be a panel
+ const inputs: Record = {};
+ for (const element of Object.values(elements)) {
+ if (element.kind !== 'Panel') {
+ throw new Error('Only panels are currenlty supported in v2 dashboards');
+ }
+
+ for (const query of element.spec.data.spec.queries) {
+ const datasourceRef = query.spec.datasource;
+ if (!datasourceRef) {
+ let dataSourceInput: DataSourceInput | undefined;
+ const dsType = query.spec.query.kind;
+ const datasource = await getDatasourceSrv().get({ type: dsType });
+ if (!datasource) {
+ dataSourceInput = {
+ name: dsType,
+ label: dsType,
+ info: `No data sources of type ${dsType} found`,
+ value: '',
+ type: InputType.DataSource,
+ pluginId: dsType,
+ };
+
+ inputs[dsType] = dataSourceInput;
+ } else {
+ dataSourceInput = {
+ name: datasource.name,
+ label: datasource.name,
+ info: `Select a ${datasource.name} data source`,
+ value: datasource.uid,
+ type: InputType.DataSource,
+ pluginId: datasource.meta?.id,
+ };
+
+ inputs[datasource.meta?.id] = dataSourceInput;
+ }
+ }
+ }
+ }
+ dispatch(setInputs(Object.values(inputs)));
+ };
+}
+
export async function getLibraryPanelInputs(dashboardJson?: {
__elements?: Record;
}): Promise {