dashboard.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import React from 'react';
  2. import {closestCenter, DndContext} from '@dnd-kit/core';
  3. import {arrayMove, rectSortingStrategy, SortableContext} from '@dnd-kit/sortable';
  4. import styled from '@emotion/styled';
  5. import {openAddDashboardWidgetModal} from 'app/actionCreators/modal';
  6. import {loadOrganizationTags} from 'app/actionCreators/tags';
  7. import {Client} from 'app/api';
  8. import space from 'app/styles/space';
  9. import {GlobalSelection, Organization} from 'app/types';
  10. import withApi from 'app/utils/withApi';
  11. import withGlobalSelection from 'app/utils/withGlobalSelection';
  12. import AddWidget, {ADD_WIDGET_BUTTON_DRAG_ID} from './addWidget';
  13. import SortableWidget from './sortableWidget';
  14. import {DashboardDetails, Widget} from './types';
  15. type Props = {
  16. api: Client;
  17. organization: Organization;
  18. dashboard: DashboardDetails;
  19. paramDashboardId: string;
  20. selection: GlobalSelection;
  21. isEditing: boolean;
  22. /**
  23. * Fired when widgets are added/removed/sorted.
  24. */
  25. onUpdate: (widgets: Widget[]) => void;
  26. };
  27. class Dashboard extends React.Component<Props> {
  28. componentDidMount() {
  29. const {isEditing} = this.props;
  30. // Load organization tags when in edit mode.
  31. if (isEditing) {
  32. this.fetchTags();
  33. }
  34. }
  35. componentDidUpdate(prevProps: Props) {
  36. const {isEditing} = this.props;
  37. // Load organization tags when going into edit mode.
  38. // We use tags on the add widget modal.
  39. if (prevProps.isEditing !== isEditing && isEditing) {
  40. this.fetchTags();
  41. }
  42. }
  43. fetchTags() {
  44. const {api, organization, selection} = this.props;
  45. loadOrganizationTags(api, organization.slug, selection);
  46. }
  47. handleStartAdd = () => {
  48. const {organization, dashboard, selection} = this.props;
  49. openAddDashboardWidgetModal({
  50. organization,
  51. dashboard,
  52. selection,
  53. onAddWidget: this.handleAddComplete,
  54. });
  55. };
  56. handleAddComplete = (widget: Widget) => {
  57. this.props.onUpdate([...this.props.dashboard.widgets, widget]);
  58. };
  59. handleUpdateComplete = (index: number) => (nextWidget: Widget) => {
  60. const nextList = [...this.props.dashboard.widgets];
  61. nextList[index] = nextWidget;
  62. this.props.onUpdate(nextList);
  63. };
  64. handleDeleteWidget = (index: number) => () => {
  65. const nextList = [...this.props.dashboard.widgets];
  66. nextList.splice(index, 1);
  67. this.props.onUpdate(nextList);
  68. };
  69. handleEditWidget = (widget: Widget, index: number) => () => {
  70. const {organization, dashboard, selection} = this.props;
  71. openAddDashboardWidgetModal({
  72. organization,
  73. dashboard,
  74. widget,
  75. selection,
  76. onAddWidget: this.handleAddComplete,
  77. onUpdateWidget: this.handleUpdateComplete(index),
  78. });
  79. };
  80. getWidgetIds() {
  81. return [
  82. ...this.props.dashboard.widgets.map((widget, index): string => {
  83. return generateWidgetId(widget, index);
  84. }),
  85. ADD_WIDGET_BUTTON_DRAG_ID,
  86. ];
  87. }
  88. renderWidget(widget: Widget, index: number) {
  89. const {isEditing} = this.props;
  90. const key = generateWidgetId(widget, index);
  91. const dragId = key;
  92. return (
  93. <SortableWidget
  94. key={key}
  95. widget={widget}
  96. dragId={dragId}
  97. isEditing={isEditing}
  98. onDelete={this.handleDeleteWidget(index)}
  99. onEdit={this.handleEditWidget(widget, index)}
  100. />
  101. );
  102. }
  103. render() {
  104. const {
  105. isEditing,
  106. onUpdate,
  107. dashboard: {widgets},
  108. organization,
  109. paramDashboardId,
  110. } = this.props;
  111. const items = this.getWidgetIds();
  112. return (
  113. <DndContext
  114. collisionDetection={closestCenter}
  115. onDragEnd={({over, active}) => {
  116. const activeDragId = active.id;
  117. const getIndex = items.indexOf.bind(items);
  118. const activeIndex = activeDragId ? getIndex(activeDragId) : -1;
  119. if (over && over.id !== ADD_WIDGET_BUTTON_DRAG_ID) {
  120. const overIndex = getIndex(over.id);
  121. if (activeIndex !== overIndex) {
  122. onUpdate(arrayMove(widgets, activeIndex, overIndex));
  123. }
  124. }
  125. }}
  126. >
  127. <WidgetContainer>
  128. <SortableContext items={items} strategy={rectSortingStrategy}>
  129. {widgets.map((widget, index) => this.renderWidget(widget, index))}
  130. {isEditing && (
  131. <AddWidget
  132. dashboardId={paramDashboardId}
  133. orgSlug={organization.slug}
  134. orgFeatures={organization.features}
  135. onClick={this.handleStartAdd}
  136. />
  137. )}
  138. </SortableContext>
  139. </WidgetContainer>
  140. </DndContext>
  141. );
  142. }
  143. }
  144. export default withApi(withGlobalSelection(Dashboard));
  145. const WidgetContainer = styled('div')`
  146. display: grid;
  147. grid-template-columns: repeat(2, minmax(0, 1fr));
  148. grid-auto-flow: row dense;
  149. grid-gap: ${space(2)};
  150. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  151. grid-template-columns: repeat(4, minmax(0, 1fr));
  152. }
  153. @media (min-width: ${p => p.theme.breakpoints[3]}) {
  154. grid-template-columns: repeat(6, minmax(0, 1fr));
  155. }
  156. @media (min-width: ${p => p.theme.breakpoints[4]}) {
  157. grid-template-columns: repeat(8, minmax(0, 1fr));
  158. }
  159. `;
  160. function generateWidgetId(widget: Widget, index: number) {
  161. return widget.id ? `${widget.id}-index-${index}` : `index-${index}`;
  162. }