dashboard.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {Component} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {closestCenter, DndContext} from '@dnd-kit/core';
  4. import {arrayMove, rectSortingStrategy, SortableContext} from '@dnd-kit/sortable';
  5. import styled from '@emotion/styled';
  6. import {Location} from 'history';
  7. import {validateWidget} from 'app/actionCreators/dashboards';
  8. import {addErrorMessage} from 'app/actionCreators/indicator';
  9. import {openAddDashboardWidgetModal} from 'app/actionCreators/modal';
  10. import {loadOrganizationTags} from 'app/actionCreators/tags';
  11. import {Client} from 'app/api';
  12. import space from 'app/styles/space';
  13. import {GlobalSelection, Organization} from 'app/types';
  14. import withApi from 'app/utils/withApi';
  15. import withGlobalSelection from 'app/utils/withGlobalSelection';
  16. import {DataSet} from './widget/utils';
  17. import AddWidget, {ADD_WIDGET_BUTTON_DRAG_ID} from './addWidget';
  18. import SortableWidget from './sortableWidget';
  19. import {DashboardDetails, MAX_WIDGETS, Widget} from './types';
  20. type Props = {
  21. api: Client;
  22. organization: Organization;
  23. dashboard: DashboardDetails;
  24. selection: GlobalSelection;
  25. isEditing: boolean;
  26. router: InjectedRouter;
  27. location: Location;
  28. /**
  29. * Fired when widgets are added/removed/sorted.
  30. */
  31. onUpdate: (widgets: Widget[]) => void;
  32. onSetWidgetToBeUpdated: (widget: Widget) => void;
  33. paramDashboardId?: string;
  34. newWidget?: Widget;
  35. };
  36. class Dashboard extends Component<Props> {
  37. async componentDidMount() {
  38. const {isEditing} = this.props;
  39. // Load organization tags when in edit mode.
  40. if (isEditing) {
  41. this.fetchTags();
  42. }
  43. this.addNewWidget();
  44. }
  45. async componentDidUpdate(prevProps: Props) {
  46. const {isEditing, newWidget} = this.props;
  47. // Load organization tags when going into edit mode.
  48. // We use tags on the add widget modal.
  49. if (prevProps.isEditing !== isEditing && isEditing) {
  50. this.fetchTags();
  51. }
  52. if (newWidget !== prevProps.newWidget) {
  53. this.addNewWidget();
  54. }
  55. }
  56. async addNewWidget() {
  57. const {api, organization, newWidget} = this.props;
  58. if (newWidget) {
  59. try {
  60. await validateWidget(api, organization.slug, newWidget);
  61. this.handleAddComplete(newWidget);
  62. } catch (error) {
  63. // Don't do anything, widget isn't valid
  64. addErrorMessage(error);
  65. }
  66. }
  67. }
  68. fetchTags() {
  69. const {api, organization, selection} = this.props;
  70. loadOrganizationTags(api, organization.slug, selection);
  71. }
  72. handleStartAdd = () => {
  73. const {organization, dashboard, selection} = this.props;
  74. openAddDashboardWidgetModal({
  75. organization,
  76. dashboard,
  77. selection,
  78. onAddWidget: this.handleAddComplete,
  79. });
  80. };
  81. handleOpenWidgetBuilder = () => {
  82. const {router, paramDashboardId, organization, location} = this.props;
  83. if (paramDashboardId) {
  84. router.push({
  85. pathname: `/organizations/${organization.slug}/dashboard/${paramDashboardId}/widget/new/`,
  86. query: {
  87. ...location.query,
  88. dataSet: DataSet.EVENTS,
  89. },
  90. });
  91. return;
  92. }
  93. router.push({
  94. pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
  95. query: {
  96. ...location.query,
  97. dataSet: DataSet.EVENTS,
  98. },
  99. });
  100. };
  101. handleAddComplete = (widget: Widget) => {
  102. this.props.onUpdate([...this.props.dashboard.widgets, widget]);
  103. };
  104. handleUpdateComplete = (index: number) => (nextWidget: Widget) => {
  105. const nextList = [...this.props.dashboard.widgets];
  106. nextList[index] = nextWidget;
  107. this.props.onUpdate(nextList);
  108. };
  109. handleDeleteWidget = (index: number) => () => {
  110. const nextList = [...this.props.dashboard.widgets];
  111. nextList.splice(index, 1);
  112. this.props.onUpdate(nextList);
  113. };
  114. handleEditWidget = (widget: Widget, index: number) => () => {
  115. const {
  116. organization,
  117. dashboard,
  118. selection,
  119. router,
  120. location,
  121. paramDashboardId,
  122. onSetWidgetToBeUpdated,
  123. } = this.props;
  124. if (organization.features.includes('metrics')) {
  125. onSetWidgetToBeUpdated(widget);
  126. if (paramDashboardId) {
  127. router.push({
  128. pathname: `/organizations/${organization.slug}/dashboard/${paramDashboardId}/widget/${index}/edit/`,
  129. query: {
  130. ...location.query,
  131. dataSet: DataSet.EVENTS,
  132. },
  133. });
  134. return;
  135. }
  136. router.push({
  137. pathname: `/organizations/${organization.slug}/dashboards/new/widget/${index}/edit/`,
  138. query: {
  139. ...location.query,
  140. dataSet: DataSet.EVENTS,
  141. },
  142. });
  143. }
  144. openAddDashboardWidgetModal({
  145. organization,
  146. dashboard,
  147. widget,
  148. selection,
  149. onAddWidget: this.handleAddComplete,
  150. onUpdateWidget: this.handleUpdateComplete(index),
  151. });
  152. };
  153. getWidgetIds() {
  154. return [
  155. ...this.props.dashboard.widgets.map((widget, index): string => {
  156. return generateWidgetId(widget, index);
  157. }),
  158. ADD_WIDGET_BUTTON_DRAG_ID,
  159. ];
  160. }
  161. renderWidget(widget: Widget, index: number) {
  162. const {isEditing} = this.props;
  163. const key = generateWidgetId(widget, index);
  164. const dragId = key;
  165. return (
  166. <SortableWidget
  167. key={key}
  168. widget={widget}
  169. dragId={dragId}
  170. isEditing={isEditing}
  171. onDelete={this.handleDeleteWidget(index)}
  172. onEdit={this.handleEditWidget(widget, index)}
  173. />
  174. );
  175. }
  176. render() {
  177. const {
  178. isEditing,
  179. onUpdate,
  180. dashboard: {widgets},
  181. organization,
  182. } = this.props;
  183. const items = this.getWidgetIds();
  184. return (
  185. <DndContext
  186. collisionDetection={closestCenter}
  187. onDragEnd={({over, active}) => {
  188. const activeDragId = active.id;
  189. const getIndex = items.indexOf.bind(items);
  190. const activeIndex = activeDragId ? getIndex(activeDragId) : -1;
  191. if (over && over.id !== ADD_WIDGET_BUTTON_DRAG_ID) {
  192. const overIndex = getIndex(over.id);
  193. if (activeIndex !== overIndex) {
  194. onUpdate(arrayMove(widgets, activeIndex, overIndex));
  195. }
  196. }
  197. }}
  198. >
  199. <WidgetContainer>
  200. <SortableContext items={items} strategy={rectSortingStrategy}>
  201. {widgets.map((widget, index) => this.renderWidget(widget, index))}
  202. {isEditing && widgets.length < MAX_WIDGETS && (
  203. <AddWidget
  204. orgFeatures={organization.features}
  205. onAddWidget={this.handleStartAdd}
  206. onOpenWidgetBuilder={this.handleOpenWidgetBuilder}
  207. />
  208. )}
  209. </SortableContext>
  210. </WidgetContainer>
  211. </DndContext>
  212. );
  213. }
  214. }
  215. export default withApi(withGlobalSelection(Dashboard));
  216. const WidgetContainer = styled('div')`
  217. display: grid;
  218. grid-template-columns: repeat(2, minmax(0, 1fr));
  219. grid-auto-flow: row dense;
  220. grid-gap: ${space(2)};
  221. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  222. grid-template-columns: repeat(4, minmax(0, 1fr));
  223. }
  224. @media (min-width: ${p => p.theme.breakpoints[3]}) {
  225. grid-template-columns: repeat(6, minmax(0, 1fr));
  226. }
  227. @media (min-width: ${p => p.theme.breakpoints[4]}) {
  228. grid-template-columns: repeat(8, minmax(0, 1fr));
  229. }
  230. `;
  231. function generateWidgetId(widget: Widget, index: number) {
  232. return widget.id ? `${widget.id}-index-${index}` : `index-${index}`;
  233. }