ImportDashboardForm.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import React, { FC, useEffect, useState } from 'react';
  2. import { selectors } from '@grafana/e2e-selectors';
  3. import { DataSourcePicker } from '@grafana/runtime';
  4. import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
  5. import {
  6. Button,
  7. Field,
  8. FormAPI,
  9. FormFieldErrors,
  10. FormsOnSubmit,
  11. HorizontalGroup,
  12. Input,
  13. InputControl,
  14. Legend,
  15. } from '@grafana/ui';
  16. import { FolderPicker } from 'app/core/components/Select/FolderPicker';
  17. import {
  18. DashboardInput,
  19. DashboardInputs,
  20. DataSourceInput,
  21. ImportDashboardDTO,
  22. LibraryPanelInputState,
  23. } from '../state/reducers';
  24. import { validateTitle, validateUid } from '../utils/validation';
  25. import { ImportDashboardLibraryPanelsList } from './ImportDashboardLibraryPanelsList';
  26. interface Props extends Pick<FormAPI<ImportDashboardDTO>, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> {
  27. uidReset: boolean;
  28. inputs: DashboardInputs;
  29. initialFolderId: number;
  30. onCancel: () => void;
  31. onUidReset: () => void;
  32. onSubmit: FormsOnSubmit<ImportDashboardDTO>;
  33. }
  34. export const ImportDashboardForm: FC<Props> = ({
  35. register,
  36. errors,
  37. control,
  38. getValues,
  39. uidReset,
  40. inputs,
  41. initialFolderId,
  42. onUidReset,
  43. onCancel,
  44. onSubmit,
  45. watch,
  46. }) => {
  47. const [isSubmitted, setSubmitted] = useState(false);
  48. const watchDataSources = watch('dataSources');
  49. const watchFolder = watch('folder');
  50. /*
  51. This useEffect is needed for overwriting a dashboard. It
  52. submits the form even if there's validation errors on title or uid.
  53. */
  54. useEffect(() => {
  55. if (isSubmitted && (errors.title || errors.uid)) {
  56. onSubmit(getValues(), {} as any);
  57. }
  58. }, [errors, getValues, isSubmitted, onSubmit]);
  59. const newLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.New) ?? [];
  60. const existingLibraryPanels = inputs?.libraryPanels?.filter((i) => i.state === LibraryPanelInputState.Exists) ?? [];
  61. return (
  62. <>
  63. <Legend>Options</Legend>
  64. <Field label="Name" invalid={!!errors.title} error={errors.title && errors.title.message}>
  65. <Input
  66. {...register('title', {
  67. required: 'Name is required',
  68. validate: async (v: string) => await validateTitle(v, getValues().folder.id),
  69. })}
  70. type="text"
  71. data-testid={selectors.components.ImportDashboardForm.name}
  72. />
  73. </Field>
  74. <Field label="Folder">
  75. <InputControl
  76. render={({ field: { ref, ...field } }) => (
  77. <FolderPicker {...field} enableCreateNew initialFolderId={initialFolderId} />
  78. )}
  79. name="folder"
  80. control={control}
  81. />
  82. </Field>
  83. <Field
  84. label="Unique identifier (UID)"
  85. description="The unique identifier (UID) of a dashboard can be used for uniquely identify a dashboard between multiple Grafana installs.
  86. The UID allows having consistent URLs for accessing dashboards so changing the title of a dashboard will not break any
  87. bookmarked links to that dashboard."
  88. invalid={!!errors.uid}
  89. error={errors.uid && errors.uid.message}
  90. >
  91. <>
  92. {!uidReset ? (
  93. <Input
  94. disabled
  95. {...register('uid', { validate: async (v: string) => await validateUid(v) })}
  96. addonAfter={!uidReset && <Button onClick={onUidReset}>Change uid</Button>}
  97. />
  98. ) : (
  99. <Input {...register('uid', { required: true, validate: async (v: string) => await validateUid(v) })} />
  100. )}
  101. </>
  102. </Field>
  103. {inputs.dataSources &&
  104. inputs.dataSources.map((input: DataSourceInput, index: number) => {
  105. if (input.pluginId === ExpressionDatasourceRef.type) {
  106. return null;
  107. }
  108. const dataSourceOption = `dataSources[${index}]`;
  109. const current = watchDataSources ?? [];
  110. return (
  111. <Field
  112. label={input.label}
  113. key={dataSourceOption}
  114. invalid={errors.dataSources && !!errors.dataSources[index]}
  115. error={errors.dataSources && errors.dataSources[index] && 'A data source is required'}
  116. >
  117. <InputControl
  118. name={dataSourceOption as any}
  119. render={({ field: { ref, ...field } }) => (
  120. <DataSourcePicker
  121. {...field}
  122. noDefault={true}
  123. placeholder={input.info}
  124. pluginId={input.pluginId}
  125. current={current[index]?.uid}
  126. />
  127. )}
  128. control={control}
  129. rules={{ required: true }}
  130. />
  131. </Field>
  132. );
  133. })}
  134. {inputs.constants &&
  135. inputs.constants.map((input: DashboardInput, index) => {
  136. const constantIndex = `constants[${index}]`;
  137. return (
  138. <Field
  139. label={input.label}
  140. error={errors.constants && errors.constants[index] && `${input.label} needs a value`}
  141. invalid={errors.constants && !!errors.constants[index]}
  142. key={constantIndex}
  143. >
  144. <Input {...register(constantIndex as any, { required: true })} defaultValue={input.value} />
  145. </Field>
  146. );
  147. })}
  148. <ImportDashboardLibraryPanelsList
  149. inputs={newLibraryPanels}
  150. label="New library panels"
  151. description="List of new library panels that will get imported."
  152. folderName={watchFolder.title}
  153. />
  154. <ImportDashboardLibraryPanelsList
  155. inputs={existingLibraryPanels}
  156. label="Existing library panels"
  157. description="List of existing library panels. These panels are not affected by the import."
  158. folderName={watchFolder.title}
  159. />
  160. <HorizontalGroup>
  161. <Button
  162. type="submit"
  163. data-testid={selectors.components.ImportDashboardForm.submit}
  164. variant={getButtonVariant(errors)}
  165. onClick={() => {
  166. setSubmitted(true);
  167. }}
  168. >
  169. {getButtonText(errors)}
  170. </Button>
  171. <Button type="reset" variant="secondary" onClick={onCancel}>
  172. Cancel
  173. </Button>
  174. </HorizontalGroup>
  175. </>
  176. );
  177. };
  178. function getButtonVariant(errors: FormFieldErrors<ImportDashboardDTO>) {
  179. return errors && (errors.title || errors.uid) ? 'destructive' : 'primary';
  180. }
  181. function getButtonText(errors: FormFieldErrors<ImportDashboardDTO>) {
  182. return errors && (errors.title || errors.uid) ? 'Import (Overwrite)' : 'Import';
  183. }