grafana/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx
Alexa V 9ea1042329
Dashboard: POC to run existing e2e with dashboardScene feature toggle (#84598)
* Standarize e2e for addDashbaord e2e flow

* WIP: Duplicate e2e dashboard flows and smoke test for scene e2e tests

* Fix autoformatting mistake for bash file

* Enable dashboardScene using local storage and only for the scene folder

* Add missing folders

* Set the feature toggle in the before of all tests

* Revert "Standarize e2e for addDashbaord e2e flow"

This reverts commit 6b9ea9d5a4.

* Add missing e2e selectors to NavToolbarActions, and modify addDashboard scene flow

* e2e: panels_smokescreen.spec.ts migrated

* e2e smokeTestSceneario migrated

* Start migrating dashbaord-suite e2e

* WIP create variable types

* adjust tests for scenes

* restore dashboard json file

* update scenes version

* restore pkg/build/wire/internal/wire/testdata modifications

* finalising test adjusments

* restore pkg/build/wire/internal/wire/testdata files

* add latest scenes version and update tests

* add drone setup for dashboard scenes tests

* update to latest scenes version

* adjust drone errors

* adjust indentation in drone yml file

* drone adjustments

* add github workflow to run scenes e2e

* restore drone file

* adjust github workflow

* wip: github workflow adjustments

* test remove gpu

* bump

* undo formating changes

* wip: github workflow debugging

* adjusting flaky tests

* update to latest scenes

* clean up workflow file

* adjust flaky test

* clean up pr

* finalise worflow logic and add to codeowners

* clean up launching old arch dashboards e2e separately

---------

Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
Co-authored-by: Jeff Levin <jeff@levinology.com>
2024-05-01 17:56:48 +03:00

201 lines
6.3 KiB
TypeScript

import debounce from 'debounce-promise';
import React, { ChangeEvent } from 'react';
import { UseFormSetValue, useForm } from 'react-hook-form';
import { selectors } from '@grafana/e2e-selectors';
import { Dashboard } from '@grafana/schema';
import { Button, Input, Switch, Field, Label, TextArea, Stack, Alert, Box } from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardChangeInfo, NameAlreadyExistsError, SaveButton, isNameExistsError } from './shared';
import { useSaveDashboard } from './useSaveDashboard';
interface SaveDashboardAsFormDTO {
firstName?: string;
title: string;
description: string;
folder: { uid?: string; title?: string };
copyTags: boolean;
}
export interface Props {
dashboard: DashboardScene;
changeInfo: DashboardChangeInfo;
}
export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
const { changedSaveModel } = changeInfo;
const { register, handleSubmit, setValue, formState, getValues, watch } = useForm<SaveDashboardAsFormDTO>({
mode: 'onBlur',
defaultValues: {
title: changeInfo.isNew ? changedSaveModel.title! : `${changedSaveModel.title} Copy`,
description: changedSaveModel.description ?? '',
folder: {
uid: dashboard.state.meta.folderUid,
title: dashboard.state.meta.folderTitle,
},
copyTags: false,
},
});
const { errors, isValid, defaultValues } = formState;
const formValues = watch();
const { state, onSaveDashboard } = useSaveDashboard(false);
const onSave = async (overwrite: boolean) => {
const data = getValues();
const dashboardToSave: Dashboard = getSaveAsDashboardSaveModel(changedSaveModel, data, changeInfo.isNew);
const result = await onSaveDashboard(dashboard, dashboardToSave, { overwrite, folderUid: data.folder.uid });
if (result.status === 'success') {
dashboard.closeModal();
}
};
const cancelButton = (
<Button variant="secondary" onClick={() => dashboard.closeModal()} fill="outline">
Cancel
</Button>
);
const saveButton = (overwrite: boolean) => (
<SaveButton isValid={isValid} isLoading={state.loading} onSave={onSave} overwrite={overwrite} />
);
function renderFooter(error?: Error) {
if (isNameExistsError(error)) {
return <NameAlreadyExistsError cancelButton={cancelButton} saveButton={saveButton} />;
}
return (
<>
{error && (
<Alert title="Failed to save dashboard" severity="error">
<p>{error.message}</p>
</Alert>
)}
<Stack alignItems="center">
{cancelButton}
{saveButton(false)}
</Stack>
</>
);
}
return (
<form onSubmit={handleSubmit(() => onSave(false))}>
<Field
label={<TitleFieldLabel dashboard={changedSaveModel} onChange={setValue} />}
invalid={!!errors.title}
error={errors.title?.message}
>
<Input
{...register('title', { required: 'Required', validate: validateDashboardName })}
aria-label="Save dashboard title field"
data-testid={selectors.components.Drawer.DashboardSaveDrawer.saveAsTitleInput}
onChange={debounce(async (e: ChangeEvent<HTMLInputElement>) => {
setValue('title', e.target.value, { shouldValidate: true });
}, 400)}
/>
</Field>
<Field
label={<DescriptionLabel dashboard={changedSaveModel} onChange={setValue} />}
invalid={!!errors.description}
error={errors.description?.message}
>
<TextArea
{...register('description', { required: false })}
aria-label="Save dashboard description field"
autoFocus
/>
</Field>
<Field label="Folder">
<FolderPicker
onChange={(uid: string | undefined, title: string | undefined) => setValue('folder', { uid, title })}
// Old folder picker fields
value={formValues.folder?.uid}
initialTitle={defaultValues!.folder!.title}
dashboardId={changedSaveModel.id ?? undefined}
enableCreateNew
/>
</Field>
{!changeInfo.isNew && (
<Field label="Copy tags">
<Switch {...register('copyTags')} />
</Field>
)}
<Box paddingTop={2}>{renderFooter(state.error)}</Box>
</form>
);
}
export interface TitleLabelProps {
dashboard: Dashboard;
onChange: UseFormSetValue<SaveDashboardAsFormDTO>;
}
export function TitleFieldLabel(props: TitleLabelProps) {
return (
<Stack justifyContent="space-between">
<Label htmlFor="description">Title</Label>
{/* {config.featureToggles.dashgpt && isNew && (
<GenAIDashDescriptionButton
onGenerate={(description) => field.onChange(description)}
dashboard={dashboard}
/>
)} */}
</Stack>
);
}
export interface DescriptionLabelProps {
dashboard: Dashboard;
onChange: UseFormSetValue<SaveDashboardAsFormDTO>;
}
export function DescriptionLabel(props: DescriptionLabelProps) {
return (
<Stack justifyContent="space-between">
<Label htmlFor="description">Description</Label>
{/* {config.featureToggles.dashgpt && isNew && (
<GenAIDashDescriptionButton
onGenerate={(description) => field.onChange(description)}
dashboard={dashboard}
/>
)} */}
</Stack>
);
}
async function validateDashboardName(title: string, formValues: SaveDashboardAsFormDTO) {
if (title === formValues.folder.title?.trim()) {
return 'Dashboard name cannot be the same as folder name';
}
try {
await validationSrv.validateNewDashboardName(formValues.folder.uid ?? 'general', title);
return true;
} catch (e) {
return e instanceof Error ? e.message : 'Dashboard name is invalid';
}
}
function getSaveAsDashboardSaveModel(source: Dashboard, form: SaveDashboardAsFormDTO, isNew?: boolean): Dashboard {
// TODO remove old alerts and thresholds when copying (See getSaveAsDashboardClone)
return {
...source,
id: null,
uid: '',
title: form.title,
description: form.description,
tags: isNew || form.copyTags ? source.tags : [],
};
}