AddToDashboardModal.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import { partial } from 'lodash';
  2. import React, { useEffect, useState } from 'react';
  3. import { DeepMap, FieldError, useForm } from 'react-hook-form';
  4. import { useSelector } from 'react-redux';
  5. import { locationUtil, SelectableValue } from '@grafana/data';
  6. import { config, locationService, reportInteraction } from '@grafana/runtime';
  7. import { Alert, Button, Field, InputControl, Modal, RadioButtonGroup } from '@grafana/ui';
  8. import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
  9. import { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
  10. import { ExploreId } from 'app/types';
  11. import { getExploreItemSelector } from '../state/selectors';
  12. import { setDashboardInLocalStorage, AddToDashboardError } from './addToDashboard';
  13. enum SaveTarget {
  14. NewDashboard = 'new-dashboard',
  15. ExistingDashboard = 'existing-dashboard',
  16. }
  17. const SAVE_TARGETS: Array<SelectableValue<SaveTarget>> = [
  18. {
  19. label: 'New dashboard',
  20. value: SaveTarget.NewDashboard,
  21. },
  22. {
  23. label: 'Existing dashboard',
  24. value: SaveTarget.ExistingDashboard,
  25. },
  26. ];
  27. interface SaveTargetDTO {
  28. saveTarget: SaveTarget;
  29. }
  30. interface SaveToNewDashboardDTO extends SaveTargetDTO {
  31. saveTarget: SaveTarget.NewDashboard;
  32. }
  33. interface SaveToExistingDashboard extends SaveTargetDTO {
  34. saveTarget: SaveTarget.ExistingDashboard;
  35. dashboardUid: string;
  36. }
  37. type FormDTO = SaveToNewDashboardDTO | SaveToExistingDashboard;
  38. function assertIsSaveToExistingDashboardError(
  39. errors: DeepMap<FormDTO, FieldError>
  40. ): asserts errors is DeepMap<SaveToExistingDashboard, FieldError> {
  41. // the shape of the errors object is always compatible with the type above, but we need to
  42. // explicitly assert its type so that TS can narrow down FormDTO to SaveToExistingDashboard
  43. // when we use it in the form.
  44. }
  45. function getDashboardURL(dashboardUid?: string) {
  46. return dashboardUid ? `d/${dashboardUid}` : 'dashboard/new';
  47. }
  48. enum GenericError {
  49. UNKNOWN = 'unknown-error',
  50. NAVIGATION = 'navigation-error',
  51. }
  52. interface SubmissionError {
  53. error: AddToDashboardError | GenericError;
  54. message: string;
  55. }
  56. interface Props {
  57. onClose: () => void;
  58. exploreId: ExploreId;
  59. }
  60. export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
  61. const exploreItem = useSelector(getExploreItemSelector(exploreId))!;
  62. const [submissionError, setSubmissionError] = useState<SubmissionError | undefined>();
  63. const {
  64. handleSubmit,
  65. control,
  66. formState: { errors },
  67. watch,
  68. } = useForm<FormDTO>({
  69. defaultValues: { saveTarget: SaveTarget.NewDashboard },
  70. });
  71. const saveTarget = watch('saveTarget');
  72. const onSubmit = async (openInNewTab: boolean, data: FormDTO) => {
  73. setSubmissionError(undefined);
  74. const dashboardUid = data.saveTarget === SaveTarget.ExistingDashboard ? data.dashboardUid : undefined;
  75. reportInteraction('e2d_submit', {
  76. newTab: openInNewTab,
  77. saveTarget: data.saveTarget,
  78. queries: exploreItem.queries.length,
  79. });
  80. try {
  81. await setDashboardInLocalStorage({
  82. dashboardUid,
  83. datasource: exploreItem.datasourceInstance?.getRef(),
  84. queries: exploreItem.queries,
  85. queryResponse: exploreItem.queryResponse,
  86. });
  87. } catch (error) {
  88. switch (error) {
  89. case AddToDashboardError.FETCH_DASHBOARD:
  90. setSubmissionError({ error, message: 'Could not fetch dashboard information. Please try again.' });
  91. break;
  92. case AddToDashboardError.SET_DASHBOARD_LS:
  93. setSubmissionError({ error, message: 'Could not add panel to dashboard. Please try again.' });
  94. break;
  95. default:
  96. setSubmissionError({ error: GenericError.UNKNOWN, message: 'Something went wrong. Please try again.' });
  97. }
  98. return;
  99. }
  100. const dashboardURL = getDashboardURL(dashboardUid);
  101. if (!openInNewTab) {
  102. onClose();
  103. locationService.push(locationUtil.stripBaseFromUrl(dashboardURL));
  104. return;
  105. }
  106. const didTabOpen = !!global.open(config.appUrl + dashboardURL, '_blank');
  107. if (!didTabOpen) {
  108. setSubmissionError({
  109. error: GenericError.NAVIGATION,
  110. message: 'Could not navigate to the selected dashboard. Please try again.',
  111. });
  112. removeDashboardToFetchFromLocalStorage();
  113. return;
  114. }
  115. onClose();
  116. };
  117. useEffect(() => {
  118. reportInteraction('e2d_open');
  119. }, []);
  120. return (
  121. <Modal title="Add panel to dashboard" onDismiss={onClose} isOpen>
  122. <form>
  123. <InputControl
  124. control={control}
  125. render={({ field: { ref, ...field } }) => (
  126. <Field label="Target dashboard" description="Choose where to add the panel.">
  127. <RadioButtonGroup options={SAVE_TARGETS} {...field} id="e2d-save-target" />
  128. </Field>
  129. )}
  130. name="saveTarget"
  131. />
  132. {saveTarget === SaveTarget.ExistingDashboard &&
  133. (() => {
  134. assertIsSaveToExistingDashboardError(errors);
  135. return (
  136. <InputControl
  137. render={({ field: { ref, value, onChange, ...field } }) => (
  138. <Field
  139. label="Dashboard"
  140. description="Select in which dashboard the panel will be created."
  141. error={errors.dashboardUid?.message}
  142. invalid={!!errors.dashboardUid}
  143. >
  144. <DashboardPicker
  145. {...field}
  146. inputId="e2d-dashboard-picker"
  147. defaultOptions
  148. onChange={(d) => onChange(d?.uid)}
  149. />
  150. </Field>
  151. )}
  152. control={control}
  153. name="dashboardUid"
  154. shouldUnregister
  155. rules={{ required: { value: true, message: 'This field is required.' } }}
  156. />
  157. );
  158. })()}
  159. {submissionError && (
  160. <Alert severity="error" title="Error adding the panel">
  161. {submissionError.message}
  162. </Alert>
  163. )}
  164. <Modal.ButtonRow>
  165. <Button type="reset" onClick={onClose} fill="outline" variant="secondary">
  166. Cancel
  167. </Button>
  168. <Button
  169. type="submit"
  170. variant="secondary"
  171. onClick={handleSubmit(partial(onSubmit, true))}
  172. icon="external-link-alt"
  173. >
  174. Open in new tab
  175. </Button>
  176. <Button type="submit" variant="primary" onClick={handleSubmit(partial(onSubmit, false))} icon="apps">
  177. Open dashboard
  178. </Button>
  179. </Modal.ButtonRow>
  180. </form>
  181. </Modal>
  182. );
  183. };