DashboardSettings.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { css, cx } from '@emotion/css';
  2. import { useDialog } from '@react-aria/dialog';
  3. import { FocusScope } from '@react-aria/focus';
  4. import { useOverlay } from '@react-aria/overlays';
  5. import React, { useCallback, useMemo, useRef } from 'react';
  6. import { Link } from 'react-router-dom';
  7. import { GrafanaTheme2, locationUtil } from '@grafana/data';
  8. import { locationService, reportInteraction } from '@grafana/runtime';
  9. import { Button, CustomScrollbar, Icon, IconName, PageToolbar, stylesFactory, useForceUpdate } from '@grafana/ui';
  10. import config from 'app/core/config';
  11. import { contextSrv } from 'app/core/services/context_srv';
  12. import { AccessControlAction } from 'app/types';
  13. import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer';
  14. import { DashboardModel } from '../../state/DashboardModel';
  15. import { AccessControlDashboardPermissions } from '../DashboardPermissions/AccessControlDashboardPermissions';
  16. import { DashboardPermissions } from '../DashboardPermissions/DashboardPermissions';
  17. import { SaveDashboardAsButton, SaveDashboardButton } from '../SaveDashboard/SaveDashboardButton';
  18. import { AnnotationsSettings } from './AnnotationsSettings';
  19. import { GeneralSettings } from './GeneralSettings';
  20. import { JsonEditorSettings } from './JsonEditorSettings';
  21. import { LinksSettings } from './LinksSettings';
  22. import { VersionsSettings } from './VersionsSettings';
  23. export interface Props {
  24. dashboard: DashboardModel;
  25. editview: string;
  26. }
  27. export interface SettingsPage {
  28. id: string;
  29. title: string;
  30. icon: IconName;
  31. component: React.ReactNode;
  32. }
  33. const onClose = () => locationService.partial({ editview: null });
  34. const MakeEditable = (props: { onMakeEditable: () => any }) => (
  35. <div>
  36. <div className="dashboard-settings__header">Dashboard not editable</div>
  37. <Button type="submit" onClick={props.onMakeEditable}>
  38. Make editable
  39. </Button>
  40. </div>
  41. );
  42. export function DashboardSettings({ dashboard, editview }: Props) {
  43. const ref = useRef<HTMLDivElement>(null);
  44. const { overlayProps } = useOverlay(
  45. {
  46. isOpen: true,
  47. onClose,
  48. },
  49. ref
  50. );
  51. const { dialogProps } = useDialog(
  52. {
  53. 'aria-label': 'Dashboard settings',
  54. },
  55. ref
  56. );
  57. const forceUpdate = useForceUpdate();
  58. const onMakeEditable = useCallback(() => {
  59. dashboard.editable = true;
  60. dashboard.meta.canMakeEditable = false;
  61. dashboard.meta.canEdit = true;
  62. dashboard.meta.canSave = true;
  63. forceUpdate();
  64. }, [dashboard, forceUpdate]);
  65. const pages = useMemo((): SettingsPage[] => {
  66. const pages: SettingsPage[] = [];
  67. if (dashboard.meta.canEdit) {
  68. pages.push({
  69. title: 'General',
  70. id: 'settings',
  71. icon: 'sliders-v-alt',
  72. component: <GeneralSettings dashboard={dashboard} />,
  73. });
  74. pages.push({
  75. title: 'Annotations',
  76. id: 'annotations',
  77. icon: 'comment-alt',
  78. component: <AnnotationsSettings dashboard={dashboard} />,
  79. });
  80. pages.push({
  81. title: 'Variables',
  82. id: 'templating',
  83. icon: 'calculator-alt',
  84. component: <VariableEditorContainer dashboard={dashboard} />,
  85. });
  86. pages.push({
  87. title: 'Links',
  88. id: 'links',
  89. icon: 'link',
  90. component: <LinksSettings dashboard={dashboard} />,
  91. });
  92. }
  93. if (dashboard.meta.canMakeEditable) {
  94. pages.push({
  95. title: 'General',
  96. icon: 'sliders-v-alt',
  97. id: 'settings',
  98. component: <MakeEditable onMakeEditable={onMakeEditable} />,
  99. });
  100. }
  101. if (dashboard.id && dashboard.meta.canSave) {
  102. pages.push({
  103. title: 'Versions',
  104. id: 'versions',
  105. icon: 'history',
  106. component: <VersionsSettings dashboard={dashboard} />,
  107. });
  108. }
  109. if (dashboard.id && dashboard.meta.canAdmin) {
  110. if (!config.rbacEnabled) {
  111. pages.push({
  112. title: 'Permissions',
  113. id: 'permissions',
  114. icon: 'lock',
  115. component: <DashboardPermissions dashboard={dashboard} />,
  116. });
  117. } else if (contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsRead)) {
  118. pages.push({
  119. title: 'Permissions',
  120. id: 'permissions',
  121. icon: 'lock',
  122. component: <AccessControlDashboardPermissions dashboard={dashboard} />,
  123. });
  124. }
  125. }
  126. pages.push({
  127. title: 'JSON Model',
  128. id: 'dashboard_json',
  129. icon: 'arrow',
  130. component: <JsonEditorSettings dashboard={dashboard} />,
  131. });
  132. return pages;
  133. }, [dashboard, onMakeEditable]);
  134. const onPostSave = () => {
  135. dashboard.meta.hasUnsavedFolderChange = false;
  136. };
  137. const folderTitle = dashboard.meta.folderTitle;
  138. const currentPage = pages.find((page) => page.id === editview) ?? pages[0];
  139. const canSaveAs = contextSrv.hasEditPermissionInFolders;
  140. const canSave = dashboard.meta.canSave;
  141. const styles = getStyles(config.theme2);
  142. return (
  143. <FocusScope contain autoFocus>
  144. <div className="dashboard-settings" ref={ref} {...overlayProps} {...dialogProps}>
  145. <PageToolbar title={`${dashboard.title} / Settings`} parent={folderTitle} onGoBack={onClose} />
  146. <CustomScrollbar>
  147. <div className={styles.scrollInner}>
  148. <div className={styles.settingsWrapper}>
  149. <aside className="dashboard-settings__aside">
  150. {pages.map((page) => (
  151. <Link
  152. onClick={() => reportInteraction(`Dashboard settings navigation to ${page.id}`)}
  153. to={(loc) => locationUtil.getUrlForPartial(loc, { editview: page.id })}
  154. className={cx('dashboard-settings__nav-item', { active: page.id === editview })}
  155. key={page.id}
  156. >
  157. <Icon name={page.icon} style={{ marginRight: '4px' }} />
  158. {page.title}
  159. </Link>
  160. ))}
  161. <div className="dashboard-settings__aside-actions">
  162. {canSave && <SaveDashboardButton dashboard={dashboard} onSaveSuccess={onPostSave} />}
  163. {canSaveAs && (
  164. <SaveDashboardAsButton dashboard={dashboard} onSaveSuccess={onPostSave} variant="secondary" />
  165. )}
  166. </div>
  167. </aside>
  168. <div className={styles.settingsContent}>{currentPage.component}</div>
  169. </div>
  170. </div>
  171. </CustomScrollbar>
  172. </div>
  173. </FocusScope>
  174. );
  175. }
  176. const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
  177. scrollInner: css`
  178. min-width: 100%;
  179. display: flex;
  180. `,
  181. settingsWrapper: css`
  182. margin: ${theme.spacing(0, 2, 2)};
  183. display: flex;
  184. flex-grow: 1;
  185. `,
  186. settingsContent: css`
  187. flex-grow: 1;
  188. height: 100%;
  189. padding: 32px;
  190. border: 1px solid ${theme.colors.border.weak};
  191. background: ${theme.colors.background.primary};
  192. border-radius: ${theme.shape.borderRadius()};
  193. `,
  194. }));