The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/explore/extensions/AddToDashboard/AddToDashboardForm.tsx

217 lines
6.9 KiB

import { partial } from 'lodash';
import React, { type ReactElement, useEffect, useState } from 'react';
import { DeepMap, FieldError, useForm } from 'react-hook-form';
import { locationUtil, SelectableValue } from '@grafana/data';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { Alert, Button, Field, InputControl, Modal, RadioButtonGroup } from '@grafana/ui';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { contextSrv } from 'app/core/services/context_srv';
import { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
import { AccessControlAction, useSelector } from 'app/types';
import { getExploreItemSelector } from '../../state/selectors';
import { setDashboardInLocalStorage, AddToDashboardError } from './addToDashboard';
enum SaveTarget {
NewDashboard = 'new-dashboard',
ExistingDashboard = 'existing-dashboard',
}
interface SaveTargetDTO {
saveTarget: SaveTarget;
}
interface SaveToNewDashboardDTO extends SaveTargetDTO {
saveTarget: SaveTarget.NewDashboard;
}
interface SaveToExistingDashboard extends SaveTargetDTO {
saveTarget: SaveTarget.ExistingDashboard;
dashboardUid: string;
}
type FormDTO = SaveToNewDashboardDTO | SaveToExistingDashboard;
function assertIsSaveToExistingDashboardError(
errors: DeepMap<FormDTO, FieldError>
): asserts errors is DeepMap<SaveToExistingDashboard, FieldError> {
// the shape of the errors object is always compatible with the type above, but we need to
// explicitly assert its type so that TS can narrow down FormDTO to SaveToExistingDashboard
// when we use it in the form.
}
function getDashboardURL(dashboardUid?: string) {
return dashboardUid ? `d/${dashboardUid}` : 'dashboard/new';
}
enum GenericError {
UNKNOWN = 'unknown-error',
NAVIGATION = 'navigation-error',
}
interface SubmissionError {
error: AddToDashboardError | GenericError;
message: string;
}
interface Props {
onClose: () => void;
exploreId: string;
}
export function AddToDashboardForm(props: Props): ReactElement {
const { exploreId, onClose } = props;
const exploreItem = useSelector(getExploreItemSelector(exploreId))!;
const [submissionError, setSubmissionError] = useState<SubmissionError | undefined>();
const {
handleSubmit,
control,
formState: { errors },
watch,
} = useForm<FormDTO>({
defaultValues: { saveTarget: SaveTarget.NewDashboard },
});
const canCreateDashboard = contextSrv.hasPermission(AccessControlAction.DashboardsCreate);
const canWriteDashboard = contextSrv.hasPermission(AccessControlAction.DashboardsWrite);
const saveTargets: Array<SelectableValue<SaveTarget>> = [];
if (canCreateDashboard) {
saveTargets.push({
label: 'New dashboard',
value: SaveTarget.NewDashboard,
});
}
if (canWriteDashboard) {
saveTargets.push({
label: 'Existing dashboard',
value: SaveTarget.ExistingDashboard,
});
}
const saveTarget = saveTargets.length > 1 ? watch('saveTarget') : saveTargets[0].value;
const onSubmit = async (openInNewTab: boolean, data: FormDTO) => {
setSubmissionError(undefined);
const dashboardUid = data.saveTarget === SaveTarget.ExistingDashboard ? data.dashboardUid : undefined;
reportInteraction('e_2_d_submit', {
newTab: openInNewTab,
saveTarget: data.saveTarget,
queries: exploreItem.queries.length,
});
try {
await setDashboardInLocalStorage({
dashboardUid,
datasource: exploreItem.datasourceInstance?.getRef(),
queries: exploreItem.queries,
queryResponse: exploreItem.queryResponse,
panelState: exploreItem?.panelsState,
});
} catch (error) {
switch (error) {
case AddToDashboardError.FETCH_DASHBOARD:
setSubmissionError({ error, message: 'Could not fetch dashboard information. Please try again.' });
break;
case AddToDashboardError.SET_DASHBOARD_LS:
setSubmissionError({ error, message: 'Could not add panel to dashboard. Please try again.' });
break;
default:
setSubmissionError({ error: GenericError.UNKNOWN, message: 'Something went wrong. Please try again.' });
}
return;
}
const dashboardURL = getDashboardURL(dashboardUid);
if (!openInNewTab) {
onClose();
locationService.push(locationUtil.stripBaseFromUrl(dashboardURL));
return;
}
const didTabOpen = !!global.open(config.appUrl + dashboardURL, '_blank');
if (!didTabOpen) {
setSubmissionError({
error: GenericError.NAVIGATION,
message: 'Could not navigate to the selected dashboard. Please try again.',
});
removeDashboardToFetchFromLocalStorage();
return;
}
onClose();
};
useEffect(() => {
reportInteraction('e_2_d_open');
}, []);
return (
<form>
{saveTargets.length > 1 && (
<InputControl
control={control}
render={({ field: { ref, ...field } }) => (
<Field label="Target dashboard" description="Choose where to add the panel.">
<RadioButtonGroup options={saveTargets} {...field} id="e2d-save-target" />
</Field>
)}
name="saveTarget"
/>
)}
{saveTarget === SaveTarget.ExistingDashboard &&
(() => {
assertIsSaveToExistingDashboardError(errors);
return (
<InputControl
render={({ field: { ref, value, onChange, ...field } }) => (
<Field
label="Dashboard"
description="Select in which dashboard the panel will be created."
error={errors.dashboardUid?.message}
invalid={!!errors.dashboardUid}
>
<DashboardPicker
{...field}
inputId="e2d-dashboard-picker"
defaultOptions
onChange={(d) => onChange(d?.uid)}
/>
</Field>
)}
control={control}
name="dashboardUid"
shouldUnregister
rules={{ required: { value: true, message: 'This field is required.' } }}
/>
);
})()}
{submissionError && (
<Alert severity="error" title="Error adding the panel">
{submissionError.message}
</Alert>
)}
<Modal.ButtonRow>
<Button type="reset" onClick={onClose} fill="outline" variant="secondary">
Cancel
</Button>
<Button
type="submit"
variant="secondary"
onClick={handleSubmit(partial(onSubmit, true))}
icon="external-link-alt"
>
Open in new tab
</Button>
<Button type="submit" variant="primary" onClick={handleSubmit(partial(onSubmit, false))} icon="apps">
Open dashboard
</Button>
</Modal.ButtonRow>
</form>
);
}