dashboard.tsx 6.6 KB

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