AddPanelWidget.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import { css, cx, keyframes } from '@emotion/css';
  2. import { chain, cloneDeep, defaults, find, sortBy } from 'lodash';
  3. import React, { useMemo, useState } from 'react';
  4. import { connect, MapDispatchToProps } from 'react-redux';
  5. import tinycolor from 'tinycolor2';
  6. import { GrafanaTheme2 } from '@grafana/data';
  7. import { selectors } from '@grafana/e2e-selectors';
  8. import { locationService, reportInteraction } from '@grafana/runtime';
  9. import { Icon, IconButton, useStyles2 } from '@grafana/ui';
  10. import { CardButton } from 'app/core/components/CardButton';
  11. import config from 'app/core/config';
  12. import { LS_PANEL_COPY_KEY } from 'app/core/constants';
  13. import store from 'app/core/store';
  14. import { addPanel } from 'app/features/dashboard/state/reducers';
  15. import {
  16. LibraryPanelsSearch,
  17. LibraryPanelsSearchVariant,
  18. } from '../../../library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch';
  19. import { LibraryElementDTO } from '../../../library-panels/types';
  20. import { toPanelModelLibraryPanel } from '../../../library-panels/utils';
  21. import { DashboardModel, PanelModel } from '../../state';
  22. export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
  23. export interface OwnProps {
  24. panel: PanelModel;
  25. dashboard: DashboardModel;
  26. }
  27. export interface DispatchProps {
  28. addPanel: typeof addPanel;
  29. }
  30. export type Props = OwnProps & DispatchProps;
  31. const getCopiedPanelPlugins = () => {
  32. const panels = chain(config.panels)
  33. .filter({ hideFromList: false })
  34. .map((item) => item)
  35. .value();
  36. const copiedPanels = [];
  37. const copiedPanelJson = store.get(LS_PANEL_COPY_KEY);
  38. if (copiedPanelJson) {
  39. const copiedPanel = JSON.parse(copiedPanelJson);
  40. const pluginInfo: any = find(panels, { id: copiedPanel.type });
  41. if (pluginInfo) {
  42. const pluginCopy = cloneDeep(pluginInfo);
  43. pluginCopy.name = copiedPanel.title;
  44. pluginCopy.sort = -1;
  45. pluginCopy.defaults = copiedPanel;
  46. copiedPanels.push(pluginCopy);
  47. }
  48. }
  49. return sortBy(copiedPanels, 'sort');
  50. };
  51. export const AddPanelWidgetUnconnected: React.FC<Props> = ({ panel, dashboard }) => {
  52. const [addPanelView, setAddPanelView] = useState(false);
  53. const onCancelAddPanel = (evt: React.MouseEvent<HTMLButtonElement>) => {
  54. evt.preventDefault();
  55. dashboard.removePanel(panel);
  56. };
  57. const onBack = () => {
  58. setAddPanelView(false);
  59. };
  60. const onCreateNewPanel = () => {
  61. const { gridPos } = panel;
  62. const newPanel: Partial<PanelModel> = {
  63. type: 'timeseries',
  64. title: 'Panel Title',
  65. gridPos: { x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h },
  66. };
  67. dashboard.addPanel(newPanel);
  68. dashboard.removePanel(panel);
  69. locationService.partial({ editPanel: newPanel.id });
  70. };
  71. const onPasteCopiedPanel = (panelPluginInfo: PanelPluginInfo) => {
  72. const { gridPos } = panel;
  73. const newPanel: any = {
  74. type: panelPluginInfo.id,
  75. title: 'Panel Title',
  76. gridPos: {
  77. x: gridPos.x,
  78. y: gridPos.y,
  79. w: panelPluginInfo.defaults.gridPos.w,
  80. h: panelPluginInfo.defaults.gridPos.h,
  81. },
  82. };
  83. // apply panel template / defaults
  84. if (panelPluginInfo.defaults) {
  85. defaults(newPanel, panelPluginInfo.defaults);
  86. newPanel.title = panelPluginInfo.defaults.title;
  87. store.delete(LS_PANEL_COPY_KEY);
  88. }
  89. dashboard.addPanel(newPanel);
  90. dashboard.removePanel(panel);
  91. };
  92. const onAddLibraryPanel = (panelInfo: LibraryElementDTO) => {
  93. const { gridPos } = panel;
  94. const newPanel: PanelModel = {
  95. ...panelInfo.model,
  96. gridPos,
  97. libraryPanel: toPanelModelLibraryPanel(panelInfo),
  98. };
  99. dashboard.addPanel(newPanel);
  100. dashboard.removePanel(panel);
  101. };
  102. const onCreateNewRow = () => {
  103. const newRow: any = {
  104. type: 'row',
  105. title: 'Row title',
  106. gridPos: { x: 0, y: 0 },
  107. };
  108. dashboard.addPanel(newRow);
  109. dashboard.removePanel(panel);
  110. };
  111. const styles = useStyles2(getStyles);
  112. const copiedPanelPlugins = useMemo(() => getCopiedPanelPlugins(), []);
  113. return (
  114. <div className={styles.wrapper}>
  115. <div className={cx('panel-container', styles.callToAction)}>
  116. <AddPanelWidgetHandle onCancel={onCancelAddPanel} onBack={addPanelView ? onBack : undefined} styles={styles}>
  117. {addPanelView ? 'Add panel from panel library' : 'Add panel'}
  118. </AddPanelWidgetHandle>
  119. {addPanelView ? (
  120. <LibraryPanelsSearch onClick={onAddLibraryPanel} variant={LibraryPanelsSearchVariant.Tight} showPanelFilter />
  121. ) : (
  122. <div className={styles.actionsWrapper}>
  123. <CardButton
  124. icon="file-blank"
  125. aria-label={selectors.pages.AddDashboard.addNewPanel}
  126. onClick={() => {
  127. reportInteraction('Create new panel');
  128. onCreateNewPanel();
  129. }}
  130. >
  131. Add a new panel
  132. </CardButton>
  133. <CardButton
  134. icon="wrap-text"
  135. aria-label={selectors.pages.AddDashboard.addNewRow}
  136. onClick={() => {
  137. reportInteraction('Create new row');
  138. onCreateNewRow();
  139. }}
  140. >
  141. Add a new row
  142. </CardButton>
  143. <CardButton
  144. icon="book-open"
  145. aria-label={selectors.pages.AddDashboard.addNewPanelLibrary}
  146. onClick={() => {
  147. reportInteraction('Add a panel from the panel library');
  148. setAddPanelView(true);
  149. }}
  150. >
  151. Add a panel from the panel library
  152. </CardButton>
  153. {copiedPanelPlugins.length === 1 && (
  154. <CardButton
  155. icon="clipboard-alt"
  156. aria-label={selectors.pages.AddDashboard.addNewPanelLibrary}
  157. onClick={() => {
  158. reportInteraction('Paste panel from clipboard');
  159. onPasteCopiedPanel(copiedPanelPlugins[0]);
  160. }}
  161. >
  162. Paste panel from clipboard
  163. </CardButton>
  164. )}
  165. </div>
  166. )}
  167. </div>
  168. </div>
  169. );
  170. };
  171. const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { addPanel };
  172. export const AddPanelWidget = connect(undefined, mapDispatchToProps)(AddPanelWidgetUnconnected);
  173. interface AddPanelWidgetHandleProps {
  174. onCancel: (e: React.MouseEvent<HTMLButtonElement>) => void;
  175. onBack?: () => void;
  176. children?: string;
  177. styles: AddPanelStyles;
  178. }
  179. const AddPanelWidgetHandle: React.FC<AddPanelWidgetHandleProps> = ({ children, onBack, onCancel, styles }) => {
  180. return (
  181. <div className={cx(styles.headerRow, 'grid-drag-handle')}>
  182. {onBack && (
  183. <div className={styles.backButton}>
  184. <IconButton aria-label="Go back" name="arrow-left" onClick={onBack} size="xl" />
  185. </div>
  186. )}
  187. {!onBack && (
  188. <div className={styles.backButton}>
  189. <Icon name="panel-add" size="md" />
  190. </div>
  191. )}
  192. {children && <span>{children}</span>}
  193. <div className="flex-grow-1" />
  194. <IconButton aria-label="Close 'Add Panel' widget" name="times" onClick={onCancel} />
  195. </div>
  196. );
  197. };
  198. const getStyles = (theme: GrafanaTheme2) => {
  199. const pulsate = keyframes`
  200. 0% {box-shadow: 0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main};}
  201. 50% {box-shadow: 0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${tinycolor(
  202. theme.colors.primary.main
  203. )
  204. .darken(20)
  205. .toHexString()};}
  206. 100% {box-shadow: 0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${theme.colors.primary.main};}
  207. `;
  208. return {
  209. // wrapper is used to make sure box-shadow animation isn't cut off in dashboard page
  210. wrapper: css`
  211. height: 100%;
  212. padding-top: ${theme.spacing(0.5)};
  213. `,
  214. callToAction: css`
  215. overflow: hidden;
  216. outline: 2px dotted transparent;
  217. outline-offset: 2px;
  218. box-shadow: 0 0 0 2px black, 0 0 0px 4px #1f60c4;
  219. animation: ${pulsate} 2s ease infinite;
  220. `,
  221. actionsWrapper: css`
  222. height: 100%;
  223. display: grid;
  224. grid-template-columns: repeat(2, 1fr);
  225. column-gap: ${theme.spacing(1)};
  226. row-gap: ${theme.spacing(1)};
  227. padding: ${theme.spacing(0, 1, 1, 1)};
  228. // This is to make the last action full width (if by itself)
  229. & > div:nth-child(2n-1):nth-last-of-type(1) {
  230. grid-column: span 2;
  231. }
  232. `,
  233. headerRow: css`
  234. display: flex;
  235. align-items: center;
  236. height: 38px;
  237. flex-shrink: 0;
  238. width: 100%;
  239. font-size: ${theme.typography.fontSize};
  240. font-weight: ${theme.typography.fontWeightMedium};
  241. padding-left: ${theme.spacing(1)};
  242. transition: background-color 0.1s ease-in-out;
  243. cursor: move;
  244. &:hover {
  245. background: ${theme.colors.background.secondary};
  246. }
  247. `,
  248. backButton: css`
  249. display: flex;
  250. align-items: center;
  251. cursor: pointer;
  252. padding-left: ${theme.spacing(0.5)};
  253. width: ${theme.spacing(4)};
  254. `,
  255. noMargin: css`
  256. margin: 0;
  257. `,
  258. };
  259. };
  260. type AddPanelStyles = ReturnType<typeof getStyles>;