Dashboards: Keep save drawer open for unhandled errors (#70434)

* user essentials mob! 🔱

* user essentials mob! 🔱

lastFile:public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx

* user essentials mob! 🔱

lastFile:public/app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer.tsx

* user essentials mob! 🔱

lastFile:public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx

* rename isHandledError fn

* fix prettier

---------

Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: Laura Benz <laura.benz@grafana.com>
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Josh Hunt 2023-07-07 14:46:53 +01:00 committed by GitHub
parent ac78146091
commit b1e76a7036
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 32 additions and 19 deletions

View File

@ -2,14 +2,14 @@ import React, { useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import { config, isFetchError } from '@grafana/runtime';
import { Drawer, Spinner, Tab, TabsBar } from '@grafana/ui';
import { Drawer, Tab, TabsBar } from '@grafana/ui';
import { backendSrv } from 'app/core/services/backend_srv';
import { jsonDiff } from '../VersionHistory/utils';
import DashboardValidation from './DashboardValidation';
import { SaveDashboardDiff } from './SaveDashboardDiff';
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
import { proxyHandlesError, SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
import { SaveDashboardForm } from './forms/SaveDashboardForm';
import { SaveProvisionedDashboardForm } from './forms/SaveProvisionedDashboardForm';
@ -72,18 +72,11 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
return <SaveDashboardDiff diff={data.diff} oldValue={previous.value} newValue={data.clone} />;
}
if (state.loading) {
return (
<div>
<Spinner />
</div>
);
}
if (isNew || isCopy) {
return (
<SaveDashboardAsForm
dashboard={dashboard}
isLoading={state.loading}
onCancel={onDismiss}
onSuccess={onSuccess}
onSubmit={onDashboardSave}
@ -99,6 +92,7 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
return (
<SaveDashboardForm
dashboard={dashboard}
isLoading={state.loading}
saveModel={data}
onCancel={onDismiss}
onSuccess={onSuccess}
@ -109,7 +103,12 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
);
};
if (state.error && isFetchError(state.error) && !state.error.isHandled) {
if (
state.error &&
isFetchError(state.error) &&
!state.error.isHandled &&
proxyHandlesError(state.error.data.status)
) {
return (
<SaveDashboardErrorProxy
error={state.error}

View File

@ -28,7 +28,7 @@ export const SaveDashboardErrorProxy = ({
const { onDashboardSave } = useDashboardSave(dashboard);
useEffect(() => {
if (error.data && isHandledError(error.data.status)) {
if (error.data && proxyHandlesError(error.data.status)) {
error.isHandled = true;
}
}, [error]);
@ -109,7 +109,7 @@ const ConfirmPluginDashboardSaveModal = ({ onDismiss, dashboard }: SaveDashboard
);
};
const isHandledError = (errorStatus: string) => {
export const proxyHandlesError = (errorStatus: string) => {
switch (errorStatus) {
case 'version-mismatch':
case 'name-exists':

View File

@ -37,6 +37,7 @@ const renderAndSubmitForm = async (
) => {
render(
<SaveDashboardAsForm
isLoading={false}
dashboard={dashboard as DashboardModel}
onCancel={() => {}}
onSuccess={() => {}}

View File

@ -40,7 +40,14 @@ export interface SaveDashboardAsFormProps extends SaveDashboardFormProps {
isNew?: boolean;
}
export const SaveDashboardAsForm = ({ dashboard, isNew, onSubmit, onCancel, onSuccess }: SaveDashboardAsFormProps) => {
export const SaveDashboardAsForm = ({
dashboard,
isLoading,
isNew,
onSubmit,
onCancel,
onSuccess,
}: SaveDashboardAsFormProps) => {
const defaultValues: SaveDashboardAsFormDTO = {
title: isNew ? dashboard.title : `${dashboard.title} Copy`,
$folder: {
@ -129,8 +136,8 @@ export const SaveDashboardAsForm = ({ dashboard, isNew, onSubmit, onCancel, onSu
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
Cancel
</Button>
<Button type="submit" aria-label="Save dashboard button">
Save
<Button disabled={isLoading} type="submit" aria-label="Save dashboard button">
{isLoading ? 'Saving...' : 'Save'}
</Button>
</HorizontalGroup>
</>

View File

@ -34,6 +34,7 @@ const prepareDashboardMock = (
const renderAndSubmitForm = async (dashboard: DashboardModel, submitSpy: jest.Mock) => {
render(
<SaveDashboardForm
isLoading={false}
dashboard={dashboard}
onCancel={() => {}}
onSuccess={() => {}}
@ -62,6 +63,7 @@ describe('SaveDashboardAsForm', () => {
it('renders switches when variables or timerange', () => {
render(
<SaveDashboardForm
isLoading={false}
dashboard={prepareDashboardMock(true, true, jest.fn(), jest.fn())}
onCancel={() => {}}
onSuccess={() => {}}
@ -124,6 +126,7 @@ describe('SaveDashboardAsForm', () => {
it('renders saved message draft if it was filled before', () => {
render(
<SaveDashboardForm
isLoading={false}
dashboard={createDashboardModelFixture()}
onCancel={() => {}}
onSuccess={() => {}}

View File

@ -13,6 +13,7 @@ interface FormDTO {
export type SaveProps = {
dashboard: DashboardModel; // original
isLoading: boolean;
saveModel: SaveDashboardData; // already cloned
onCancel: () => void;
onSuccess: () => void;
@ -23,6 +24,7 @@ export type SaveProps = {
export const SaveDashboardForm = ({
dashboard,
isLoading,
saveModel,
options,
onSubmit,
@ -108,11 +110,11 @@ export const SaveDashboardForm = ({
</Button>
<Button
type="submit"
disabled={!saveModel.hasChanges}
disabled={!saveModel.hasChanges || isLoading}
icon={saving ? 'fa fa-spinner' : undefined}
aria-label={selectors.pages.SaveDashboardModal.save}
>
Save
{isLoading ? 'Saving...' : 'Save'}
</Button>
{!saveModel.hasChanges && <div>No changes to save</div>}
</Stack>

View File

@ -7,7 +7,7 @@ import { Button, ClipboardButton, HorizontalGroup, TextArea } from '@grafana/ui'
import { SaveDashboardFormProps } from '../types';
export const SaveProvisionedDashboardForm = ({ dashboard, onCancel }: SaveDashboardFormProps) => {
export const SaveProvisionedDashboardForm = ({ dashboard, onCancel }: Omit<SaveDashboardFormProps, 'isLoading'>) => {
const [dashboardJSON, setDashboardJson] = useState(() => {
const clone = dashboard.getSaveModelClone();
delete clone.id;

View File

@ -26,6 +26,7 @@ export interface SaveDashboardCommand {
export interface SaveDashboardFormProps {
dashboard: DashboardModel;
isLoading: boolean;
onCancel: () => void;
onSuccess: () => void;
onSubmit?: (clone: DashboardModel, options: SaveDashboardOptions, dashboard: DashboardModel) => Promise<any>;